diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a88649..7519e83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 1c68364..e6581bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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>` 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 136477c..85af001 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 93f5944..a7bd9e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -511,7 +511,6 @@ dependencies = [ "anyhow", "gtk4", "libadwaita", - "once_cell", "regex", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index 43f4803..addc30f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/config.rs b/src/config.rs index afe6ced..619c9a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use once_cell::sync::Lazy; +use std::sync::LazyLock; -pub static XDG_PATHS: Lazy = Lazy::new(XdgPaths::new); +pub static XDG_PATHS: LazyLock = LazyLock::new(XdgPaths::new); #[derive(Debug, Clone)] pub struct XdgPaths { diff --git a/src/desktop_entry/writer.rs b/src/desktop_entry/writer.rs index 69fe534..d840f0b 100644 --- a/src/desktop_entry/writer.rs +++ b/src/desktop_entry/writer.rs @@ -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; @@ -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 = Lazy::new(|| Regex::new(r"%[uUfFick]").unwrap()); + static FIELD_CODE_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"%[uUfFick]").unwrap()); let stripped = FIELD_CODE_PATTERN.replace_all(exec, ""); stripped.split_whitespace().collect::>().join(" ") } diff --git a/src/discovery/applications.rs b/src/discovery/applications.rs index 151237e..e28d96c 100644 --- a/src/discovery/applications.rs +++ b/src/discovery/applications.rs @@ -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()); diff --git a/src/model/autostart_entry.rs b/src/model/autostart_entry.rs index 4b6f2d5..51f499e 100644 --- a/src/model/autostart_entry.rs +++ b/src/model/autostart_entry.rs @@ -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() diff --git a/src/operations/create.rs b/src/operations/create.rs index 9cd7b59..4dfffe5 100644 --- a/src/operations/create.rs +++ b/src/operations/create.rs @@ -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 diff --git a/src/operations/delay.rs b/src/operations/delay.rs index f137016..0178e20 100644 --- a/src/operations/delay.rs +++ b/src/operations/delay.rs @@ -1,8 +1,9 @@ -use once_cell::sync::Lazy; +use std::sync::LazyLock; + use regex::Regex; -static DELAY_PATTERN: Lazy = - Lazy::new(|| Regex::new(r"^sh -c 'sleep (\d+) && exec (.+)'$").unwrap()); +static DELAY_PATTERN: LazyLock = + 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 { diff --git a/src/operations/toggle.rs b/src/operations/toggle.rs index e3f800b..7d7bfbe 100644 --- a/src/operations/toggle.rs +++ b/src/operations/toggle.rs @@ -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()); diff --git a/src/ui/app_chooser.rs b/src/ui/app_chooser.rs index 312a96f..225b326 100644 --- a/src/ui/app_chooser.rs +++ b/src/ui/app_chooser.rs @@ -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 { diff --git a/src/ui/autostart_row.rs b/src/ui/autostart_row.rs index 741d597..1489c3b 100644 --- a/src/ui/autostart_row.rs +++ b/src/ui/autostart_row.rs @@ -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 { @@ -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); diff --git a/src/ui/window.rs b/src/ui/window.rs index fca35a5..ea76162 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -135,6 +135,7 @@ impl MainWindow { &list_box_clone, &stack_clone, &toast_overlay_clone, + true, ); }); } @@ -275,6 +276,7 @@ impl MainWindow { list_box: >k4::ListBox, stack: >k4::Stack, toast_overlay: &adw::ToastOverlay, + show_toast: bool, ) { match discover_autostart_entries() { Ok(discovered) => { @@ -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); @@ -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); @@ -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); @@ -520,6 +526,7 @@ impl MainWindow { &list_box_clone, stack, &toast_overlay_clone, + false, ); } let toast = adw::Toast::new(&format!("Updated {}", entry_name)); @@ -540,7 +547,7 @@ impl MainWindow { fn handle_delete( path: PathBuf, - id: &str, + name: &str, entries: &Rc>>, list_box: >k4::ListBox, toast_overlay: &adw::ToastOverlay, @@ -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) => {