From d1c327002b414b559b12e594a828fe612daafe0e Mon Sep 17 00:00:00 2001 From: vikingowl Date: Wed, 13 May 2026 02:23:13 +0200 Subject: [PATCH] refactor(power): rename sys provider to power (D13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per D13 in the v2 plan: 'sys' collides mentally with 'systemd'. The power & session provider becomes 'power' everywhere. Pre-v2 names ('sys', 'system', badge_sys) remain accepted as serde aliases so existing user configs keep parsing. Also fixes a pre-existing bug: filter.rs mapped :sys/:system/:power prefixes to the type_id 'system', but the provider exposed itself as Plugin('sys'). The two never matched, so prefix filtering on this provider was a no-op. Now everything (filter table, ProviderType FromStr, provider's own provider_type, config key) agrees on 'power'. Files renamed: providers/system.rs -> providers/power.rs Struct renamed: SystemProvider -> PowerProvider type_id: 'sys' -> 'power' Item IDs: 'sys:shutdown' -> 'power:shutdown' (frecency for the 7 power items resets after upgrade — acceptable for a v2 break) Config key: providers.system -> providers.power (alias 'system', 'sys') Theme color: colors.badge_sys -> colors.badge_power (alias 'badge_sys'). theme.rs emits both --owlry-badge-power and --owlry-badge-sys so existing stylesheets keep rendering. UI provider_meta: 'system' arm becomes 'power' | 'system' ProviderType::FromStr: 'power', 'sys', 'system' all -> Plugin('power') (and 'uuctl', 'systemd' -> Plugin('uuctl') as parallel hygiene) Tests added (TDD): - provider_type_from_str_maps_power_aliases - provider_type_from_str_maps_systemd_aliases - providers_config_accepts_power_key - providers_config_accepts_pre_v2_system_alias - theme_colors_accepts_pre_v2_badge_sys_alias - all_item_ids_use_power_prefix (in power.rs) 239 tests pass (up from 234) with --features full. Task #8 complete. --- crates/owlry/src/config/mod.rs | 47 +++++++++-- crates/owlry/src/filter.rs | 6 +- crates/owlry/src/providers/mod.rs | 47 ++++++++++- .../src/providers/{system.rs => power.rs} | 77 ++++++++++++++----- crates/owlry/src/theme.rs | 6 +- crates/owlry/src/ui/main_window.rs | 6 +- crates/owlry/src/ui/provider_meta.rs | 8 +- 7 files changed, 156 insertions(+), 41 deletions(-) rename crates/owlry/src/providers/{system.rs => power.rs} (62%) diff --git a/crates/owlry/src/config/mod.rs b/crates/owlry/src/config/mod.rs index 48d158a..ecde8c6 100644 --- a/crates/owlry/src/config/mod.rs +++ b/crates/owlry/src/config/mod.rs @@ -103,7 +103,8 @@ pub struct ThemeColors { pub badge_file: Option, pub badge_script: Option, pub badge_ssh: Option, - pub badge_sys: Option, + #[serde(alias = "badge_sys")] + pub badge_power: Option, pub badge_uuctl: Option, pub badge_web: Option, // Widget badge colors @@ -168,9 +169,10 @@ pub struct ProvidersConfig { /// Enable built-in unit/currency converter (> trigger) #[serde(default = "default_true")] pub converter: bool, - /// Enable built-in system actions (shutdown, reboot, lock, etc.) - #[serde(default = "default_true")] - pub system: bool, + /// Enable built-in power actions (shutdown, reboot, lock, etc.) + /// Pre-v2 config key `system` is still accepted as an alias. + #[serde(default = "default_true", alias = "system", alias = "sys")] + pub power: bool, /// Enable systemd user units provider (alias: uuctl) #[serde(default = "default_true", alias = "uuctl")] pub systemd: bool, @@ -212,7 +214,7 @@ impl Default for ProvidersConfig { commands: true, calculator: true, converter: true, - system: true, + power: true, systemd: true, bookmarks: true, clipboard: true, @@ -612,3 +614,38 @@ mod tests { assert!(!super::command_exists("owlry_nonexistent_binary_abc123")); } } + +#[cfg(test)] +mod v2_rename_tests { + use super::*; + + #[test] + fn providers_config_accepts_power_key() { + let toml = r#"[providers] +power = false +"#; + let cfg: Config = toml::from_str(toml).expect("must parse"); + assert!(!cfg.providers.power); + } + + #[test] + fn providers_config_accepts_pre_v2_system_alias() { + // Pre-v2 configs used `system = ...`. Serde alias keeps them working. + let toml = r#"[providers] +system = false +"#; + let cfg: Config = toml::from_str(toml).expect("must parse"); + assert!(!cfg.providers.power); + } + + #[test] + fn theme_colors_accepts_pre_v2_badge_sys_alias() { + // Pre-v2 stylesheets named the color `badge_sys`. Serde alias keeps + // existing user configs working. + let toml = r##"[appearance.colors] +badge_sys = "#ff8800" +"##; + let cfg: Config = toml::from_str(toml).expect("must parse"); + assert_eq!(cfg.appearance.colors.badge_power.as_deref(), Some("#ff8800")); + } +} diff --git a/crates/owlry/src/filter.rs b/crates/owlry/src/filter.rs index 1f4f0c4..90973d8 100644 --- a/crates/owlry/src/filter.rs +++ b/crates/owlry/src/filter.rs @@ -241,9 +241,9 @@ impl ProviderFilter { ("script", "scripts"), ("scripts", "scripts"), ("ssh", "ssh"), - ("sys", "system"), - ("system", "system"), - ("power", "system"), + ("sys", "power"), + ("system", "power"), + ("power", "power"), ("uuctl", "uuctl"), ("systemd", "uuctl"), ("web", "websearch"), diff --git a/crates/owlry/src/providers/mod.rs b/crates/owlry/src/providers/mod.rs index 257db2d..37381ab 100644 --- a/crates/owlry/src/providers/mod.rs +++ b/crates/owlry/src/providers/mod.rs @@ -4,7 +4,7 @@ mod command; pub mod dmenu; pub(crate) mod calculator; pub(crate) mod converter; -pub(crate) mod system; +pub(crate) mod power; // Optional feature-gated providers #[cfg(feature = "bookmarks")] @@ -137,6 +137,10 @@ impl std::str::FromStr for ProviderType { "app" | "apps" | "application" | "applications" => Ok(ProviderType::Application), "cmd" | "cmds" | "command" | "commands" => Ok(ProviderType::Command), "dmenu" => Ok(ProviderType::Dmenu), + // Power provider — `sys` and `system` are pre-v2 muscle-memory aliases. + "power" | "sys" | "system" => Ok(ProviderType::Plugin("power".into())), + // systemd provider — type_id is `uuctl` (CLI back-compat). + "uuctl" | "systemd" => Ok(ProviderType::Plugin("uuctl".into())), other => Ok(ProviderType::Plugin(other.to_string())), } } @@ -268,9 +272,9 @@ impl ProviderManager { Box::new(CommandProvider::new()), ]; - if cfg_snapshot.system { - core_providers.push(Box::new(system::SystemProvider::new())); - info!("Registered built-in system provider"); + if cfg_snapshot.power { + core_providers.push(Box::new(power::PowerProvider::new())); + info!("Registered built-in power provider"); } #[cfg(feature = "bookmarks")] @@ -937,6 +941,41 @@ mod tests { assert_eq!(ProviderPosition::Widget.as_str(), "widget"); } + #[test] + fn provider_type_from_str_maps_power_aliases() { + // v2 renamed `sys` -> `power`. `sys` and `system` are kept as aliases + // for muscle memory; they must all resolve to Plugin("power"), never + // Plugin("sys") or Plugin("system"). + use std::str::FromStr; + assert_eq!( + ProviderType::from_str("power").unwrap(), + ProviderType::Plugin("power".into()) + ); + assert_eq!( + ProviderType::from_str("sys").unwrap(), + ProviderType::Plugin("power".into()) + ); + assert_eq!( + ProviderType::from_str("system").unwrap(), + ProviderType::Plugin("power".into()) + ); + } + + #[test] + fn provider_type_from_str_maps_systemd_aliases() { + // systemd provider's type_id is `uuctl` (CLI back-compat from pre-v2). + // Both `uuctl` and `systemd` must resolve to Plugin("uuctl"). + use std::str::FromStr; + assert_eq!( + ProviderType::from_str("uuctl").unwrap(), + ProviderType::Plugin("uuctl".into()) + ); + assert_eq!( + ProviderType::from_str("systemd").unwrap(), + ProviderType::Plugin("uuctl".into()) + ); + } + #[test] fn provider_type_from_str_accepts_plural_aliases() { // After the demolition, FromStr accepts both singular and plural aliases. diff --git a/crates/owlry/src/providers/system.rs b/crates/owlry/src/providers/power.rs similarity index 62% rename from crates/owlry/src/providers/system.rs rename to crates/owlry/src/providers/power.rs index a7d3c6f..9003bf9 100644 --- a/crates/owlry/src/providers/system.rs +++ b/crates/owlry/src/providers/power.rs @@ -1,13 +1,20 @@ use super::{ItemSource, LaunchItem, Provider, ProviderType}; -/// Built-in system provider. Returns a fixed set of power and session management actions. +/// Built-in power & session provider. Returns a fixed set of shutdown / reboot / +/// suspend / lock / logout actions. /// -/// This is a static provider — items are populated in `new()` and `refresh()` is a no-op. -pub(crate) struct SystemProvider { +/// Static provider — items are populated in `new()` and `refresh()` is a no-op. +/// +/// type_id is `"power"`. CLI aliases `:sys` and `:system` still resolve here for +/// muscle-memory back-compat (see [`crate::providers::ProviderType::FromStr`] +/// and [`crate::filter::ProviderFilter::parse_query`]). +pub(crate) struct PowerProvider { items: Vec, } -impl SystemProvider { +const TYPE_ID: &str = "power"; + +impl PowerProvider { pub fn new() -> Self { let commands: &[(&str, &str, &str, &str, &str)] = &[ ( @@ -64,14 +71,14 @@ impl SystemProvider { let items = commands .iter() .map(|(action_id, name, description, icon, command)| LaunchItem { - id: format!("sys:{}", action_id), + id: format!("power:{}", action_id), name: name.to_string(), description: Some(description.to_string()), icon: Some(icon.to_string()), - provider: ProviderType::Plugin("sys".into()), + provider: ProviderType::Plugin(TYPE_ID.into()), command: command.to_string(), terminal: false, - tags: vec!["system".into()], + tags: vec!["power".into()], source: ItemSource::Core, }) .collect(); @@ -80,13 +87,13 @@ impl SystemProvider { } } -impl Provider for SystemProvider { +impl Provider for PowerProvider { fn name(&self) -> &str { - "System" + "Power" } fn provider_type(&self) -> ProviderType { - ProviderType::Plugin("sys".into()) + ProviderType::Plugin(TYPE_ID.into()) } fn refresh(&mut self) { @@ -96,6 +103,22 @@ impl Provider for SystemProvider { fn items(&self) -> &[LaunchItem] { &self.items } + + fn prefix(&self) -> Option<&str> { + Some(":power") + } + + fn icon(&self) -> &str { + "system-shutdown" + } + + fn tab_label(&self) -> Option<&str> { + Some("Power") + } + + fn search_noun(&self) -> Option<&str> { + Some("power actions") + } } #[cfg(test)] @@ -104,13 +127,13 @@ mod tests { #[test] fn has_seven_actions() { - let provider = SystemProvider::new(); + let provider = PowerProvider::new(); assert_eq!(provider.items().len(), 7); } #[test] fn contains_expected_action_names() { - let provider = SystemProvider::new(); + let provider = PowerProvider::new(); let names: Vec<&str> = provider.items().iter().map(|i| i.name.as_str()).collect(); assert!(names.contains(&"Shutdown")); assert!(names.contains(&"Reboot")); @@ -119,14 +142,14 @@ mod tests { } #[test] - fn provider_type_is_sys_plugin() { - let provider = SystemProvider::new(); - assert_eq!(provider.provider_type(), ProviderType::Plugin("sys".into())); + fn provider_type_is_power_plugin() { + let provider = PowerProvider::new(); + assert_eq!(provider.provider_type(), ProviderType::Plugin("power".into())); } #[test] fn shutdown_command_is_correct() { - let provider = SystemProvider::new(); + let provider = PowerProvider::new(); let shutdown = provider .items() .iter() @@ -136,14 +159,28 @@ mod tests { } #[test] - fn all_items_have_system_tag() { - let provider = SystemProvider::new(); + fn all_items_have_power_tag() { + let provider = PowerProvider::new(); for item in provider.items() { assert!( - item.tags.contains(&"system".to_string()), - "item '{}' is missing 'system' tag", + item.tags.contains(&"power".to_string()), + "item '{}' is missing 'power' tag", item.name ); } } + + #[test] + fn all_item_ids_use_power_prefix() { + // Frecency / IPC compatibility check: every item id must start with + // the canonical "power:" prefix after the v2 rename. + let provider = PowerProvider::new(); + for item in provider.items() { + assert!( + item.id.starts_with("power:"), + "item id '{}' should start with 'power:'", + item.id + ); + } + } } diff --git a/crates/owlry/src/theme.rs b/crates/owlry/src/theme.rs index b3f4db6..e516e69 100644 --- a/crates/owlry/src/theme.rs +++ b/crates/owlry/src/theme.rs @@ -71,8 +71,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String { if let Some(ref badge_ssh) = config.colors.badge_ssh { css.push_str(&format!(" --owlry-badge-ssh: {};\n", badge_ssh)); } - if let Some(ref badge_sys) = config.colors.badge_sys { - css.push_str(&format!(" --owlry-badge-sys: {};\n", badge_sys)); + if let Some(ref badge_power) = config.colors.badge_power { + // Emit both for transition: pre-v2 stylesheets reference --owlry-badge-sys. + css.push_str(&format!(" --owlry-badge-power: {};\n", badge_power)); + css.push_str(&format!(" --owlry-badge-sys: {};\n", badge_power)); } if let Some(ref badge_uuctl) = config.colors.badge_uuctl { css.push_str(&format!(" --owlry-badge-uuctl: {};\n", badge_uuctl)); diff --git a/crates/owlry/src/ui/main_window.rs b/crates/owlry/src/ui/main_window.rs index 291edce..109f40d 100644 --- a/crates/owlry/src/ui/main_window.rs +++ b/crates/owlry/src/ui/main_window.rs @@ -396,8 +396,8 @@ impl MainWindow { if config.converter { parts.push("> conv".to_string()); } - if config.system { - parts.push(":sys".to_string()); + if config.power { + parts.push(":power".to_string()); } parts.join(" ") @@ -595,7 +595,7 @@ impl MainWindow { "pomodoro" => "pomodoro", "scripts" => "scripts", "ssh" => "SSH hosts", - "system" => "system", + "power" | "system" => "power actions", "uuctl" => "uuctl units", "weather" => "weather", "websearch" => "web", diff --git a/crates/owlry/src/ui/provider_meta.rs b/crates/owlry/src/ui/provider_meta.rs index 2f4c517..12cb161 100644 --- a/crates/owlry/src/ui/provider_meta.rs +++ b/crates/owlry/src/ui/provider_meta.rs @@ -86,10 +86,10 @@ fn hardcoded(provider: &ProviderType) -> ProviderMeta { css_class: "owlry-filter-ssh".to_string(), search_noun: "SSH hosts".to_string(), }, - "system" => ProviderMeta { - tab_label: "System".to_string(), - css_class: "owlry-filter-sys".to_string(), - search_noun: "system".to_string(), + "power" | "system" => ProviderMeta { + tab_label: "Power".to_string(), + css_class: "owlry-filter-power".to_string(), + search_noun: "power actions".to_string(), }, "uuctl" => ProviderMeta { tab_label: "uuctl".to_string(),