From 72ebbf3761fd5bd078a44e49b2d5c2f3cd98dfc5 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 13 May 2025 23:55:27 +0300 Subject: [PATCH] daemon: add conflict detection and governor override Also adds governor override functionality allowing users to persistently force a specific CPU governor mode. --- src/config.rs | 50 +++++++++---------- src/conflict.rs | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ src/core.rs | 38 ++++++++++---- src/cpu.rs | 77 ++++++++++++++++++++++++++++- src/daemon.rs | 55 ++++++++++++--------- src/engine.rs | 43 +++++++++------- src/main.rs | 28 +++++++---- src/monitor.rs | 86 ++++++++++++++++---------------- 8 files changed, 377 insertions(+), 128 deletions(-) create mode 100644 src/conflict.rs diff --git a/src/config.rs b/src/config.rs index 1fbbc08..9cc1eac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,7 +18,7 @@ pub struct ProfileConfig { impl Default for ProfileConfig { fn default() -> Self { - ProfileConfig { + Self { governor: Some("schedutil".to_string()), // common sensible default (?) turbo: Some(TurboSetting::Auto), epp: None, // defaults depend on governor and system @@ -45,7 +45,7 @@ pub struct AppConfig { pub daemon: DaemonConfig, } -fn default_poll_interval_sec() -> u64 { +const fn default_poll_interval_sec() -> u64 { 5 } @@ -59,24 +59,24 @@ pub enum ConfigError { } impl From for ConfigError { - fn from(err: std::io::Error) -> ConfigError { - ConfigError::Io(err) + fn from(err: std::io::Error) -> Self { + Self::Io(err) } } impl From for ConfigError { - fn from(err: toml::de::Error) -> ConfigError { - ConfigError::Toml(err) + fn from(err: toml::de::Error) -> Self { + Self::Toml(err) } } impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ConfigError::Io(e) => write!(f, "I/O error: {}", e), - ConfigError::Toml(e) => write!(f, "TOML parsing error: {}", e), - ConfigError::NoValidConfigFound => write!(f, "No valid configuration file found."), - ConfigError::HomeDirNotFound => write!(f, "Could not determine user home directory."), + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::Toml(e) => write!(f, "TOML parsing error: {e}"), + Self::NoValidConfigFound => write!(f, "No valid configuration file found."), + Self::HomeDirNotFound => write!(f, "Could not determine user home directory."), } } } @@ -128,8 +128,8 @@ pub fn load_config() -> Result { .daemon .max_poll_interval_sec, throttle_on_battery: toml_app_config.daemon.throttle_on_battery, - log_level: toml_app_config.daemon.log_level.clone(), - stats_file_path: toml_app_config.daemon.stats_file_path.clone(), + log_level: toml_app_config.daemon.log_level, + stats_file_path: toml_app_config.daemon.stats_file_path, }, }; return Ok(app_config); @@ -187,7 +187,7 @@ pub struct AppConfigToml { impl Default for ProfileConfigToml { fn default() -> Self { - ProfileConfigToml { + Self { governor: Some("schedutil".to_string()), turbo: Some("auto".to_string()), epp: None, @@ -214,19 +214,19 @@ pub const DEFAULT_LOAD_THRESHOLD_HIGH: f32 = 70.0; // enable turbo if load is ab pub const DEFAULT_LOAD_THRESHOLD_LOW: f32 = 30.0; // disable turbo if load is below this pub const DEFAULT_TEMP_THRESHOLD_HIGH: f32 = 75.0; // disable turbo if temperature is above this -fn default_load_threshold_high() -> f32 { +const fn default_load_threshold_high() -> f32 { DEFAULT_LOAD_THRESHOLD_HIGH } -fn default_load_threshold_low() -> f32 { +const fn default_load_threshold_low() -> f32 { DEFAULT_LOAD_THRESHOLD_LOW } -fn default_temp_threshold_high() -> f32 { +const fn default_temp_threshold_high() -> f32 { DEFAULT_TEMP_THRESHOLD_HIGH } impl Default for TurboAutoSettings { fn default() -> Self { - TurboAutoSettings { + Self { load_threshold_high: DEFAULT_LOAD_THRESHOLD_HIGH, load_threshold_low: DEFAULT_LOAD_THRESHOLD_LOW, temp_threshold_high: DEFAULT_TEMP_THRESHOLD_HIGH, @@ -236,7 +236,7 @@ impl Default for TurboAutoSettings { impl From for ProfileConfig { fn from(toml_config: ProfileConfigToml) -> Self { - ProfileConfig { + Self { governor: toml_config.governor, turbo: toml_config .turbo @@ -274,7 +274,7 @@ pub struct DaemonConfig { pub stats_file_path: Option, } -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] pub enum LogLevel { Error, Warning, @@ -296,27 +296,27 @@ impl Default for DaemonConfig { } } -fn default_adaptive_interval() -> bool { +const fn default_adaptive_interval() -> bool { false } -fn default_min_poll_interval_sec() -> u64 { +const fn default_min_poll_interval_sec() -> u64 { 1 } -fn default_max_poll_interval_sec() -> u64 { +const fn default_max_poll_interval_sec() -> u64 { 30 } -fn default_throttle_on_battery() -> bool { +const fn default_throttle_on_battery() -> bool { true } -fn default_log_level() -> LogLevel { +const fn default_log_level() -> LogLevel { LogLevel::Info } -fn default_stats_file_path() -> Option { +const fn default_stats_file_path() -> Option { None } diff --git a/src/conflict.rs b/src/conflict.rs new file mode 100644 index 0000000..fe08e70 --- /dev/null +++ b/src/conflict.rs @@ -0,0 +1,128 @@ +use std::path::Path; +use std::process::Command; + +/// Represents detected conflicts with other power management services +#[derive(Debug)] +pub struct ConflictDetection { + /// Whether TLP service was detected + pub tlp: bool, + /// Whether GNOME Power Profiles daemon was detected + pub gnome_power: bool, + /// Whether tuned service was detected + pub tuned: bool, + /// Other power managers that were detected + pub other: Vec, +} + +impl ConflictDetection { + /// Returns true if any conflicts were detected + pub fn has_conflicts(&self) -> bool { + self.tlp || self.gnome_power || self.tuned || !self.other.is_empty() + } + + /// Get formatted conflict information + pub fn get_conflict_message(&self) -> String { + if !self.has_conflicts() { + return "No conflicts detected with other power management services.".to_string(); + } + + let mut message = + "Potential conflicts detected with other power management services:\n".to_string(); + + if self.tlp { + message.push_str("- TLP service is active. This may interfere with CPU settings.\n"); + } + + if self.gnome_power { + message.push_str( + "- GNOME Power Profiles daemon is active. This may override CPU/power settings.\n", + ); + } + + if self.tuned { + message.push_str( + "- Tuned service is active. This may conflict with CPU frequency settings.\n", + ); + } + + for other in &self.other { + message.push_str(&format!( + "- {other} is active. This may conflict with superfreq.\n" + )); + } + + message.push_str("\nConsider disabling conflicting services for optimal operation."); + + message + } +} + +/// Detect if systemctl is available +fn systemctl_exists() -> bool { + Command::new("sh") + .arg("-c") + .arg("command -v systemctl") + .status() + .is_ok_and(|status| status.success()) +} + +/// Check if a specific systemd service is active. +// TODO: maybe we can use some kind of a binding here +// or figure out a better detection method? +fn is_service_active(service: &str) -> bool { + if !systemctl_exists() { + return false; + } + + Command::new("systemctl") + .arg("--quiet") + .arg("is-active") + .arg(service) + .status() + .is_ok_and(|status| status.success()) +} + +/// Check for conflicts with other power management services +pub fn detect_conflicts() -> ConflictDetection { + let mut conflicts = ConflictDetection { + tlp: false, + gnome_power: false, + tuned: false, + other: Vec::new(), + }; + + // Check for TLP + conflicts.tlp = is_service_active("tlp.service"); + + // Check for GNOME Power Profiles daemon + conflicts.gnome_power = is_service_active("power-profiles-daemon.service"); + + // Check for tuned + conflicts.tuned = is_service_active("tuned.service"); + + // Check for other common power managers + let other_services = ["thermald.service", "powertop.service"]; + for service in other_services { + if is_service_active(service) { + conflicts.other.push(service.to_string()); + } + } + + // Also check if TLP is installed but not running as a service + // FIXME: This will obviously not work on non-FHS distros like NixOS + // which I kinda want to prioritize. Though, since we can't actually + // predict store paths I also don't know how else we can perform this + // check... + if !conflicts.tlp + && Path::new("/usr/share/tlp").exists() + && Command::new("sh") + .arg("-c") + .arg("tlp-stat -s 2>/dev/null | grep -q 'TLP power save = enabled'") + .status() + .is_ok_and(|status| status.success()) + { + conflicts.tlp = true; + } + + conflicts +} diff --git a/src/core.rs b/src/core.rs index fa3f188..2be64ff 100644 --- a/src/core.rs +++ b/src/core.rs @@ -1,3 +1,31 @@ +use clap::ValueEnum; +use serde::Deserialize; +use std::fmt; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)] +pub enum TurboSetting { + Always, // turbo is forced on (if possible) + Auto, // system or driver controls turbo + Never, // turbo is forced off +} + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum GovernorOverrideMode { + Performance, + Powersave, + Reset, +} + +impl fmt::Display for GovernorOverrideMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Performance => write!(f, "performance"), + Self::Powersave => write!(f, "powersave"), + Self::Reset => write!(f, "reset"), + } + } +} + pub struct SystemInfo { // Overall system details pub cpu_model: String, @@ -59,13 +87,3 @@ pub enum OperationalMode { Powersave, Performance, } - -use clap::ValueEnum; -use serde::Deserialize; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, ValueEnum)] -pub enum TurboSetting { - Always, // turbo is forced on (if possible) - Auto, // system or driver controls turbo - Never, // turbo is forced off -} diff --git a/src/cpu.rs b/src/cpu.rs index 52ea6b8..ae45ffb 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,4 +1,4 @@ -use crate::core::TurboSetting; +use crate::core::{GovernorOverrideMode, TurboSetting}; use core::str; use std::{fs, io, path::Path, string::ToString}; @@ -276,3 +276,78 @@ pub fn get_platform_profiles() -> Result> { .map(ToString::to_string) .collect()) } + +/// Path for storing the governor override state +const GOVERNOR_OVERRIDE_PATH: &str = "/etc/superfreq/governor_override"; + +/// Force a specific CPU governor or reset to automatic mode +pub fn force_governor(mode: GovernorOverrideMode) -> Result<()> { + // Create directory if it doesn't exist + let dir_path = Path::new("/etc/superfreq"); + if !dir_path.exists() { + fs::create_dir_all(dir_path).map_err(|e| { + if e.kind() == io::ErrorKind::PermissionDenied { + ControlError::PermissionDenied(format!( + "Permission denied creating directory: {}. Try running with sudo.", + dir_path.display() + )) + } else { + ControlError::Io(e) + } + })?; + } + + match mode { + GovernorOverrideMode::Reset => { + // Remove the override file if it exists + if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { + fs::remove_file(GOVERNOR_OVERRIDE_PATH).map_err(|e| { + if e.kind() == io::ErrorKind::PermissionDenied { + ControlError::PermissionDenied(format!( + "Permission denied removing override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." + )) + } else { + ControlError::Io(e) + } + })?; + println!( + "Governor override has been reset. Normal profile-based settings will be used." + ); + } else { + println!("No governor override was set."); + } + Ok(()) + } + GovernorOverrideMode::Performance | GovernorOverrideMode::Powersave => { + // Create the override file with the selected governor + let governor = mode.to_string().to_lowercase(); + fs::write(GOVERNOR_OVERRIDE_PATH, &governor).map_err(|e| { + if e.kind() == io::ErrorKind::PermissionDenied { + ControlError::PermissionDenied(format!( + "Permission denied writing to override file: {GOVERNOR_OVERRIDE_PATH}. Try running with sudo." + )) + } else { + ControlError::Io(e) + } + })?; + + // Also apply the governor immediately + set_governor(&governor, None)?; + + println!( + "Governor override set to '{governor}'. This setting will persist across reboots." + ); + println!("To reset, use: superfreq force-governor reset"); + Ok(()) + } + } +} + +/// Get the current governor override if set +pub fn get_governor_override() -> Option { + if Path::new(GOVERNOR_OVERRIDE_PATH).exists() { + fs::read_to_string(GOVERNOR_OVERRIDE_PATH).ok() + } else { + None + } +} diff --git a/src/daemon.rs b/src/daemon.rs index 967978b..bcf00ee 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,4 +1,5 @@ use crate::config::{AppConfig, LogLevel}; +use crate::conflict; use crate::core::SystemReport; use crate::engine; use crate::monitor; @@ -14,7 +15,7 @@ pub fn run_daemon(config: AppConfig, verbose: bool) -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box eprintln!("ERROR: {}", message), - LogLevel::Warning => eprintln!("WARNING: {}", message), - LogLevel::Info => println!("INFO: {}", message), - LogLevel::Debug => println!("DEBUG: {}", message), + LogLevel::Error => eprintln!("ERROR: {message}"), + LogLevel::Warning => eprintln!("WARNING: {message}"), + LogLevel::Info => println!("INFO: {message}"), + LogLevel::Debug => println!("DEBUG: {message}"), } } } @@ -225,7 +234,7 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er writeln!(file, "turbo={:?}", report.cpu_global.turbo_status)?; if let Some(temp) = report.cpu_global.average_temperature_celsius { - writeln!(file, "cpu_temp={:.1}", temp)?; + writeln!(file, "cpu_temp={temp:.1}")?; } // Battery info @@ -233,7 +242,7 @@ fn write_stats_file(path: &str, report: &SystemReport) -> Result<(), std::io::Er let battery = &report.batteries[0]; writeln!(file, "ac_power={}", battery.ac_connected)?; if let Some(cap) = battery.capacity_percent { - writeln!(file, "battery_percent={}", cap)?; + writeln!(file, "battery_percent={cap}")?; } } @@ -263,12 +272,13 @@ fn determine_system_state(report: &SystemReport) -> SystemState { if let Some(battery) = report.batteries.first() { if battery.ac_connected { return SystemState::OnAC; - } else { - return SystemState::OnBattery; } + return SystemState::OnBattery; } - } else { - // No batteries means desktop, so always AC + } + + // No batteries means desktop, so always AC + if report.batteries.is_empty() { return SystemState::OnAC; } @@ -283,7 +293,8 @@ fn determine_system_state(report: &SystemReport) -> SystemState { let avg_load = report.system_load.load_avg_1min; if avg_load > 3.0 { return SystemState::HighLoad; - } else if avg_load < 0.5 { + } + if avg_load < 0.5 { return SystemState::LowLoad; } diff --git a/src/engine.rs b/src/engine.rs index d4f3af2..a047196 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -10,15 +10,15 @@ pub enum EngineError { impl From for EngineError { fn from(err: ControlError) -> Self { - EngineError::ControlError(err) + Self::ControlError(err) } } impl std::fmt::Display for EngineError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - EngineError::ControlError(e) => write!(f, "CPU control error: {}", e), - EngineError::ConfigurationError(s) => write!(f, "Configuration error: {}", s), + Self::ControlError(e) => write!(f, "CPU control error: {e}"), + Self::ConfigurationError(s) => write!(f, "Configuration error: {s}"), } } } @@ -26,8 +26,8 @@ impl std::fmt::Display for EngineError { impl std::error::Error for EngineError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { - EngineError::ControlError(e) => Some(e), - EngineError::ConfigurationError(_) => None, + Self::ControlError(e) => Some(e), + Self::ConfigurationError(_) => None, } } } @@ -39,6 +39,15 @@ pub fn determine_and_apply_settings( config: &AppConfig, force_mode: Option, ) -> Result<(), EngineError> { + // First, check if there's a governor override set + if let Some(override_governor) = cpu::get_governor_override() { + println!( + "Engine: Governor override is active: '{}'. Setting governor.", + override_governor.trim() + ); + cpu::set_governor(override_governor.trim(), None)?; + } + let selected_profile_config: &ProfileConfig; if let Some(mode) = force_mode { @@ -58,7 +67,7 @@ pub fn determine_and_apply_settings( // Otherwise, check the ac_connected status from the (first) battery. // XXX: This relies on the setting ac_connected in BatteryInfo being set correctly. let on_ac_power = report.batteries.is_empty() - || report.batteries.first().map_or(false, |b| b.ac_connected); + || report.batteries.first().is_some_and(|b| b.ac_connected); if on_ac_power { println!("Engine: On AC power, selecting Charger profile."); @@ -74,12 +83,12 @@ pub fn determine_and_apply_settings( // and we'd like to replace them with proper logging in the future. if let Some(governor) = &selected_profile_config.governor { - println!("Engine: Setting governor to '{}'", governor); + println!("Engine: Setting governor to '{governor}'"); cpu::set_governor(governor, None)?; } if let Some(turbo_setting) = selected_profile_config.turbo { - println!("Engine: Setting turbo to '{:?}'", turbo_setting); + println!("Engine: Setting turbo to '{turbo_setting:?}'"); match turbo_setting { TurboSetting::Auto => { println!("Engine: Managing turbo in auto mode based on system conditions"); @@ -90,27 +99,27 @@ pub fn determine_and_apply_settings( } if let Some(epp) = &selected_profile_config.epp { - println!("Engine: Setting EPP to '{}'", epp); + println!("Engine: Setting EPP to '{epp}'"); cpu::set_epp(epp, None)?; } if let Some(epb) = &selected_profile_config.epb { - println!("Engine: Setting EPB to '{}'", epb); + println!("Engine: Setting EPB to '{epb}'"); cpu::set_epb(epb, None)?; } if let Some(min_freq) = selected_profile_config.min_freq_mhz { - println!("Engine: Setting min frequency to '{} MHz'", min_freq); + println!("Engine: Setting min frequency to '{min_freq} MHz'"); cpu::set_min_frequency(min_freq, None)?; } if let Some(max_freq) = selected_profile_config.max_freq_mhz { - println!("Engine: Setting max frequency to '{} MHz'", max_freq); + println!("Engine: Setting max frequency to '{max_freq} MHz'"); cpu::set_max_frequency(max_freq, None)?; } if let Some(profile) = &selected_profile_config.platform_profile { - println!("Engine: Setting platform profile to '{}'", profile); + println!("Engine: Setting platform profile to '{profile}'"); cpu::set_platform_profile(profile)?; } @@ -127,7 +136,9 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() let cpu_temp = report.cpu_global.average_temperature_celsius; // Check if we have CPU usage data available - let avg_cpu_usage = if !report.cpu_cores.is_empty() { + let avg_cpu_usage = if report.cpu_cores.is_empty() { + None + } else { let sum: f32 = report .cpu_cores .iter() @@ -144,8 +155,6 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() } else { None } - } else { - None }; // Decision logic for enabling/disabling turbo @@ -190,7 +199,7 @@ fn manage_auto_turbo(report: &SystemReport, config: &ProfileConfig) -> Result<() }; match cpu::set_turbo(turbo_setting) { - Ok(_) => { + Ok(()) => { println!( "Engine: Auto Turbo: Successfully set turbo to {}", if enable_turbo { "enabled" } else { "disabled" } diff --git a/src/main.rs b/src/main.rs index 3114b79..f114a3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod conflict; mod core; mod cpu; mod daemon; @@ -6,7 +7,7 @@ mod engine; mod monitor; use crate::config::AppConfig; -use crate::core::TurboSetting; +use crate::core::{GovernorOverrideMode, TurboSetting}; use clap::Parser; #[derive(Parser, Debug)] @@ -31,6 +32,12 @@ enum Commands { #[clap(long)] core_id: Option, }, + /// Force a specific governor mode persistently + ForceGovernor { + /// Mode to force: performance, powersave, or reset + #[clap(value_enum)] + mode: GovernorOverrideMode, + }, /// Set turbo boost behavior SetTurbo { #[clap(value_enum)] @@ -72,7 +79,7 @@ fn main() { let config = match config::load_config() { Ok(cfg) => cfg, Err(e) => { - eprintln!("Error loading configuration: {}. Using default values.", e); + eprintln!("Error loading configuration: {e}. Using default values."); // Proceed with default config if loading fails, as per previous steps AppConfig::default() } @@ -104,7 +111,7 @@ fn main() { "Average CPU Temperature: {}", report.cpu_global.average_temperature_celsius.map_or_else( || "N/A (CPU temperature sensor not detected)".to_string(), - |t| format!("{:.1}°C", t) + |t| format!("{t:.1}°C") ) ); @@ -124,10 +131,10 @@ fn main() { .map_or_else(|| "N/A".to_string(), |f| f.to_string()), core_info .usage_percent - .map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)), + .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")), core_info .temperature_celsius - .map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)) + .map_or_else(|| "N/A".to_string(), |f| format!("{f:.1}")) ); } @@ -146,7 +153,7 @@ fn main() { .map_or_else(|| "N/A".to_string(), |c| c.to_string()), battery_info .power_rate_watts - .map_or_else(|| "N/A".to_string(), |p| format!("{:.2}", p)), + .map_or_else(|| "N/A".to_string(), |p| format!("{p:.2}")), battery_info .charge_start_threshold .map_or_else(|| "N/A".to_string(), |t| t.to_string()), @@ -170,6 +177,9 @@ fn main() { }, Some(Commands::SetGovernor { governor, core_id }) => cpu::set_governor(&governor, core_id) .map_err(|e| Box::new(e) as Box), + Some(Commands::ForceGovernor { mode }) => { + cpu::force_governor(mode).map_err(|e| Box::new(e) as Box) + } Some(Commands::SetTurbo { setting }) => { cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box) } @@ -192,15 +202,15 @@ fn main() { Some(Commands::Daemon { verbose }) => daemon::run_daemon(config, verbose), None => { println!("Welcome to superfreq! Use --help for commands."); - println!("Current effective configuration: {:?}", config); + println!("Current effective configuration: {config:?}"); Ok(()) } }; if let Err(e) = command_result { - eprintln!("Error executing command: {}", e); + eprintln!("Error executing command: {e}"); if let Some(source) = e.source() { - eprintln!("Caused by: {}", source); + eprintln!("Caused by: {source}"); } // TODO: Consider specific error handling for PermissionDenied from cpu here // For example, check if e.downcast_ref::() matches PermissionDenied diff --git a/src/monitor.rs b/src/monitor.rs index 6271010..a7918b3 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -20,21 +20,21 @@ pub enum SysMonitorError { } impl From for SysMonitorError { - fn from(err: io::Error) -> SysMonitorError { - SysMonitorError::Io(err) + fn from(err: io::Error) -> Self { + Self::Io(err) } } impl std::fmt::Display for SysMonitorError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - SysMonitorError::Io(e) => write!(f, "I/O error: {}", e), - SysMonitorError::ReadError(s) => write!(f, "Failed to read sysfs path: {}", s), - SysMonitorError::ParseError(s) => write!(f, "Failed to parse value: {}", s), - SysMonitorError::ProcStatParseError(s) => { - write!(f, "Failed to parse /proc/stat: {}", s) + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::ReadError(s) => write!(f, "Failed to read sysfs path: {s}"), + Self::ParseError(s) => write!(f, "Failed to parse value: {s}"), + Self::ProcStatParseError(s) => { + write!(f, "Failed to parse /proc/stat: {s}") } - SysMonitorError::NotAvailable(s) => write!(f, "Information not available: {}", s), + Self::NotAvailable(s) => write!(f, "Information not available: {s}"), } } } @@ -123,7 +123,7 @@ fn get_logical_core_count() -> Result { // Check if it's a directory representing a core that can have cpufreq if entry.path().join("cpufreq").exists() { count += 1; - } else if Path::new(&format!("/sys/devices/system/cpu/{}/online", name_str)) + } else if Path::new(&format!("/sys/devices/system/cpu/{name_str}/online")) .exists() { // Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads) @@ -159,7 +159,7 @@ struct CpuTimes { } impl CpuTimes { - fn total_time(&self) -> u64 { + const fn total_time(&self) -> u64 { self.user + self.nice + self.system @@ -170,7 +170,7 @@ impl CpuTimes { + self.steal } - fn idle_time(&self) -> u64 { + const fn idle_time(&self) -> u64 { self.idle + self.iowait } } @@ -180,20 +180,18 @@ fn read_all_cpu_times() -> Result> { let mut cpu_times_map = HashMap::new(); for line in content.lines() { - if line.starts_with("cpu") && line.chars().nth(3).map_or(false, |c| c.is_digit(10)) { + if line.starts_with("cpu") && line.chars().nth(3).is_some_and(|c| c.is_ascii_digit()) { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() < 11 { return Err(SysMonitorError::ProcStatParseError(format!( - "Line too short: {}", - line + "Line too short: {line}" ))); } let core_id_str = &parts[0][3..]; let core_id = core_id_str.parse::().map_err(|_| { SysMonitorError::ProcStatParseError(format!( - "Failed to parse core_id: {}", - core_id_str + "Failed to parse core_id: {core_id_str}" )) })?; @@ -270,7 +268,7 @@ pub fn get_cpu_core_info( prev_times: &CpuTimes, current_times: &CpuTimes, ) -> Result { - let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/cpufreq/", core_id)); + let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{core_id}/cpufreq/")); let current_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_cur_freq")) .map(|khz| khz / 1000) @@ -405,21 +403,21 @@ pub fn get_cpu_core_info( fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> Option { for i in 1..=32 { // Increased range to handle systems with many sensors - let label_path = hw_path.join(format!("temp{}_label", i)); - let input_path = hw_path.join(format!("temp{}_input", i)); + let label_path = hw_path.join(format!("temp{i}_label")); + let input_path = hw_path.join(format!("temp{i}_input")); if label_path.exists() && input_path.exists() { if let Ok(label) = read_sysfs_file_trimmed(&label_path) { // Match various common label formats: // "Core X", "core X", "Core-X", "CPU Core X", etc. - let core_pattern = format!("{} {}", label_prefix, core_id); - let alt_pattern = format!("{}-{}", label_prefix, core_id); + let core_pattern = format!("{label_prefix} {core_id}"); + let alt_pattern = format!("{label_prefix}-{core_id}"); if label.eq_ignore_ascii_case(&core_pattern) || label.eq_ignore_ascii_case(&alt_pattern) || label .to_lowercase() - .contains(&format!("core {}", core_id).to_lowercase()) + .contains(&format!("core {core_id}").to_lowercase()) { if let Ok(temp_mc) = read_sysfs_value::(&input_path) { return Some(temp_mc as f32 / 1000.0); @@ -434,8 +432,8 @@ fn get_temperature_for_core(hw_path: &Path, core_id: u32, label_prefix: &str) -> // Finds generic sensor temperatures by label fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option { for i in 1..=32 { - let label_path = hw_path.join(format!("temp{}_label", i)); - let input_path = hw_path.join(format!("temp{}_input", i)); + let label_path = hw_path.join(format!("temp{i}_label")); + let input_path = hw_path.join(format!("temp{i}_input")); if label_path.exists() && input_path.exists() { if let Ok(label) = read_sysfs_file_trimmed(&label_path) { @@ -460,7 +458,7 @@ fn get_generic_sensor_temperature(hw_path: &Path, label_name: &str) -> Option Option { for i in 1..=32 { - let input_path = hw_path.join(format!("temp{}_input", i)); + let input_path = hw_path.join(format!("temp{i}_input")); if input_path.exists() { if let Ok(temp_mc) = read_sysfs_value::(&input_path) { @@ -488,12 +486,12 @@ pub fn get_all_cpu_core_info() -> Result> { Ok(info) => core_infos.push(info), Err(e) => { // Log or handle error for a single core, maybe push a partial info or skip - eprintln!("Error getting info for core {}: {}", core_id, e); + eprintln!("Error getting info for core {core_id}: {e}"); } } } else { // Log or handle missing times for a core - eprintln!("Missing CPU time data for core {}", core_id); + eprintln!("Missing CPU time data for core {core_id}"); } } Ok(core_infos) @@ -511,9 +509,7 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { }; let available_governors = if cpufreq_base.join("scaling_available_governors").exists() { - read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")) - .map(|s| s.split_whitespace().map(String::from).collect()) - .unwrap_or_else(|_| vec![]) + read_sysfs_file_trimmed(cpufreq_base.join("scaling_available_governors")).map_or_else(|_| vec![], |s| s.split_whitespace().map(String::from).collect()) } else { vec![] }; @@ -532,43 +528,46 @@ pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> Result { None }; - let epp = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok(); + // EPP (Energy Performance Preference) + let energy_perf_pref = + read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_preference")).ok(); - // EPB is often an integer 0-15. Reading as string for now. - let epb = read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok(); + // EPB (Energy Performance Bias) + let energy_perf_bias = + read_sysfs_file_trimmed(cpufreq_base.join("energy_performance_bias")).ok(); let platform_profile = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile").ok(); let _platform_profile_choices = read_sysfs_file_trimmed("/sys/firmware/acpi/platform_profile_choices").ok(); // Calculate average CPU temperature from the core temperatures - let average_temperature_celsius = if !cpu_cores.is_empty() { + let average_temperature_celsius = if cpu_cores.is_empty() { + None + } else { // Filter cores with temperature readings, then calculate average let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores .iter() .filter(|core| core.temperature_celsius.is_some()) .collect(); - if !cores_with_temp.is_empty() { + if cores_with_temp.is_empty() { + None + } else { // Sum up all temperatures and divide by count let sum: f32 = cores_with_temp .iter() .map(|core| core.temperature_celsius.unwrap()) .sum(); Some(sum / cores_with_temp.len() as f32) - } else { - None } - } else { - None }; Ok(CpuGlobalInfo { current_governor, available_governors, turbo_status, - epp, - epb, + epp: energy_perf_pref, + epb: energy_perf_bias, platform_profile, average_temperature_celsius, }) @@ -584,8 +583,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { let ignored_supplies = config .ignored_power_supplies - .as_ref() - .cloned() + .clone() .unwrap_or_default(); // Determine overall AC connection status @@ -649,7 +647,7 @@ pub fn get_battery_info(config: &AppConfig) -> Result> { if let (Some(c), Some(v)) = (current_ua, voltage_uv) { // Power (W) = (Voltage (V) * Current (A)) // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W - Some((c as f64 * v as f64 / 1_000_000_000_000.0) as f32) + Some((f64::from(c) * f64::from(v) / 1_000_000_000_000.0) as f32) } else { None }