refactor(power): rename sys provider to power (D13)

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.
This commit is contained in:
2026-05-13 02:23:13 +02:00
parent cb2ea5973b
commit d1c327002b
7 changed files with 156 additions and 41 deletions
+42 -5
View File
@@ -103,7 +103,8 @@ pub struct ThemeColors {
pub badge_file: Option<String>,
pub badge_script: Option<String>,
pub badge_ssh: Option<String>,
pub badge_sys: Option<String>,
#[serde(alias = "badge_sys")]
pub badge_power: Option<String>,
pub badge_uuctl: Option<String>,
pub badge_web: Option<String>,
// 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"));
}
}
+3 -3
View File
@@ -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"),
+43 -4
View File
@@ -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.
@@ -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<LaunchItem>,
}
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
);
}
}
}
+4 -2
View File
@@ -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));
+3 -3
View File
@@ -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",
+4 -4
View File
@@ -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(),