From 8285210557106f76a29bc1e1b718b5696b70dbbd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 13 May 2025 17:39:51 +0300 Subject: [PATCH] initial commit Guess which idiot accidentally deleted the .git directory --- .gitignore | 2 + Cargo.lock | 474 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 ++ src/config.rs | 197 ++++++++++++++++++++ src/core.rs | 70 ++++++++ src/cpu.rs | 233 ++++++++++++++++++++++++ src/engine.rs | 114 ++++++++++++ src/main.rs | 181 +++++++++++++++++++ src/monitor.rs | 364 +++++++++++++++++++++++++++++++++++++ 9 files changed, 1646 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/core.rs create mode 100644 src/cpu.rs create mode 100644 src/engine.rs create mode 100644 src/main.rs create mode 100644 src/monitor.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62727a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target* +result* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..59edb23 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,474 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "superfreq" +version = "0.1.0" +dependencies = [ + "clap", + "dirs", + "num_cpus", + "serde", + "toml", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8e458e6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "superfreq" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +dirs = "6.0" +clap = { version = "4.0", features = ["derive"] } +num_cpus = "1.16" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6a9d4fd --- /dev/null +++ b/src/config.rs @@ -0,0 +1,197 @@ +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::fs; +use crate::core::{OperationalMode, TurboSetting}; + +// Structs for configuration using serde::Deserialize +#[derive(Deserialize, Debug, Clone)] +pub struct ProfileConfig { + pub governor: Option, + pub turbo: Option, + pub epp: Option, // Energy Performance Preference (EPP) + pub epb: Option, // Energy Performance Bias (EPB) - usually an integer, but string for flexibility from sysfs + pub min_freq_mhz: Option, + pub max_freq_mhz: Option, + pub platform_profile: Option, +} + +impl Default for ProfileConfig { + fn default() -> Self { + ProfileConfig { + governor: Some("schedutil".to_string()), // common sensible default (?) + turbo: Some(TurboSetting::Auto), + epp: None, // defaults depend on governor and system + epb: None, // defaults depend on governor and system + min_freq_mhz: None, // no override + max_freq_mhz: None, // no override + platform_profile: None, // no override + } + } +} + +#[derive(Deserialize, Debug, Default, Clone)] +pub struct AppConfig { + #[serde(default)] + pub charger: ProfileConfig, + #[serde(default)] + pub battery: ProfileConfig, + pub battery_charge_thresholds: Option<(u8, u8)>, // (start_threshold, stop_threshold) + pub ignored_power_supplies: Option>, + #[serde(default = "default_poll_interval_sec")] + pub poll_interval_sec: u64, +} + +fn default_poll_interval_sec() -> u64 { + 5 +} + +// Error type for config loading +#[derive(Debug)] +pub enum ConfigError { + Io(std::io::Error), + Toml(toml::de::Error), + NoValidConfigFound, + HomeDirNotFound, +} + +impl From for ConfigError { + fn from(err: std::io::Error) -> ConfigError { + ConfigError::Io(err) + } +} + +impl From for ConfigError { + fn from(err: toml::de::Error) -> ConfigError { + ConfigError::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."), + } + } +} + +impl std::error::Error for ConfigError {} + +// The primary function to load application configuration. +// It tries user-specific and then system-wide TOML files. +// Falls back to default settings if no file is found or if parsing fails. +pub fn load_config() -> Result { + let mut config_paths: Vec = Vec::new(); + + // User-specific path + if let Some(home_dir) = dirs::home_dir() { + let user_config_path = home_dir.join(".config/auto_cpufreq_rs/config.toml"); + config_paths.push(user_config_path); + } else { + eprintln!("Warning: Could not determine home directory. User-specific config will not be loaded."); + } + + // System-wide path + let system_config_path = PathBuf::from("/etc/auto_cpufreq_rs/config.toml"); + config_paths.push(system_config_path); + + for path in config_paths { + if path.exists() { + println!("Attempting to load config from: {}", path.display()); + match fs::read_to_string(&path) { + Ok(contents) => { + match toml::from_str::(&contents) { + Ok(toml_app_config) => { + // Convert AppConfigToml to AppConfig + let app_config = AppConfig { + charger: ProfileConfig::from(toml_app_config.charger), + battery: ProfileConfig::from(toml_app_config.battery), + battery_charge_thresholds: toml_app_config.battery_charge_thresholds, + ignored_power_supplies: toml_app_config.ignored_power_supplies, + poll_interval_sec: toml_app_config.poll_interval_sec, + }; + return Ok(app_config); + } + Err(e) => { + eprintln!("Error parsing config file {}: {}", path.display(), e); + } + } + } + Err(e) => { + eprintln!("Error reading config file {}: {}", path.display(), e); + } + } + } + } + + println!("No configuration file found or all failed to parse. Using default configuration."); + // Construct default AppConfig by converting default AppConfigToml + let default_toml_config = AppConfigToml::default(); + Ok(AppConfig { + charger: ProfileConfig::from(default_toml_config.charger), + battery: ProfileConfig::from(default_toml_config.battery), + battery_charge_thresholds: default_toml_config.battery_charge_thresholds, + ignored_power_supplies: default_toml_config.ignored_power_supplies, + poll_interval_sec: default_toml_config.poll_interval_sec, + }) +} + +// Intermediate structs for TOML parsing +#[derive(Deserialize, Debug, Clone)] +pub struct ProfileConfigToml { + pub governor: Option, + pub turbo: Option, // "always", "auto", "never" + pub epp: Option, + pub epb: Option, + pub min_freq_mhz: Option, + pub max_freq_mhz: Option, + pub platform_profile: Option, +} + +#[derive(Deserialize, Debug, Clone, Default)] +pub struct AppConfigToml { + #[serde(default)] + pub charger: ProfileConfigToml, + #[serde(default)] + pub battery: ProfileConfigToml, + pub battery_charge_thresholds: Option<(u8, u8)>, + pub ignored_power_supplies: Option>, + #[serde(default = "default_poll_interval_sec")] + pub poll_interval_sec: u64, +} + +impl Default for ProfileConfigToml { + fn default() -> Self { + ProfileConfigToml { + governor: Some("schedutil".to_string()), + turbo: Some("auto".to_string()), + epp: None, + epb: None, + min_freq_mhz: None, + max_freq_mhz: None, + platform_profile: None, + } + } +} + + +impl From for ProfileConfig { + fn from(toml_config: ProfileConfigToml) -> Self { + ProfileConfig { + governor: toml_config.governor, + turbo: toml_config.turbo.and_then(|s| match s.to_lowercase().as_str() { + "always" => Some(TurboSetting::Always), + "auto" => Some(TurboSetting::Auto), + "never" => Some(TurboSetting::Never), + _ => None, + }), + epp: toml_config.epp, + epb: toml_config.epb, + min_freq_mhz: toml_config.min_freq_mhz, + max_freq_mhz: toml_config.max_freq_mhz, + platform_profile: toml_config.platform_profile, + } + } +} diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..102069b --- /dev/null +++ b/src/core.rs @@ -0,0 +1,70 @@ +pub struct SystemInfo { + // Overall system details + pub cpu_model: String, + pub architecture: String, + pub linux_distribution: String, +} + +pub struct CpuCoreInfo { + // Per-core data + pub core_id: u32, + pub current_frequency_mhz: Option, + pub min_frequency_mhz: Option, + pub max_frequency_mhz: Option, + pub usage_percent: Option, + pub temperature_celsius: Option, +} + +pub struct CpuGlobalInfo { + // System-wide CPU settings + pub current_governor: Option, + pub available_governors: Vec, + pub turbo_status: Option, // true for enabled, false for disabled + pub epp: Option, // Energy Performance Preference + pub epb: Option, // Energy Performance Bias + pub platform_profile: Option, +} + +pub struct BatteryInfo { + // Battery status (AC connected, charging state, capacity, power rate, charge start/stop thresholds if available). + pub name: String, + pub ac_connected: bool, + pub charging_state: Option, // e.g., "Charging", "Discharging", "Full" + pub capacity_percent: Option, + pub power_rate_watts: Option, // positive for charging, negative for discharging + pub charge_start_threshold: Option, + pub charge_stop_threshold: Option, +} + +pub struct SystemLoad { + // System load averages. + pub load_avg_1min: f32, + pub load_avg_5min: f32, + pub load_avg_15min: f32, +} + +pub struct SystemReport { + // Now combine all the above for a snapshot of the system state. + pub system_info: SystemInfo, + pub cpu_cores: Vec, + pub cpu_global: CpuGlobalInfo, + pub batteries: Vec, + pub system_load: SystemLoad, + pub timestamp: std::time::SystemTime, // so we know when the report was generated +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationalMode { + Powersave, + Performance, +} + +use serde::Deserialize; +use clap::ValueEnum; + +#[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 new file mode 100644 index 0000000..74f7858 --- /dev/null +++ b/src/cpu.rs @@ -0,0 +1,233 @@ +use crate::core::TurboSetting; +use crate::monitor::Result as MonitorResult; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; + +#[derive(Debug)] +pub enum ControlError { + Io(io::Error), + WriteError(String), + InvalidValueError(String), + NotSupported(String), + PermissionDenied(String), +} + +impl From for ControlError { + fn from(err: io::Error) -> ControlError { + match err.kind() { + io::ErrorKind::PermissionDenied => ControlError::PermissionDenied(err.to_string()), + _ => ControlError::Io(err), + } + } +} + +impl std::fmt::Display for ControlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ControlError::Io(e) => write!(f, "I/O error: {}", e), + ControlError::WriteError(s) => write!(f, "Failed to write to sysfs path: {}", s), + ControlError::InvalidValueError(s) => write!(f, "Invalid value for setting: {}", s), + ControlError::NotSupported(s) => write!(f, "Control action not supported: {}", s), + ControlError::PermissionDenied(s) => { + write!(f, "Permission denied: {}. Try running with sudo.", s) + } + } + } +} + +impl std::error::Error for ControlError {} + +pub type Result = std::result::Result; + +// Write a value to a sysfs file +fn write_sysfs_value(path: impl AsRef, value: &str) -> Result<()> { + let p = path.as_ref(); + fs::write(p, value).map_err(|e| { + let error_msg = format!("Path: {:?}, Value: '{}', Error: {}", p.display(), value, e); + if e.kind() == io::ErrorKind::PermissionDenied { + ControlError::PermissionDenied(error_msg) + } else { + ControlError::WriteError(error_msg) + } + }) +} + +fn for_each_cpu_core(mut action: F) -> Result<()> +where + F: FnMut(u32) -> Result<()>, +{ + // Using num_cpus::get() for a reliable count of logical cores accessible. + // The monitor module's get_logical_core_count might be more specific to cpufreq-capable cores, + // but for applying settings, we might want to iterate over all reported by OS. + // However, settings usually apply to cores with cpufreq. + // Let's use a similar discovery to monitor's get_logical_core_count + let mut cores_to_act_on = Vec::new(); + let path = Path::new("/sys/devices/system/cpu"); + if path.exists() { + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + let name = entry.file_name(); + if let Some(name_str) = name.to_str() { + if name_str.starts_with("cpu") + && name_str.len() > 3 + && name_str[3..].chars().all(char::is_numeric) + { + if entry.path().join("cpufreq").exists() { + if let Ok(core_id) = name_str[3..].parse::() { + cores_to_act_on.push(core_id); + } + } + } + } + } + } + } + if cores_to_act_on.is_empty() { + // Fallback if sysfs iteration above fails to find any cpufreq cores + let num_cores = num_cpus::get() as u32; + for core_id in 0..num_cores { + cores_to_act_on.push(core_id); + } + } + + for core_id in cores_to_act_on { + action(core_id)?; + } + Ok(()) +} + +pub fn set_governor(governor: &str, core_id: Option) -> Result<()> { + let action = |id: u32| { + let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor", id); + if Path::new(&path).exists() { + write_sysfs_value(&path, governor) + } else { + // Silently ignore if the path doesn't exist for a specific core, + // as not all cores might have cpufreq (e.g. offline cores) + Ok(()) + } + }; + + if let Some(id) = core_id { + action(id) + } else { + for_each_cpu_core(action) + } +} + +pub fn set_turbo(setting: TurboSetting) -> Result<()> { + let value_pstate = match setting { + TurboSetting::Always => "0", // no_turbo = 0 means turbo is enabled + TurboSetting::Never => "1", // no_turbo = 1 means turbo is disabled + TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())), + }; + let value_boost = match setting { + TurboSetting::Always => "1", // boost = 1 means turbo is enabled + TurboSetting::Never => "0", // boost = 0 means turbo is disabled + TurboSetting::Auto => return Err(ControlError::InvalidValueError("Turbo Auto cannot be directly set via intel_pstate/no_turbo or cpufreq/boost. System default.".to_string())), + }; + + let pstate_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"; + let boost_path = "/sys/devices/system/cpu/cpufreq/boost"; + + if Path::new(pstate_path).exists() { + write_sysfs_value(pstate_path, value_pstate) + } else if Path::new(boost_path).exists() { + write_sysfs_value(boost_path, value_boost) + } else { + Err(ControlError::NotSupported( + "Neither intel_pstate/no_turbo nor cpufreq/boost found.".to_string(), + )) + } +} + +pub fn set_epp(epp: &str, core_id: Option) -> Result<()> { + let action = |id: u32| { + let path = format!( + "/sys/devices/system/cpu/cpu{}/cpufreq/energy_performance_preference", + id + ); + if Path::new(&path).exists() { + write_sysfs_value(&path, epp) + } else { + Ok(()) + } + }; + if let Some(id) = core_id { + action(id) + } else { + for_each_cpu_core(action) + } +} + +pub fn set_epb(epb: &str, core_id: Option) -> Result<()> { + // EPB is often an integer 0-15. Ensure `epb` string is valid if parsing. + // For now, writing it directly as a string. + let action = |id: u32| { + let path = format!( + "/sys/devices/system/cpu/cpu{}/cpufreq/energy_performance_bias", + id + ); + if Path::new(&path).exists() { + write_sysfs_value(&path, epb) + } else { + Ok(()) + } + }; + if let Some(id) = core_id { + action(id) + } else { + for_each_cpu_core(action) + } +} + +pub fn set_min_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { + let freq_khz_str = (freq_mhz * 1000).to_string(); + let action = |id: u32| { + let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_min_freq", id); + if Path::new(&path).exists() { + write_sysfs_value(&path, &freq_khz_str) + } else { + Ok(()) + } + }; + if let Some(id) = core_id { + action(id) + } else { + for_each_cpu_core(action) + } +} + +pub fn set_max_frequency(freq_mhz: u32, core_id: Option) -> Result<()> { + let freq_khz_str = (freq_mhz * 1000).to_string(); + let action = |id: u32| { + let path = format!("/sys/devices/system/cpu/cpu{}/cpufreq/scaling_max_freq", id); + if Path::new(&path).exists() { + write_sysfs_value(&path, &freq_khz_str) + } else { + Ok(()) + } + }; + if let Some(id) = core_id { + action(id) + } else { + for_each_cpu_core(action) + } +} + +pub fn set_platform_profile(profile: &str) -> Result<()> { + let path = "/sys/firmware/acpi/platform_profile"; + if Path::new(path).exists() { + // Before writing, it'd be nice to check if the profile is in available_profiles + // Reading available profiles: /sys/firmware/acpi/platform_profile_choices + // TODO: Validate profile against available profiles here for a cleaner impl. + write_sysfs_value(path, profile) + } else { + Err(ControlError::NotSupported( + "Platform profile control not found at /sys/firmware/acpi/platform_profile." + .to_string(), + )) + } +} diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 0000000..d322aaa --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,114 @@ +use crate::core::{SystemReport, OperationalMode, TurboSetting}; +use crate::config::{AppConfig, ProfileConfig}; +use crate::cpu::{self, ControlError}; + +#[derive(Debug)] +pub enum EngineError { + ControlError(ControlError), + ConfigurationError(String), +} + +impl From for EngineError { + fn from(err: ControlError) -> Self { + EngineError::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), + } + } +} + +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, + } + } +} + +/// Determines the appropriate CPU profile based on power status or forced mode, +/// and applies the settings using functions from the `cpu` module. +pub fn determine_and_apply_settings( + report: &SystemReport, + config: &AppConfig, + force_mode: Option, +) -> Result<(), EngineError> { + let selected_profile_config: &ProfileConfig; + + if let Some(mode) = force_mode { + match mode { + OperationalMode::Powersave => { + println!("Engine: Forced Powersave mode selected. Applying 'battery' profile."); + selected_profile_config = &config.battery; + } + OperationalMode::Performance => { + println!("Engine: Forced Performance mode selected. Applying 'charger' profile."); + selected_profile_config = &config.charger; + } + } + } else { + // Determine AC/Battery status + // If no batteries, assume AC power (desktop). + // 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); + + if on_ac_power { + println!("Engine: On AC power, selecting Charger profile."); + selected_profile_config = &config.charger; + } else { + println!("Engine: On Battery power, selecting Battery profile."); + selected_profile_config = &config.battery; + } + } + + // Apply settings from selected_profile_config + // TODO: The println! statements are for temporary debugging/logging + // 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); + cpu::set_governor(governor, None)?; + } + + if let Some(turbo_setting) = selected_profile_config.turbo { + println!("Engine: Setting turbo to '{:?}'", turbo_setting); + cpu::set_turbo(turbo_setting)?; + } + + if let Some(epp) = &selected_profile_config.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); + 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); + 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); + cpu::set_max_frequency(max_freq, None)?; + } + + if let Some(profile) = &selected_profile_config.platform_profile { + println!("Engine: Setting platform profile to '{}'", profile); + cpu::set_platform_profile(profile)?; + } + + println!("Engine: Profile settings applied successfully."); + + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7a4e1a0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,181 @@ +mod core; +mod config; +mod monitor; +mod cpu; +mod engine; + +use clap::Parser; +use crate::config::AppConfig; +use crate::core::TurboSetting; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Option, +} + +#[derive(Parser, Debug)] +enum Commands { + /// Display current system information + Info, + /// Set CPU governor + SetGovernor { + governor: String, + #[clap(long)] + core_id: Option, + }, + /// Set turbo boost behavior + SetTurbo { + #[clap(value_enum)] + setting: TurboSetting, + }, + /// Set Energy Performance Preference (EPP) + SetEpp { + epp: String, + #[clap(long)] + core_id: Option, + }, + /// Set Energy Performance Bias (EPB) + SetEpb { + epb: String, // Typically 0-15 + #[clap(long)] + core_id: Option, + }, + /// Set minimum CPU frequency + SetMinFreq { + freq_mhz: u32, + #[clap(long)] + core_id: Option, + }, + /// Set maximum CPU frequency + SetMaxFreq { + freq_mhz: u32, + #[clap(long)] + core_id: Option, + }, + /// Set ACPI platform profile + SetPlatformProfile { + profile: String, + }, +} + +fn main() { + let cli = Cli::parse(); + + // Load configuration first, as it might be needed by the monitor module + // E.g., for ignored power supplies + let config = match config::load_config() { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("Error loading configuration: {}. Using default values.", e); + // Proceed with default config if loading fails, as per previous steps + AppConfig::default() + } + }; + + let command_result = match cli.command { + Some(Commands::Info) => { + match monitor::collect_system_report(&config) { + Ok(report) => { + println!("--- System Information ---"); + println!("CPU Model: {}", report.system_info.cpu_model); + println!("Architecture: {}", report.system_info.architecture); + println!("Linux Distribution: {}", report.system_info.linux_distribution); + println!("Timestamp: {:?}", report.timestamp); + + println!("\n--- CPU Global Info ---"); + println!("Current Governor: {:?}", report.cpu_global.current_governor); + println!("Available Governors: {:?}", report.cpu_global.available_governors.join(", ")); + println!("Turbo Status: {:?}", report.cpu_global.turbo_status); + println!("EPP: {:?}", report.cpu_global.epp); + println!("EPB: {:?}", report.cpu_global.epb); + println!("Platform Profile: {:?}", report.cpu_global.platform_profile); + + println!("\n--- CPU Core Info ---"); + for core_info in report.cpu_cores { + println!( + " Core {}: Current Freq: {:?} MHz, Min Freq: {:?} MHz, Max Freq: {:?} MHz, Usage: {:?}%, Temp: {:?}°C", + core_info.core_id, + core_info.current_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()), + core_info.min_frequency_mhz.map_or_else(|| "N/A".to_string(), |f| f.to_string()), + core_info.max_frequency_mhz.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)), + core_info.temperature_celsius.map_or_else(|| "N/A".to_string(), |f| format!("{:.1}", f)) + ); + } + + println!("\n--- Battery Info ---"); + if report.batteries.is_empty() { + println!(" No batteries found or all are ignored."); + } else { + for battery_info in report.batteries { + println!( + " Battery {}: AC Connected: {}, State: {:?}, Capacity: {:?}%, Power Rate: {:?} W, Charge Thresholds: {:?}-{:?}", + battery_info.name, + battery_info.ac_connected, + battery_info.charging_state.as_deref().unwrap_or("N/A"), + battery_info.capacity_percent.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)), + battery_info.charge_start_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string()), + battery_info.charge_stop_threshold.map_or_else(|| "N/A".to_string(), |t| t.to_string()) + ); + } + } + + println!("\n--- System Load ---"); + println!("Load Average (1m, 5m, 15m): {:.2}, {:.2}, {:.2}", + report.system_load.load_avg_1min, + report.system_load.load_avg_5min, + report.system_load.load_avg_15min); + Ok(()) + } + Err(e) => Err(Box::new(e) as Box), + } + } + Some(Commands::SetGovernor { governor, core_id }) => { + cpu::set_governor(&governor, core_id).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetTurbo { setting }) => { + cpu::set_turbo(setting).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetEpp { epp, core_id }) => { + cpu::set_epp(&epp, core_id).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetEpb { epb, core_id }) => { + cpu::set_epb(&epb, core_id).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetMinFreq { freq_mhz, core_id }) => { + cpu::set_min_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetMaxFreq { freq_mhz, core_id }) => { + cpu::set_max_frequency(freq_mhz, core_id).map_err(|e| Box::new(e) as Box) + } + Some(Commands::SetPlatformProfile { profile }) => { + cpu::set_platform_profile(&profile).map_err(|e| Box::new(e) as Box) + } + None => { + println!("Welcome to superfreq! Use --help for commands."); + println!("Current effective configuration: {:?}", config); + Ok(()) + } + }; + + if let Err(e) = command_result { + eprintln!("Error executing command: {}", e); + if let Some(source) = e.source() { + eprintln!("Caused by: {}", source); + } + // TODO: Consider specific error handling for PermissionDenied from cpu here + // For example, check if e.downcast_ref::() matches PermissionDenied + // and print a more specific message like "Try running with sudo." + // We'll revisit this in the future once CPU logic is more stable. + if let Some(control_error) = e.downcast_ref::() { + if matches!(control_error, cpu::ControlError::PermissionDenied(_)) { + eprintln!("Hint: This operation may require administrator privileges (e.g., run with sudo)."); + } + } + + std::process::exit(1); + } +} diff --git a/src/monitor.rs b/src/monitor.rs new file mode 100644 index 0000000..f307ae9 --- /dev/null +++ b/src/monitor.rs @@ -0,0 +1,364 @@ +use crate::core::{SystemInfo, CpuCoreInfo, CpuGlobalInfo, BatteryInfo, SystemLoad, SystemReport}; +use crate::config::AppConfig; +use std::{fs, io, path::{Path, PathBuf}, str::FromStr, time::SystemTime}; + +#[derive(Debug)] +pub enum SysMonitorError { + Io(io::Error), + ReadError(String), + ParseError(String), + NotAvailable(String), +} + +impl From for SysMonitorError { + fn from(err: io::Error) -> SysMonitorError { + SysMonitorError::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::NotAvailable(s) => write!(f, "Information not available: {}", s), + } + } +} + +impl std::error::Error for SysMonitorError {} + +pub type Result = std::result::Result; + +// Read a sysfs file to a string, trimming whitespace +fn read_sysfs_file_trimmed(path: impl AsRef) -> Result { + fs::read_to_string(path.as_ref()) + .map(|s| s.trim().to_string()) + .map_err(|e| { + SysMonitorError::ReadError(format!("Path: {:?}, Error: {}", path.as_ref().display(), e)) + }) +} + +// Read a sysfs file and parse it to a specific type +fn read_sysfs_value(path: impl AsRef) -> Result { + let content = read_sysfs_file_trimmed(path.as_ref())?; + content.parse::().map_err(|_| { + SysMonitorError::ParseError(format!( + "Could not parse '{}' from {:?}", + content, + path.as_ref().display() + )) + }) +} + +pub fn get_system_info() -> Result { + let mut cpu_model = "Unknown".to_string(); + if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") { + for line in cpuinfo.lines() { + if line.starts_with("model name") { + if let Some(val) = line.split(':').nth(1) { + cpu_model = val.trim().to_string(); + break; + } + } + } + } + + let architecture = std::env::consts::ARCH.to_string(); + + let mut linux_distribution = "Unknown".to_string(); + if let Ok(os_release) = fs::read_to_string("/etc/os-release") { + for line in os_release.lines() { + if line.starts_with("PRETTY_NAME=") { + if let Some(val) = line.split('=').nth(1) { + linux_distribution = val.trim_matches('"').to_string(); + break; + } + } + } + } else if let Ok(lsb_release) = fs::read_to_string("/etc/lsb-release") { // fallback for some systems + for line in lsb_release.lines() { + if line.starts_with("DISTRIB_DESCRIPTION=") { + if let Some(val) = line.split('=').nth(1) { + linux_distribution = val.trim_matches('"').to_string(); + break; + } + } + } + } + + + Ok(SystemInfo { + cpu_model, + architecture, + linux_distribution, + }) +} + +fn get_logical_core_count() -> Result { + let mut count = 0; + let path = Path::new("/sys/devices/system/cpu"); + if path.exists() { + for entry in fs::read_dir(path)? { + let entry = entry?; + let name = entry.file_name(); + if let Some(name_str) = name.to_str() { + if name_str.starts_with("cpu") && + name_str.len() > 3 && + name_str[3..].chars().all(char::is_numeric) { + // 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)).exists() { + // Fallback for cores that might not have cpufreq but are online (e.g. E-cores on some setups before driver loads) + // This is a simplification; true cpufreq capability is key. + // If cpufreq dir doesn't exist, it might not be controllable by this tool. + // For counting purposes, we count it if it's an online CPU. + count +=1; + } + } + } + } + } + if count == 0 { + // Fallback to num_cpus crate if sysfs parsing fails or yields 0 + Ok(num_cpus::get() as u32) + } else { + Ok(count) + } +} + + +pub fn get_cpu_core_info(core_id: u32) -> Result { + let cpufreq_path = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/cpufreq/", core_id)); + + let current_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_cur_freq")) + .map(|khz| khz / 1000) + .ok(); + let min_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_min_freq")) + .map(|khz| khz / 1000) + .ok(); + let max_frequency_mhz = read_sysfs_value::(cpufreq_path.join("scaling_max_freq")) + .map(|khz| khz / 1000) + .ok(); + + // Temperature: Iterate through hwmon to find core-specific temperatures + // This is a common but not universal approach. + let mut temperature_celsius: Option = None; + if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") { + for hw_entry in hwmon_dir.flatten() { + let hw_path = hw_entry.path(); + // Try to find a label that indicates it's for this core or package + // e.g. /sys/class/hwmon/hwmonX/name might be "coretemp" or similar + // and /sys/class/hwmon/hwmonX/tempY_label might be "Core Z" or "Physical id 0" + // This is highly system-dependent, and not all systems will have this. For now, + // we'll try a common pattern for "coretemp" driver because it works:tm: on my system. + if let Ok(name) = read_sysfs_file_trimmed(hw_path.join("name")) { + if name == "coretemp" { // Common driver for Intel core temperatures + for i in 1..=16 { // Check a reasonable number of temp inputs + let label_path = hw_path.join(format!("temp{}_label", i)); + let input_path = hw_path.join(format!("temp{}_input", i)); + if label_path.exists() && input_path.exists() { + if let Ok(label) = read_sysfs_file_trimmed(&label_path) { + // Example: "Core 0", "Core 1", etc. or "Physical id 0" for package + if label.eq_ignore_ascii_case(&format!("Core {}", core_id)) || + label.eq_ignore_ascii_case(&format!("Package id {}", core_id)) { //core_id might map to package for some sensors + if let Ok(temp_mc) = read_sysfs_value::(&input_path) { + temperature_celsius = Some(temp_mc as f32 / 1000.0); + break; // found temp for this core + } + } + } + } + } + } + } + if temperature_celsius.is_some() { break; } + } + } + + // FIXME: This is a placeholder so that I can actually run the code. It is a little + //complex to calculate from raw sysfs/procfs data. It typically involves reading /proc/stat + // and calculating deltas over time. This is out of scope for simple sysfs reads here. + // We will be returning here to this later. + let usage_percent: Option = None; + + Ok(CpuCoreInfo { + core_id, + current_frequency_mhz, + min_frequency_mhz, + max_frequency_mhz, + usage_percent, + temperature_celsius, + }) +} + +pub fn get_all_cpu_core_info() -> Result> { + let num_cores = get_logical_core_count()?; + (0..num_cores).map(get_cpu_core_info).collect() +} + +pub fn get_cpu_global_info() -> Result { + // FIXME: Assume global settings can be read from cpu0 or are consistent. + // This might not work properly for heterogeneous systems (e.g. big.LITTLE) + let cpufreq_base = Path::new("/sys/devices/system/cpu/cpu0/cpufreq/"); + + let current_governor = if cpufreq_base.join("scaling_governor").exists() { + read_sysfs_file_trimmed(cpufreq_base.join("scaling_governor")).ok() + } else { None }; + + 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![]) + } else { vec![] }; + + let turbo_status = if Path::new("/sys/devices/system/cpu/intel_pstate/no_turbo").exists() { + // 0 means turbo enabled, 1 means disabled for intel_pstate + read_sysfs_value::("/sys/devices/system/cpu/intel_pstate/no_turbo").map(|val| val == 0).ok() + } else if Path::new("/sys/devices/system/cpu/cpufreq/boost").exists() { + // 1 means turbo enabled, 0 means disabled for generic cpufreq boost + read_sysfs_value::("/sys/devices/system/cpu/cpufreq/boost").map(|val| val == 1).ok() + } else { + None + }; + + let epp = 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(); + + 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(); + + + Ok(CpuGlobalInfo { + current_governor, + available_governors, + turbo_status, + epp, + epb, + platform_profile, + }) +} + +pub fn get_battery_info(config: &AppConfig) -> Result> { + let mut batteries = Vec::new(); + let power_supply_path = Path::new("/sys/class/power_supply"); + + if !power_supply_path.exists() { + return Ok(batteries); // no power supply directory + } + + let ignored_supplies = config.ignored_power_supplies.as_ref().cloned().unwrap_or_default(); + + // Determine overall AC connection status + let mut overall_ac_connected = false; + for entry in fs::read_dir(power_supply_path)? { + let entry = entry?; + let ps_path = entry.path(); + let name = entry.file_name().into_string().unwrap_or_default(); + + // Check for AC adapter type (common names: AC, ACAD, ADP) + if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { + if ps_type == "Mains" || ps_type == "USB_PD_DRP" || ps_type == "USB_PD" || ps_type == "USB_DCP" || ps_type == "USB_CDP" || ps_type == "USB_ACA" { // USB types can also provide power + if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { + if online == 1 { + overall_ac_connected = true; + break; + } + } + } + } else if name.starts_with("AC") || name.contains("ACAD") || name.contains("ADP") { // fallback for type file missing + if let Ok(online) = read_sysfs_value::(ps_path.join("online")) { + if online == 1 { + overall_ac_connected = true; + break; + } + } + } + } + + for entry in fs::read_dir(power_supply_path)? { + let entry = entry?; + let ps_path = entry.path(); + let name = entry.file_name().into_string().unwrap_or_default(); + + if ignored_supplies.contains(&name) { + continue; + } + + if let Ok(ps_type) = read_sysfs_file_trimmed(ps_path.join("type")) { + if ps_type == "Battery" { + let status_str = read_sysfs_file_trimmed(ps_path.join("status")).ok(); + let capacity_percent = read_sysfs_value::(ps_path.join("capacity")).ok(); + + let power_rate_watts = if ps_path.join("power_now").exists() { + read_sysfs_value::(ps_path.join("power_now")) // uW + .map(|uw| uw as f32 / 1_000_000.0) + .ok() + } else if ps_path.join("current_now").exists() && ps_path.join("voltage_now").exists() { + let current_ua = read_sysfs_value::(ps_path.join("current_now")).ok(); // uA + let voltage_uv = read_sysfs_value::(ps_path.join("voltage_now")).ok(); // uV + 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) + } else { None } + } else { None }; + + let charge_start_threshold = read_sysfs_value::(ps_path.join("charge_control_start_threshold")).ok(); + let charge_stop_threshold = read_sysfs_value::(ps_path.join("charge_control_end_threshold")).ok(); + + batteries.push(BatteryInfo { + name: name.clone(), + ac_connected: overall_ac_connected, + charging_state: status_str, + capacity_percent, + power_rate_watts, + charge_start_threshold, + charge_stop_threshold, + }); + } + } + } + Ok(batteries) +} + +pub fn get_system_load() -> Result { + let loadavg_str = read_sysfs_file_trimmed("/proc/loadavg")?; + let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); + if parts.len() < 3 { + return Err(SysMonitorError::ParseError( + "Could not parse /proc/loadavg: expected at least 3 parts".to_string(), + )); + } + let load_avg_1min = parts[0].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 1min load: {}", parts[0])))?; + let load_avg_5min = parts[1].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 5min load: {}", parts[1])))?; + let load_avg_15min = parts[2].parse().map_err(|_| SysMonitorError::ParseError(format!("Failed to parse 15min load: {}", parts[2])))?; + + Ok(SystemLoad { + load_avg_1min, + load_avg_5min, + load_avg_15min, + }) +} + +pub fn collect_system_report(config: &AppConfig) -> Result { + let system_info = get_system_info()?; + let cpu_cores = get_all_cpu_core_info()?; + let cpu_global = get_cpu_global_info()?; + let batteries = get_battery_info(config)?; + let system_load = get_system_load()?; + + Ok(SystemReport { + system_info, + cpu_cores, + cpu_global, + batteries, + system_load, + timestamp: SystemTime::now(), + }) +} +