Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --check
- run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libadwaita-1-dev
- run: cargo clippy -- -D warnings
- run: cargo test
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ cargo test
- **Error handling**: `anyhow::Result` everywhere, `.with_context()` for path-related errors
- **File I/O**: Always use `write_atomic()` (temp file + rename) for writing .desktop files
- **GTK state**: `Rc<RefCell<T>>` for shared mutable state in callbacks — this is standard GTK-rs
- **Lazy statics**: `once_cell::sync::Lazy` for compiled regexes and XDG paths
- **Lazy statics**: `std::sync::LazyLock` for compiled regexes and XDG paths
- **Desktop entry format**: INI-like with `[Desktop Entry]` section header, key=value pairs
- **Startup delay**: Wraps exec as `sh -c 'sleep N && exec CMD'`, parsed back with regex
- **Field codes**: `%u`, `%U`, `%f`, `%F`, `%i`, `%c`, `%k` are stripped from Exec lines (meaningless for autostart)
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Thanks for your interest in contributing to Onset!

- Open a PR against `main`. CI must pass.
- Keep changes focused. One fix or feature per PR.
- Run `cargo fmt` before committing.
- Run `cargo fmt` and `cargo clippy` before committing.

## Reporting Issues

Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ anyhow = "1.0.100"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
regex = "1.12"
once_cell = "1.21"

[profile.release]
lto = true
Expand Down
4 changes: 2 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::path::PathBuf;

use once_cell::sync::Lazy;
use std::sync::LazyLock;

pub static XDG_PATHS: Lazy<XdgPaths> = Lazy::new(XdgPaths::new);
pub static XDG_PATHS: LazyLock<XdgPaths> = LazyLock::new(XdgPaths::new);

#[derive(Debug, Clone)]
pub struct XdgPaths {
Expand Down
6 changes: 4 additions & 2 deletions src/desktop_entry/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use std::io::Write;
use std::path::Path;

use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use std::sync::LazyLock;

use regex::Regex;

use super::parser::escape_value;
Expand All @@ -14,7 +15,8 @@ use crate::operations::delay::{unwrap_delay, wrap_with_delay};
/// Exec line. These are placeholders for file/URL arguments that are
/// meaningless in an autostart context where no file or URL is being opened.
pub(crate) fn strip_field_codes(exec: &str) -> String {
static FIELD_CODE_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r"%[uUfFick]").unwrap());
static FIELD_CODE_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"%[uUfFick]").unwrap());
let stripped = FIELD_CODE_PATTERN.replace_all(exec, "");
stripped.split_whitespace().collect::<Vec<_>>().join(" ")
}
Expand Down
8 changes: 4 additions & 4 deletions src/discovery/applications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ fn scan_application_dir(
continue;
}

