From ad58dc95920d70d79768125e19ae4ad7443346a0 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sat, 10 Jan 2026 14:50:04 +0100 Subject: [PATCH 01/16] Added battery check to doctor --- Cargo.lock | 123 ++++++++++++++++++++++++++++------- Cargo.toml | 1 + src/doctor/checks.rs | 2 + src/doctor/checks/energy.rs | 124 ++++++++++++++++++++++++++++++++++++ src/doctor/registry.rs | 3 + 5 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 src/doctor/checks/energy.rs diff --git a/Cargo.lock b/Cargo.lock index 48dfecc2..7cd6ec29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,29 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "battery" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b624268937c0e0a3edb7c27843f9e547c320d730c610d3b8e6e8e95b2026e4" +dependencies = [ + "cfg-if", + "core-foundation 0.7.0", + "lazycell", + "libc", + "mach", + "nix 0.19.1", + "num-traits", + "uom", + "winapi", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -305,16 +328,32 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -336,7 +375,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "mio", "parking_lot", @@ -352,7 +391,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "crossterm_winapi", "derive_more", "document-features", @@ -818,7 +857,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -1008,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.7", "iana-time-zone-haiku", "js-sys", "log", @@ -1181,6 +1220,7 @@ dependencies = [ "anyhow", "async-trait", "base64", + "battery", "chrono", "clap", "clap_complete", @@ -1195,7 +1235,7 @@ dependencies = [ "git2", "indicatif", "lazy_static", - "nix", + "nix 0.29.0", "once_cell", "pathdiff", "pulldown-cmark", @@ -1306,6 +1346,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.177" @@ -1332,7 +1378,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -1421,6 +1467,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.7.6" @@ -1472,13 +1527,25 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", +] + [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -1511,7 +1578,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1673,7 +1740,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags", + "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -1737,7 +1804,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cassowary", "compact_str", "crossterm 0.28.1", @@ -1758,7 +1825,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -1875,7 +1942,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1890,7 +1957,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1903,7 +1970,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -2000,9 +2067,9 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", "libc", "security-framework-sys", ] @@ -2013,7 +2080,7 @@ version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] @@ -2343,8 +2410,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.10.0", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2354,7 +2421,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.7", "libc", ] @@ -2542,7 +2609,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2668,6 +2735,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "uom" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed" +dependencies = [ + "num-traits", + "typenum", +] + [[package]] name = "url" version = "2.5.7" diff --git a/Cargo.toml b/Cargo.toml index cf3f81ef..839b1553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ serde_yaml = "0.9.34" nix = { version = "0.29", features = ["fs"] } pathdiff = "0.2" urlencoding = "2.1.3" +battery = "0.7.8" [dev-dependencies] serial_test = "3" diff --git a/src/doctor/checks.rs b/src/doctor/checks.rs index 7a1e5fd9..6f54736b 100644 --- a/src/doctor/checks.rs +++ b/src/doctor/checks.rs @@ -12,6 +12,7 @@ use crate::doctor::{CheckStatus, DoctorCheck, PrivilegeLevel}; pub mod display; +pub mod energy; pub mod locale; pub mod nerdfont; pub mod network; @@ -22,6 +23,7 @@ pub mod tools; // Re-export all check types for easy access pub use display::SwayDisplayCheck; +pub use energy::PowerCheck; pub use locale::LocaleCheck; pub use nerdfont::NerdFontCheck; pub use network::{InstantRepoCheck, InternetCheck}; diff --git a/src/doctor/checks/energy.rs b/src/doctor/checks/energy.rs new file mode 100644 index 00000000..689dab8d --- /dev/null +++ b/src/doctor/checks/energy.rs @@ -0,0 +1,124 @@ +use crate::doctor::{CheckStatus, DoctorCheck}; +use async_trait::async_trait; +use battery::Battery; + +#[async_trait] +trait BatteryCheck: DoctorCheck { + /// Called asynchronously with batteries containing at least one item + fn check_parameter(&self, batteries: Vec) -> CheckStatus; + + /// Ensures that at least one battery is present + async fn execute(&self) -> CheckStatus { + if let Ok(manager) = battery::Manager::new() + && let Ok(maybe_batteries) = manager.batteries() + { + let batteries = maybe_batteries + .filter(|maybe_battery| maybe_battery.is_ok()) + .map(|battery| battery.unwrap()) + .collect::>(); + if batteries.is_empty() { + return CheckStatus::Skipped("No batteries found".into()); + } + + self.check_parameter(batteries) + } else { + CheckStatus::Fail { + message: "Could not initialize battery manager".to_string(), + fixable: false, + } + } + } + + fn format_battery(&self, battery: &Battery) -> String { + format!( + "{} ({})", + battery.model().unwrap_or("Unknown model"), + battery.serial_number().unwrap_or("Unknown S/N"), + ) + } +} + +#[derive(Default)] +pub struct PowerCheck; + +#[async_trait] +impl DoctorCheck for PowerCheck { + fn name(&self) -> &'static str { + "Power level".into() + } + + fn id(&self) -> &'static str { + "power".into() + } + + async fn execute(&self) -> CheckStatus { + BatteryCheck::execute(self).await + } +} + +impl BatteryCheck for PowerCheck { + fn check_parameter(&self, mut batteries: Vec) -> CheckStatus { + // Ordering by percentage + batteries.sort_by(|b1, b2| { + b1.state_of_charge() + .value + .total_cmp(&b2.state_of_charge().value) + }); + + // Get battery with the lowest charge + let lowest = batteries.first().unwrap(); + let lowest_charge = lowest.state_of_charge(); + let battery_str = self.format_battery(lowest); + let percent = (lowest_charge.value * 100.0) as u64; + match lowest_charge.value { + 0.0..0.25 => CheckStatus::Fail { + message: format!("{} - Critical power: {}%", battery_str, percent), + fixable: false, + }, + 0.25..0.5 => CheckStatus::Warning { + message: format!("{} - Low power: {}%", battery_str, percent), + fixable: false, + }, + _ => CheckStatus::Pass(format!("{} - Power OK: {}%", battery_str, percent)), + } + } +} + +#[derive(Default)] +pub struct BatteryHealthCheck; + +#[async_trait] +impl DoctorCheck for BatteryHealthCheck { + fn name(&self) -> &'static str { + "Battery life" + } + + fn id(&self) -> &'static str { + "battery-life" + } + + async fn execute(&self) -> CheckStatus { + BatteryCheck::execute(self).await + } +} + +impl BatteryCheck for BatteryHealthCheck { + fn check_parameter(&self, mut batteries: Vec) -> CheckStatus { + batteries.sort_by(|b1, b2| { + b1.state_of_health() + .value + .total_cmp(&b2.state_of_health().value) + }); + let lowest = batteries.first().unwrap(); + let lowest_health = lowest.state_of_health(); + let percent = (lowest_health.value * 100.0) as u64; + if percent < 90 { + CheckStatus::Warning { + message: "Battery health degraded".into(), + fixable: false, + } + } else { + CheckStatus::Pass(format!("Battery life: {}%", percent)) + } + } +} diff --git a/src/doctor/registry.rs b/src/doctor/registry.rs index 0178f312..aaba42f3 100644 --- a/src/doctor/registry.rs +++ b/src/doctor/registry.rs @@ -1,4 +1,5 @@ use super::{DoctorCheck, checks::*}; +use crate::doctor::checks::energy::BatteryHealthCheck; use std::collections::HashMap; pub type CheckFactory = fn() -> Box; @@ -28,6 +29,8 @@ impl CheckRegistry { registry.register::("sway-display"); registry.register::("polkit-agent"); registry.register::("bat-cache"); + registry.register::("power"); + registry.register::("battery-life"); registry } From 21513126bc552fd1212a48e9566569a3fbb61b05 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sat, 10 Jan 2026 15:18:27 +0100 Subject: [PATCH 02/16] Formatting of battery model --- src/doctor/checks/energy.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/doctor/checks/energy.rs b/src/doctor/checks/energy.rs index 689dab8d..d426e7e9 100644 --- a/src/doctor/checks/energy.rs +++ b/src/doctor/checks/energy.rs @@ -31,9 +31,9 @@ trait BatteryCheck: DoctorCheck { fn format_battery(&self, battery: &Battery) -> String { format!( - "{} ({})", + "{} {}", + battery.vendor().unwrap_or("Unknown vendor"), battery.model().unwrap_or("Unknown model"), - battery.serial_number().unwrap_or("Unknown S/N"), ) } } @@ -69,17 +69,27 @@ impl BatteryCheck for PowerCheck { let lowest = batteries.first().unwrap(); let lowest_charge = lowest.state_of_charge(); let battery_str = self.format_battery(lowest); + let battery_status = lowest.state().to_string(); let percent = (lowest_charge.value * 100.0) as u64; match lowest_charge.value { 0.0..0.25 => CheckStatus::Fail { - message: format!("{} - Critical power: {}%", battery_str, percent), + message: format!( + "{} - Critical power: {} % ({})", + battery_str, percent, battery_status + ), fixable: false, }, 0.25..0.5 => CheckStatus::Warning { - message: format!("{} - Low power: {}%", battery_str, percent), + message: format!( + "{} - Low power: {} %, ({})", + battery_str, percent, battery_status + ), fixable: false, }, - _ => CheckStatus::Pass(format!("{} - Power OK: {}%", battery_str, percent)), + _ => CheckStatus::Pass(format!( + "{} - Power OK: {} % ({})", + battery_str, percent, battery_status + )), } } } @@ -110,15 +120,16 @@ impl BatteryCheck for BatteryHealthCheck { .total_cmp(&b2.state_of_health().value) }); let lowest = batteries.first().unwrap(); + let battery_str = self.format_battery(lowest); let lowest_health = lowest.state_of_health(); let percent = (lowest_health.value * 100.0) as u64; if percent < 90 { CheckStatus::Warning { - message: "Battery health degraded".into(), + message: format!("{} health degraded - {} %", battery_str, percent), fixable: false, } } else { - CheckStatus::Pass(format!("Battery life: {}%", percent)) + CheckStatus::Pass(format!("{} OK - {} %", battery_str, percent)) } } } From 5cfeff785988e0c13ab35ccecccbe38c20eb6bed Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sat, 10 Jan 2026 19:50:07 +0100 Subject: [PATCH 03/16] Added performance check (WIP) --- Cargo.lock | 24 ++++- Cargo.toml | 2 + src/doctor/checks.rs | 1 + src/doctor/checks/performance.rs | 156 +++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 src/doctor/checks/performance.rs diff --git a/Cargo.lock b/Cargo.lock index 7cd6ec29..f8703e8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1251,6 +1251,8 @@ dependencies = [ "serial_test", "sha2", "shellexpand", + "strum 0.27.2", + "strum_macros 0.27.2", "sudo", "tempfile", "thiserror 2.0.17", @@ -1813,7 +1815,7 @@ dependencies = [ "itertools", "lru", "paste", - "strum", + "strum 0.26.3", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.0", @@ -2341,9 +2343,15 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", ] +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + [[package]] name = "strum_macros" version = "0.26.4" @@ -2357,6 +2365,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 839b1553..002b1495 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ nix = { version = "0.29", features = ["fs"] } pathdiff = "0.2" urlencoding = "2.1.3" battery = "0.7.8" +strum = "0.27.2" +strum_macros = "0.27.2" [dev-dependencies] serial_test = "3" diff --git a/src/doctor/checks.rs b/src/doctor/checks.rs index 6f54736b..1e9696d2 100644 --- a/src/doctor/checks.rs +++ b/src/doctor/checks.rs @@ -16,6 +16,7 @@ pub mod energy; pub mod locale; pub mod nerdfont; pub mod network; +mod performance; pub mod security; pub mod storage; pub mod system; diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs new file mode 100644 index 00000000..acbae96d --- /dev/null +++ b/src/doctor/checks/performance.rs @@ -0,0 +1,156 @@ +use async_trait::async_trait; +use std::str::FromStr; +use strum_macros::{EnumString, IntoStaticStr}; +use tokio::fs::File as TokioFile; +use tokio::process::Command as TokioCommand; + +/// +/// Performance <-> Power-Saving +/// +/// Many laptops automatically activate power saving mode and sometimes do not +/// switch back to performance, even when plugged in. +/// This is a UNIX-only feature +/// We try to query the current power authority and get the mode +/// First we try "powerprofilesctl", if available, we manage power using this tool +/// Otherwise, we manually insert our preference: +/// The directory /sys/devices/system/cpu/cpu*/cpufreq/ provides information +/// about the current CPU power mode, * is to replace with the id of the core. +/// + +/// Abstract performance mode +#[derive(Default)] +pub enum GeneralPowerMode { + /// Absolute performance + Performance, + + /// Power + Balanced, + + /// Maximum power saving + PowerSave, + + #[default] + Unknown, +} + +/// Adapts to one of the two possible ways described in the top comment +#[async_trait] +pub trait PowerHandle +where + T: From + FromStr + Into<&'static str>, +{ + /// Retrieves the current performance mode + async fn query_performance_mode(&self) -> T; + + /// Changes the performance mode, might require sudo + /// Returns true on success + async fn change_performance_mode(&mut self, mode: T) -> bool; +} + +/// Performance modes used by `powerprofilesctl` +#[derive(Default, EnumString, IntoStaticStr)] +pub enum GnomePowerMode { + #[strum(serialize = "performance")] + Performance, + + #[strum(serialize = "balanced")] + Balanced, + + #[strum(serialize = "power-saver")] + PowerSaver, + + #[default] + Unknown, +} + +impl From for GnomePowerMode { + fn from(mode: GeneralPowerMode) -> Self { + match mode { + GeneralPowerMode::Performance => GnomePowerMode::Performance, + GeneralPowerMode::Balanced => GnomePowerMode::Balanced, + GeneralPowerMode::PowerSave => GnomePowerMode::PowerSaver, + _ => GnomePowerMode::Unknown, + } + } +} + +/// Implementation for `powerprofilesctl` +const PP_CTL: &str = "powerprofilesctl"; +#[derive(Default)] +pub struct GnomePowerHandle; + +#[async_trait] +impl PowerHandle for GnomePowerHandle { + async fn query_performance_mode(&self) -> GnomePowerMode { + let output = TokioCommand::new(PP_CTL).output(); + todo!(); + } + + async fn change_performance_mode(&mut self, mode: GnomePowerMode) -> bool { + TokioCommand::new(PP_CTL) + .args(["set", mode.into()]) + .output() + .await + .map(|output| output.status.success()) + .unwrap_or(false) + } +} + +pub enum LegacyPowerMode { + Performance, + PowerSave, + Unknown, +} + +/// Uses `/sys/devices/system/cpu/cpu_/cpufreq` to control performance mode +#[derive(Default)] +pub struct LegacyPowerHandle; + +impl From<&'static str> for LegacyPowerMode { + /// Turns the value read from /sys/devices/system/cpu/cpu_/cpufreq/scaling_available_governors/ + /// into a variant of the PerformanceMode enum + fn from(value: &'static str) -> Self { + match value { + "performance" => LegacyPowerMode::Performance, + "powersave" => LegacyPowerMode::PowerSave, + _ => LegacyPowerMode::Unknown, + } + } +} + +/// This enum holds all supported power handles +pub enum PowerHandles { + ProfiledPowerHandle(GnomePowerHandle), + LegacyPowerHandle(LegacyPowerHandle), +} + +/// Creates a power handle +#[derive(Default)] +pub struct PowerHandleFactory; + +impl PowerHandleFactory { + async fn build_power_handle(&self) -> Option { + // We check if powerprofilesctl is available + let profiled_power = TokioCommand::new("which") + .arg("powerprofilesctl") + .output() + .await + .map(|output| output.status.success()) + .unwrap_or(false); + if profiled_power { + return PowerHandles::ProfiledPowerHandle(GnomePowerHandle::default()).into(); + } + + // If not, we check if we have access to the sysfiles + let sys_available = + TokioFile::open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") + .await + .is_ok(); + if sys_available { + PowerHandles::LegacyPowerHandle(LegacyPowerHandle::default()).into() + } else { + // Unfortunately we have no way to control power management + None + } + } +} From 82758402068dec2d4356661d6b706604ee0d4e01 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sat, 10 Jan 2026 21:57:19 +0100 Subject: [PATCH 04/16] Finished performance framework --- src/doctor/checks/performance.rs | 230 ++++++++++++++++++++++--------- 1 file changed, 162 insertions(+), 68 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index acbae96d..cb23db6f 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use std::str::FromStr; -use strum_macros::{EnumString, IntoStaticStr}; +use strum_macros::EnumString; use tokio::fs::File as TokioFile; use tokio::process::Command as TokioCommand; @@ -17,61 +17,31 @@ use tokio::process::Command as TokioCommand; /// about the current CPU power mode, * is to replace with the id of the core. /// -/// Abstract performance mode -#[derive(Default)] -pub enum GeneralPowerMode { - /// Absolute performance - Performance, - - /// Power - Balanced, - - /// Maximum power saving - PowerSave, - - #[default] - Unknown, -} - /// Adapts to one of the two possible ways described in the top comment #[async_trait] -pub trait PowerHandle -where - T: From + FromStr + Into<&'static str>, -{ +pub trait PowerHandle { /// Retrieves the current performance mode - async fn query_performance_mode(&self) -> T; + async fn query_performance_mode(&self) -> Option; /// Changes the performance mode, might require sudo /// Returns true on success - async fn change_performance_mode(&mut self, mode: T) -> bool; + async fn change_performance_mode(&self, mode: PowerMode) -> bool; + + /// Returns all available performance modes on the system + async fn available_modes(&self) -> Vec; } -/// Performance modes used by `powerprofilesctl` -#[derive(Default, EnumString, IntoStaticStr)] -pub enum GnomePowerMode { +/// Performance modes +#[derive(PartialEq, EnumString, Debug)] +pub enum PowerMode { #[strum(serialize = "performance")] Performance, #[strum(serialize = "balanced")] Balanced, - #[strum(serialize = "power-saver")] + #[strum(serialize = "power-saver", serialize = "powersave")] PowerSaver, - - #[default] - Unknown, -} - -impl From for GnomePowerMode { - fn from(mode: GeneralPowerMode) -> Self { - match mode { - GeneralPowerMode::Performance => GnomePowerMode::Performance, - GeneralPowerMode::Balanced => GnomePowerMode::Balanced, - GeneralPowerMode::PowerSave => GnomePowerMode::PowerSaver, - _ => GnomePowerMode::Unknown, - } - } } /// Implementation for `powerprofilesctl` @@ -80,48 +50,140 @@ const PP_CTL: &str = "powerprofilesctl"; pub struct GnomePowerHandle; #[async_trait] -impl PowerHandle for GnomePowerHandle { - async fn query_performance_mode(&self) -> GnomePowerMode { - let output = TokioCommand::new(PP_CTL).output(); - todo!(); +impl PowerHandle for GnomePowerHandle { + async fn query_performance_mode(&self) -> Option { + let output = TokioCommand::new(PP_CTL).arg("get").output().await; + + match output { + Ok(output) => { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + PowerMode::from_str(stdout.as_ref().trim()).ok() + } else { + None + } + } + Err(_) => None, + } } - async fn change_performance_mode(&mut self, mode: GnomePowerMode) -> bool { + async fn change_performance_mode(&self, mode: PowerMode) -> bool { + let gnome_identifier = match mode { + PowerMode::Performance => "performance", + PowerMode::Balanced => "balanced", + PowerMode::PowerSaver => "power-save", + }; + TokioCommand::new(PP_CTL) - .args(["set", mode.into()]) + .args(["set", gnome_identifier]) .output() .await .map(|output| output.status.success()) .unwrap_or(false) } -} -pub enum LegacyPowerMode { - Performance, - PowerSave, - Unknown, + async fn available_modes(&self) -> Vec { + let output = TokioCommand::new(PP_CTL).arg("list").output().await; + + match output { + Ok(output) if output.status.success() => { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.ends_with(':') { + let profile_name = + line.trim_end_matches(':').trim_start_matches('*').trim(); + profile_name.parse().ok() + } else { + None + } + }) + .collect() + } + _ => vec![], + } + } } /// Uses `/sys/devices/system/cpu/cpu_/cpufreq` to control performance mode #[derive(Default)] pub struct LegacyPowerHandle; -impl From<&'static str> for LegacyPowerMode { - /// Turns the value read from /sys/devices/system/cpu/cpu_/cpufreq/scaling_available_governors/ - /// into a variant of the PerformanceMode enum - fn from(value: &'static str) -> Self { - match value { - "performance" => LegacyPowerMode::Performance, - "powersave" => LegacyPowerMode::PowerSave, - _ => LegacyPowerMode::Unknown, +#[async_trait] +impl PowerHandle for LegacyPowerHandle { + async fn query_performance_mode(&self) -> Option { + const GOVERNOR_PATH: &str = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"; + + match tokio::fs::read_to_string(GOVERNOR_PATH).await { + Ok(content) => PowerMode::from_str(content.trim()).ok(), + Err(_) => None, } } -} -/// This enum holds all supported power handles -pub enum PowerHandles { - ProfiledPowerHandle(GnomePowerHandle), - LegacyPowerHandle(LegacyPowerHandle), + async fn change_performance_mode(&self, mode: PowerMode) -> bool { + let available_modes = self.available_modes().await; + + if !available_modes.contains(&mode) { + return false; + } + + let legacy_idenitfier = match mode { + PowerMode::Performance | PowerMode::Balanced => "performance", + PowerMode::PowerSaver => "powersave", + }; + + let mut all_success = true; + + let mut cpu_dirs = match tokio::fs::read_dir("/sys/devices/system/cpu").await { + Ok(dir) => dir, + Err(_) => return false, + }; + + loop { + let entry_opt = match cpu_dirs.next_entry().await { + Ok(entry) => entry, + Err(_) => continue, + }; + + if let Some(entry) = entry_opt { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + + if name_str.starts_with("cpu") && name_str[3..].parse::().is_ok() { + let governor_path = format!( + "/sys/devices/system/cpu/{}/cpufreq/scaling_governor", + name_str + ); + + if tokio::fs::write(&governor_path, legacy_idenitfier) + .await + .is_err() + { + all_success = false; + } + } + } else { + break; + } + } + + all_success + } + + async fn available_modes(&self) -> Vec { + const AVAILABLE_GOVERNORS_PATH: &str = + "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors"; + + match tokio::fs::read_to_string(AVAILABLE_GOVERNORS_PATH).await { + Ok(content) => content + .split_whitespace() + .filter_map(|governor| governor.parse().ok()) + .collect(), + Err(_) => vec![], + } + } } /// Creates a power handle @@ -129,7 +191,7 @@ pub enum PowerHandles { pub struct PowerHandleFactory; impl PowerHandleFactory { - async fn build_power_handle(&self) -> Option { + async fn build_power_handle(&self) -> Option> { // We check if powerprofilesctl is available let profiled_power = TokioCommand::new("which") .arg("powerprofilesctl") @@ -138,7 +200,7 @@ impl PowerHandleFactory { .map(|output| output.status.success()) .unwrap_or(false); if profiled_power { - return PowerHandles::ProfiledPowerHandle(GnomePowerHandle::default()).into(); + return Some(Box::new(GnomePowerHandle::default())); } // If not, we check if we have access to the sysfiles @@ -147,10 +209,42 @@ impl PowerHandleFactory { .await .is_ok(); if sys_available { - PowerHandles::LegacyPowerHandle(LegacyPowerHandle::default()).into() + Some(Box::new(LegacyPowerHandle::default())) } else { // Unfortunately we have no way to control power management None } } } + +/// TODO: Implement this +#[derive(Default)] +struct PerformanceTest; + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_factory() { + let factory = PowerHandleFactory::default(); + let power_handle = factory.build_power_handle().await.unwrap(); + println!( + "Current mode: {:?}", + power_handle.query_performance_mode().await.unwrap() + ); + println!("Power modes:"); + for power_mode in power_handle.available_modes().await { + println!("{:?}", power_mode) + } + } + + #[tokio::test] + async fn get_mode_legacy() { + let handle = LegacyPowerHandle::default(); + println!( + "Legacy mode: {:?}", + handle.query_performance_mode().await.unwrap() + ); + } +} From 71cd52c5ee106ccf9315beb2be84f24b0736e344 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 12:45:17 +0100 Subject: [PATCH 05/16] Implemented performance doctor check (WIP, testing required) --- src/doctor/checks/performance.rs | 101 +++++++++++++++++++++++-------- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index cb23db6f..75802fac 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,6 +1,8 @@ +use crate::doctor::{CheckStatus, DoctorCheck}; +use anyhow::anyhow; use async_trait::async_trait; use std::str::FromStr; -use strum_macros::EnumString; +use strum_macros::{Display, EnumString}; use tokio::fs::File as TokioFile; use tokio::process::Command as TokioCommand; @@ -19,20 +21,20 @@ use tokio::process::Command as TokioCommand; /// Adapts to one of the two possible ways described in the top comment #[async_trait] -pub trait PowerHandle { +pub trait PowerHandle: Send + Sync { /// Retrieves the current performance mode async fn query_performance_mode(&self) -> Option; /// Changes the performance mode, might require sudo /// Returns true on success - async fn change_performance_mode(&self, mode: PowerMode) -> bool; + async fn change_performance_mode(&self, mode: PowerMode) -> anyhow::Result<()>; /// Returns all available performance modes on the system async fn available_modes(&self) -> Vec; } /// Performance modes -#[derive(PartialEq, EnumString, Debug)] +#[derive(PartialEq, EnumString, Debug, Display)] pub enum PowerMode { #[strum(serialize = "performance")] Performance, @@ -67,19 +69,23 @@ impl PowerHandle for GnomePowerHandle { } } - async fn change_performance_mode(&self, mode: PowerMode) -> bool { + async fn change_performance_mode(&self, mode: PowerMode) -> anyhow::Result<()> { let gnome_identifier = match mode { PowerMode::Performance => "performance", PowerMode::Balanced => "balanced", PowerMode::PowerSaver => "power-save", }; - TokioCommand::new(PP_CTL) + let success = TokioCommand::new(PP_CTL) .args(["set", gnome_identifier]) .output() .await - .map(|output| output.status.success()) - .unwrap_or(false) + .map(|output| output.status.success())?; + if !success { + Err(anyhow!("Failed to set power-saver mode")) + } else { + Ok(()) + } } async fn available_modes(&self) -> Vec { @@ -122,11 +128,11 @@ impl PowerHandle for LegacyPowerHandle { } } - async fn change_performance_mode(&self, mode: PowerMode) -> bool { + async fn change_performance_mode(&self, mode: PowerMode) -> anyhow::Result<()> { let available_modes = self.available_modes().await; if !available_modes.contains(&mode) { - return false; + return Err(anyhow!("Power mode {} is not available", mode)); } let legacy_idenitfier = match mode { @@ -134,42 +140,44 @@ impl PowerHandle for LegacyPowerHandle { PowerMode::PowerSaver => "powersave", }; - let mut all_success = true; - let mut cpu_dirs = match tokio::fs::read_dir("/sys/devices/system/cpu").await { Ok(dir) => dir, - Err(_) => return false, + Err(_) => return Err(anyhow!("Failed to read dir /sys/devices/system/cpu")), }; loop { let entry_opt = match cpu_dirs.next_entry().await { Ok(entry) => entry, - Err(_) => continue, + Err(_) => { + return Err(anyhow!( + "Failed to read directory entry from /sys/devices/system/cpu" + )); + } }; if let Some(entry) = entry_opt { let name = entry.file_name(); let name_str = name.to_string_lossy(); - if name_str.starts_with("cpu") && name_str[3..].parse::().is_ok() { - let governor_path = format!( - "/sys/devices/system/cpu/{}/cpufreq/scaling_governor", - name_str - ); + if !name_str.starts_with("cpu") { + continue; + } + + if let Ok(core) = name_str[3..].parse::() { + let governor_path = + format!("/sys/devices/system/cpu/{}/cpufreq/scaling_governor", core); if tokio::fs::write(&governor_path, legacy_idenitfier) .await .is_err() { - all_success = false; + return Err(anyhow!("Failed to write to {}", governor_path)); } } } else { - break; + return Ok(()); } } - - all_success } async fn available_modes(&self) -> Vec { @@ -217,10 +225,55 @@ impl PowerHandleFactory { } } -/// TODO: Implement this #[derive(Default)] struct PerformanceTest; +impl PerformanceTest { + async fn try_execute(&self) -> Option { + let handle = PowerHandleFactory::default().build_power_handle().await?; + let mode = handle.query_performance_mode().await?; + if mode != PowerMode::Performance { + Some(CheckStatus::Warning { + message: format!("Power mode is not performance but {}", mode), + fixable: true, + }) + } else { + Some(CheckStatus::Pass("Power mode is performance".into())) + } + } +} + +#[async_trait] +impl DoctorCheck for PerformanceTest { + fn name(&self) -> &'static str { + "Performance Mode" + } + + fn id(&self) -> &'static str { + "performance" + } + + async fn execute(&self) -> CheckStatus { + if let Some(status) = self.try_execute().await { + status + } else { + CheckStatus::Skipped("Could not query performance mode".into()) + } + } + + fn fix_message(&self) -> Option { + Some("Set power mode to performance".into()) + } + + async fn fix(&self) -> anyhow::Result<()> { + let handle = PowerHandleFactory::default() + .build_power_handle() + .await + .ok_or_else(|| anyhow!("Failed to build power handle"))?; + handle.change_performance_mode(PowerMode::Performance).await + } +} + #[cfg(test)] mod tests { use super::*; From 62b7ea28aeea10a0a2a6746fdad1190851a289aa Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:41:36 +0100 Subject: [PATCH 06/16] Registered performance check --- src/doctor/checks.rs | 1 + src/doctor/checks/performance.rs | 2 +- src/doctor/registry.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/doctor/checks.rs b/src/doctor/checks.rs index 1e9696d2..8449ffda 100644 --- a/src/doctor/checks.rs +++ b/src/doctor/checks.rs @@ -28,6 +28,7 @@ pub use energy::PowerCheck; pub use locale::LocaleCheck; pub use nerdfont::NerdFontCheck; pub use network::{InstantRepoCheck, InternetCheck}; +pub use performance::PerformanceTest; pub use security::PolkitAgentCheck; pub use storage::{ PacmanCacheCheck, PacmanDbSyncCheck, PacmanStaleDownloadsCheck, SmartHealthCheck, YayCacheCheck, diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 75802fac..4ee77a83 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -226,7 +226,7 @@ impl PowerHandleFactory { } #[derive(Default)] -struct PerformanceTest; +pub struct PerformanceTest; impl PerformanceTest { async fn try_execute(&self) -> Option { diff --git a/src/doctor/registry.rs b/src/doctor/registry.rs index aaba42f3..430a8df8 100644 --- a/src/doctor/registry.rs +++ b/src/doctor/registry.rs @@ -31,6 +31,7 @@ impl CheckRegistry { registry.register::("bat-cache"); registry.register::("power"); registry.register::("battery-life"); + registry.register::("performance"); registry } From afbbee1a2caca7bb771baf643065f463040045cb Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:58:17 +0100 Subject: [PATCH 07/16] Fixed legacy path --- src/doctor/checks/performance.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 4ee77a83..f2e15c34 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,5 +1,5 @@ use crate::doctor::{CheckStatus, DoctorCheck}; -use anyhow::anyhow; +use anyhow::{Context, anyhow}; use async_trait::async_trait; use std::str::FromStr; use strum_macros::{Display, EnumString}; @@ -164,15 +164,14 @@ impl PowerHandle for LegacyPowerHandle { } if let Ok(core) = name_str[3..].parse::() { - let governor_path = - format!("/sys/devices/system/cpu/{}/cpufreq/scaling_governor", core); + let governor_path = format!( + "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor", + core + ); - if tokio::fs::write(&governor_path, legacy_idenitfier) + tokio::fs::write(&governor_path, legacy_idenitfier) .await - .is_err() - { - return Err(anyhow!("Failed to write to {}", governor_path)); - } + .with_context(|| format!("writing to {}", governor_path))?; } } else { return Ok(()); From 6703e345fb54f12afc9ef5f46ff1a206bdb86e62 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:12:50 +0100 Subject: [PATCH 08/16] Performance change requires root (only legacy) --- src/doctor/checks/performance.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index f2e15c34..90e520b1 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,4 +1,4 @@ -use crate::doctor::{CheckStatus, DoctorCheck}; +use crate::doctor::{CheckStatus, DoctorCheck, PrivilegeLevel}; use anyhow::{Context, anyhow}; use async_trait::async_trait; use std::str::FromStr; @@ -271,6 +271,10 @@ impl DoctorCheck for PerformanceTest { .ok_or_else(|| anyhow!("Failed to build power handle"))?; handle.change_performance_mode(PowerMode::Performance).await } + + fn fix_privilege_level(&self) -> PrivilegeLevel { + PrivilegeLevel::Root + } } #[cfg(test)] @@ -290,13 +294,4 @@ mod tests { println!("{:?}", power_mode) } } - - #[tokio::test] - async fn get_mode_legacy() { - let handle = LegacyPowerHandle::default(); - println!( - "Legacy mode: {:?}", - handle.query_performance_mode().await.unwrap() - ); - } } From dff1d7994fc51de4b31f4ed55a8ef78a1364f413 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:19:18 +0100 Subject: [PATCH 09/16] Error propagation using anyhow --- src/doctor/checks/performance.rs | 51 +++++++++++++------------------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 90e520b1..7b3d361e 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -23,7 +23,7 @@ use tokio::process::Command as TokioCommand; #[async_trait] pub trait PowerHandle: Send + Sync { /// Retrieves the current performance mode - async fn query_performance_mode(&self) -> Option; + async fn query_performance_mode(&self) -> anyhow::Result; /// Changes the performance mode, might require sudo /// Returns true on success @@ -53,19 +53,13 @@ pub struct GnomePowerHandle; #[async_trait] impl PowerHandle for GnomePowerHandle { - async fn query_performance_mode(&self) -> Option { - let output = TokioCommand::new(PP_CTL).arg("get").output().await; - - match output { - Ok(output) => { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - PowerMode::from_str(stdout.as_ref().trim()).ok() - } else { - None - } - } - Err(_) => None, + async fn query_performance_mode(&self) -> anyhow::Result { + let output = TokioCommand::new(PP_CTL).arg("get").output().await?; + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + Ok(PowerMode::from_str(stdout.as_ref().trim())?) + } else { + Err(anyhow!("{} get returned non-zero value", PP_CTL)) } } @@ -119,13 +113,11 @@ pub struct LegacyPowerHandle; #[async_trait] impl PowerHandle for LegacyPowerHandle { - async fn query_performance_mode(&self) -> Option { + async fn query_performance_mode(&self) -> anyhow::Result { const GOVERNOR_PATH: &str = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"; - match tokio::fs::read_to_string(GOVERNOR_PATH).await { - Ok(content) => PowerMode::from_str(content.trim()).ok(), - Err(_) => None, - } + let content = tokio::fs::read_to_string(GOVERNOR_PATH).await?; + Ok(PowerMode::from_str(content.trim())?) } async fn change_performance_mode(&self, mode: PowerMode) -> anyhow::Result<()> { @@ -198,7 +190,7 @@ impl PowerHandle for LegacyPowerHandle { pub struct PowerHandleFactory; impl PowerHandleFactory { - async fn build_power_handle(&self) -> Option> { + async fn build_power_handle(&self) -> anyhow::Result> { // We check if powerprofilesctl is available let profiled_power = TokioCommand::new("which") .arg("powerprofilesctl") @@ -207,7 +199,7 @@ impl PowerHandleFactory { .map(|output| output.status.success()) .unwrap_or(false); if profiled_power { - return Some(Box::new(GnomePowerHandle::default())); + return Ok(Box::new(GnomePowerHandle::default())); } // If not, we check if we have access to the sysfiles @@ -216,10 +208,10 @@ impl PowerHandleFactory { .await .is_ok(); if sys_available { - Some(Box::new(LegacyPowerHandle::default())) + Ok(Box::new(LegacyPowerHandle::default())) } else { // Unfortunately we have no way to control power management - None + Err(anyhow!("Power management is not available")) } } } @@ -228,16 +220,16 @@ impl PowerHandleFactory { pub struct PerformanceTest; impl PerformanceTest { - async fn try_execute(&self) -> Option { + async fn try_execute(&self) -> anyhow::Result { let handle = PowerHandleFactory::default().build_power_handle().await?; let mode = handle.query_performance_mode().await?; if mode != PowerMode::Performance { - Some(CheckStatus::Warning { + Ok(CheckStatus::Warning { message: format!("Power mode is not performance but {}", mode), fixable: true, }) } else { - Some(CheckStatus::Pass("Power mode is performance".into())) + Ok(CheckStatus::Pass("Power mode is performance".into())) } } } @@ -253,7 +245,7 @@ impl DoctorCheck for PerformanceTest { } async fn execute(&self) -> CheckStatus { - if let Some(status) = self.try_execute().await { + if let Ok(status) = self.try_execute().await { status } else { CheckStatus::Skipped("Could not query performance mode".into()) @@ -265,10 +257,7 @@ impl DoctorCheck for PerformanceTest { } async fn fix(&self) -> anyhow::Result<()> { - let handle = PowerHandleFactory::default() - .build_power_handle() - .await - .ok_or_else(|| anyhow!("Failed to build power handle"))?; + let handle = PowerHandleFactory::default().build_power_handle().await?; handle.change_performance_mode(PowerMode::Performance).await } From b22783b59befef47de065460bb6aa7c1fe13afc0 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:00:20 +0100 Subject: [PATCH 10/16] Using dependency system for power-profiles-daemon --- src/common/deps.rs | 10 ++++++++++ src/doctor/checks/performance.rs | 8 ++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/common/deps.rs b/src/common/deps.rs index 3bcf8bce..108f8198 100644 --- a/src/common/deps.rs +++ b/src/common/deps.rs @@ -108,3 +108,13 @@ pub static SMARTMONTOOLS: Dependency = Dependency { ], tests: &[InstallTest::WhichSucceeds("smartctl")], }; + +pub static POWERPROFILESDAEMON: Dependency = Dependency { + name: "power-profiles-daemon", + description: Some("Makes power profiles handling available over D-Bus"), + packages: &[ + PackageDefinition::new("power-profiles-daemon", PackageManager::Apt), + PackageDefinition::new("power-profiles-daemon", PackageManager::Pacman), + ], + tests: &[InstallTest::WhichSucceeds("powerprofilesctl")], +}; diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 7b3d361e..6b1d884e 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,3 +1,4 @@ +use crate::common::deps; use crate::doctor::{CheckStatus, DoctorCheck, PrivilegeLevel}; use anyhow::{Context, anyhow}; use async_trait::async_trait; @@ -192,12 +193,7 @@ pub struct PowerHandleFactory; impl PowerHandleFactory { async fn build_power_handle(&self) -> anyhow::Result> { // We check if powerprofilesctl is available - let profiled_power = TokioCommand::new("which") - .arg("powerprofilesctl") - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false); + let profiled_power = deps::POWERPROFILESDAEMON.is_installed(); if profiled_power { return Ok(Box::new(GnomePowerHandle::default())); } From c542166918336fa771a21ba6b721f008db415d17 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:00:34 +0100 Subject: [PATCH 11/16] Added power-profiles-daemon to CI dependencies --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90495cbe..952975ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: pacman -Syu --noconfirm - name: Install build dependencies run: | - pacman -S --noconfirm --needed base-devel rust pkgconf openssl git cmake restic jq + pacman -S --noconfirm --needed base-devel rust pkgconf openssl git cmake restic jq power-profiles-daemon - name: Rust cache uses: Swatinem/rust-cache@v2 with: From c5712fdc15e709c8053c0ecac0a60b500660c8ae Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:17:45 +0100 Subject: [PATCH 12/16] Very reasonable fixes recommended by the rabbit --- .github/workflows/ci.yml | 2 +- src/doctor/checks/performance.rs | 21 +-------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 952975ae..90495cbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: pacman -Syu --noconfirm - name: Install build dependencies run: | - pacman -S --noconfirm --needed base-devel rust pkgconf openssl git cmake restic jq power-profiles-daemon + pacman -S --noconfirm --needed base-devel rust pkgconf openssl git cmake restic jq - name: Rust cache uses: Swatinem/rust-cache@v2 with: diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 6b1d884e..06bf0b5f 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -68,7 +68,7 @@ impl PowerHandle for GnomePowerHandle { let gnome_identifier = match mode { PowerMode::Performance => "performance", PowerMode::Balanced => "balanced", - PowerMode::PowerSaver => "power-save", + PowerMode::PowerSaver => "power-saver", }; let success = TokioCommand::new(PP_CTL) @@ -261,22 +261,3 @@ impl DoctorCheck for PerformanceTest { PrivilegeLevel::Root } } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_factory() { - let factory = PowerHandleFactory::default(); - let power_handle = factory.build_power_handle().await.unwrap(); - println!( - "Current mode: {:?}", - power_handle.query_performance_mode().await.unwrap() - ); - println!("Power modes:"); - for power_mode in power_handle.available_modes().await { - println!("{:?}", power_mode) - } - } -} From a429c6b1cd6b92f2cf40f37c85e7f84a960f4ee5 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:23:57 +0100 Subject: [PATCH 13/16] Global performance policy instead of per-core --- src/doctor/checks/performance.rs | 50 ++++++-------------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 06bf0b5f..94d03271 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -1,6 +1,6 @@ use crate::common::deps; use crate::doctor::{CheckStatus, DoctorCheck, PrivilegeLevel}; -use anyhow::{Context, anyhow}; +use anyhow::anyhow; use async_trait::async_trait; use std::str::FromStr; use strum_macros::{Display, EnumString}; @@ -115,7 +115,7 @@ pub struct LegacyPowerHandle; #[async_trait] impl PowerHandle for LegacyPowerHandle { async fn query_performance_mode(&self) -> anyhow::Result { - const GOVERNOR_PATH: &str = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"; + const GOVERNOR_PATH: &str = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor"; let content = tokio::fs::read_to_string(GOVERNOR_PATH).await?; Ok(PowerMode::from_str(content.trim())?) @@ -133,48 +133,16 @@ impl PowerHandle for LegacyPowerHandle { PowerMode::PowerSaver => "powersave", }; - let mut cpu_dirs = match tokio::fs::read_dir("/sys/devices/system/cpu").await { - Ok(dir) => dir, - Err(_) => return Err(anyhow!("Failed to read dir /sys/devices/system/cpu")), - }; - - loop { - let entry_opt = match cpu_dirs.next_entry().await { - Ok(entry) => entry, - Err(_) => { - return Err(anyhow!( - "Failed to read directory entry from /sys/devices/system/cpu" - )); - } - }; - - if let Some(entry) = entry_opt { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - - if !name_str.starts_with("cpu") { - continue; - } - - if let Ok(core) = name_str[3..].parse::() { - let governor_path = format!( - "/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor", - core - ); - - tokio::fs::write(&governor_path, legacy_idenitfier) - .await - .with_context(|| format!("writing to {}", governor_path))?; - } - } else { - return Ok(()); - } - } + Ok(tokio::fs::write( + "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor", + legacy_idenitfier, + ) + .await?) } async fn available_modes(&self) -> Vec { const AVAILABLE_GOVERNORS_PATH: &str = - "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors"; + "/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors"; match tokio::fs::read_to_string(AVAILABLE_GOVERNORS_PATH).await { Ok(content) => content @@ -200,7 +168,7 @@ impl PowerHandleFactory { // If not, we check if we have access to the sysfiles let sys_available = - TokioFile::open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") + TokioFile::open("/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors") .await .is_ok(); if sys_available { From 3e2510b2f22592d07d0b469316ec3c422a8417f9 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:26:55 +0100 Subject: [PATCH 14/16] Checks if powerprofilesctl can be used before returning it in factory --- src/doctor/checks/performance.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 94d03271..3a069751 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -163,7 +163,10 @@ impl PowerHandleFactory { // We check if powerprofilesctl is available let profiled_power = deps::POWERPROFILESDAEMON.is_installed(); if profiled_power { - return Ok(Box::new(GnomePowerHandle::default())); + let gnome_handle = GnomePowerHandle::default(); + if gnome_handle.query_performance_mode().await.is_ok() { + return Ok(Box::new(gnome_handle)); + } } // If not, we check if we have access to the sysfiles From 4df034ad4542ccb24d95370331e00494823939d5 Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:29:33 +0100 Subject: [PATCH 15/16] Using try_exists instead of trying to open file --- src/doctor/checks/performance.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index 3a069751..a61d8f96 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -4,7 +4,6 @@ use anyhow::anyhow; use async_trait::async_trait; use std::str::FromStr; use strum_macros::{Display, EnumString}; -use tokio::fs::File as TokioFile; use tokio::process::Command as TokioCommand; /// @@ -170,10 +169,10 @@ impl PowerHandleFactory { } // If not, we check if we have access to the sysfiles - let sys_available = - TokioFile::open("/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors") - .await - .is_ok(); + let sys_available = tokio::fs::try_exists( + "/sys/devices/system/cpu/cpufreq/policy0/scaling_available_governors", + ) + .await?; if sys_available { Ok(Box::new(LegacyPowerHandle::default())) } else { From d23561ce3f65aba7c65f643a1d50ed7bd8c6657e Mon Sep 17 00:00:00 2001 From: oskmasi <145667256+oskmasi@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:45:27 +0100 Subject: [PATCH 16/16] Strum no longer case-sensitive --- src/doctor/checks/performance.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/doctor/checks/performance.rs b/src/doctor/checks/performance.rs index a61d8f96..8f2a3bda 100644 --- a/src/doctor/checks/performance.rs +++ b/src/doctor/checks/performance.rs @@ -35,6 +35,7 @@ pub trait PowerHandle: Send + Sync { /// Performance modes #[derive(PartialEq, EnumString, Debug, Display)] +#[strum(ascii_case_insensitive)] pub enum PowerMode { #[strum(serialize = "performance")] Performance,