if let Some(ref try_exec) = desktop_entry.try_exec {
if !binary_exists(try_exec) {
continue;
}
if let Some(ref try_exec) = desktop_entry.try_exec
&& !binary_exists(try_exec)
{
continue;
}

seen_ids.insert(id.clone());
Expand Down
8 changes: 4 additions & 4 deletions src/model/autostart_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ impl AutostartEntry {
return EffectiveState::Disabled;
}

if let Some(ref try_exec) = self.desktop_entry.try_exec {
if !binary_exists(try_exec) {
return EffectiveState::TryExecFailed;
}
if let Some(ref try_exec) = self.desktop_entry.try_exec
&& !binary_exists(try_exec)
{
return EffectiveState::TryExecFailed;
}

if !self.desktop_entry.only_show_in.is_empty()
Expand Down
11 changes: 1 addition & 10 deletions src/operations/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,9 @@ fn find_unique_path(base_id: &str) -> PathBuf {
return base_path;
}

for i in 1..1000 {
let suffixed_path = XDG_PATHS
.user_autostart
.join(format!("{}_{}.desktop", base_id, i));
if !suffixed_path.exists() {
return suffixed_path;
}
}

let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.map(|d| d.as_millis())
.unwrap_or(0);
XDG_PATHS
.user_autostart
Expand Down
7 changes: 4 additions & 3 deletions src/operations/delay.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use once_cell::sync::Lazy;
use std::sync::LazyLock;

use regex::Regex;

static DELAY_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^sh -c 'sleep (\d+) && exec (.+)'$").unwrap());
static DELAY_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^sh -c 'sleep (\d+) && exec (.+)'$").unwrap());

pub fn wrap_with_delay(exec: &str, delay_seconds: u32) -> String {
if delay_seconds == 0 {
Expand Down
17 changes: 8 additions & 9 deletions src/operations/toggle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ pub fn set_entry_enabled_by_path(path: &Path, enabled: bool) -> Result<()> {
continue;
}

if in_desktop_entry {
if let Some((key, _)) = trimmed.split_once('=') {
if key.trim() == "Hidden" {
if !enabled {
lines.push("Hidden=true".to_string());
hidden_written = true;
}
continue;
}
if in_desktop_entry
&& let Some((key, _)) = trimmed.split_once('=')
&& key.trim() == "Hidden"
{
if !enabled {
lines.push("Hidden=true".to_string());
hidden_written = true;
}
continue;
}

lines.push(line.to_string());
Expand Down
1 change: 1 addition & 0 deletions src/ui/app_chooser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ impl AppChooserDialog {
let row = adw::ActionRow::builder()
.title(&app.name)
.activatable(true)
.use_markup(false)
.build();

if let Some(ref comment) = app.comment {
Expand Down
5 changes: 3 additions & 2 deletions src/ui/autostart_row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ where
let row = adw::ActionRow::builder()
.title(&entry.desktop_entry.name)
.activatable(true)
.use_markup(false)
.build();

if let Some(ref comment) = entry.desktop_entry.comment {
Expand Down Expand Up @@ -143,9 +144,9 @@ where
.build();

let delete_path = entry.path.clone();
let delete_id = entry.id.clone();
let delete_name = entry.desktop_entry.name.clone();
delete_button.connect_clicked(move |_| {
on_delete(delete_path.clone(), delete_id.clone());
on_delete(delete_path.clone(), delete_name.clone());
});

row.add_suffix(&delete_button);
Expand Down
23 changes: 15 additions & 8 deletions src/ui/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ impl MainWindow {
&list_box_clone,
&stack_clone,
&toast_overlay_clone,
true,
);
});
}
Expand Down Expand Up @@ -275,6 +276,7 @@ impl MainWindow {
list_box: &gtk4::ListBox,
stack: &gtk4::Stack,
toast_overlay: &adw::ToastOverlay,
show_toast: bool,
) {
match discover_autostart_entries() {
Ok(discovered) => {
Expand Down Expand Up @@ -333,8 +335,10 @@ impl MainWindow {
stack.set_visible_child_name("list");
}

let toast = adw::Toast::new("Entries refreshed");
toast_overlay.add_toast(toast);
if show_toast {
let toast = adw::Toast::new("Entries refreshed");
toast_overlay.add_toast(toast);
}
}
Err(e) => {
tracing::error!("Failed to refresh entries: {}", e);
Expand Down Expand Up @@ -377,6 +381,7 @@ impl MainWindow {
&list_box_clone,
&stack_clone,
&toast_overlay_clone,
false,
);
let toast = adw::Toast::new(&format!("Added {}", app.name));
toast_overlay_clone.add_toast(toast);
Expand Down Expand Up @@ -431,6 +436,7 @@ impl MainWindow {
&list_box_clone,
&stack_clone,
&toast_overlay_clone,
false,
);
let toast = adw::Toast::new(&format!("Created {}", name));
toast_overlay_clone.add_toast(toast);
Expand Down Expand Up @@ -520,6 +526,7 @@ impl MainWindow {
&list_box_clone,
stack,
&toast_overlay_clone,
false,
);
}
let toast = adw::Toast::new(&format!("Updated {}", entry_name));
Expand All @@ -540,7 +547,7 @@ impl MainWindow {

fn handle_delete(
path: PathBuf,
id: &str,
name: &str,
entries: &Rc<RefCell<Vec<AutostartEntry>>>,
list_box: &gtk4::ListBox,
toast_overlay: &adw::ToastOverlay,
Expand All @@ -552,13 +559,13 @@ impl MainWindow {
let delete_index = { entries.borrow().iter().position(|e| e.path == path) };
entries.borrow_mut().retain(|e| e.path != path);

if let Some(index) = delete_index {
if let Some(row) = list_box.row_at_index(index as i32) {
list_box.remove(&row);
}
if let Some(index) = delete_index
&& let Some(row) = list_box.row_at_index(index as i32)
{
list_box.remove(&row);
}

let toast = adw::Toast::new(&format!("Deleted {}", id));
let toast = adw::Toast::new(&format!("Deleted {}", name));
toast_overlay.add_toast(toast);
}
Err(e) => {
Expand Down