1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-08-02 20:07:47 +00:00

Compare commits

..

38 commits

Author SHA1 Message Date
0358e0bece
system: cpu temperatures scanning 2025-06-04 18:35:52 +03:00
2dbc747868
cpu: wip temperature scanning, waiting for raf to stand up from his desk and open his laptop on the other side of the room 2025-05-28 23:18:57 +03:00
6b4972864f
cpu: cache /proc/stat 2025-05-28 21:48:04 +03:00
15bcdd269c
system: check for chassis type 31 and move power saving check below
Co-Authored-By: flashrun24 <flashrun42@gmail.com>
2025-05-27 21:24:36 +03:00
533825beb8
system: is_ac 2025-05-22 20:12:35 +03:00
29944b7dbb
power_supply: add more stuff and store them 2025-05-22 19:49:58 +03:00
1fe3ca3bfd
cpu: add usage percent 2025-05-22 18:40:36 +03:00
3aec909d5d
cpu: add global turbo querying 2025-05-22 17:47:32 +03:00
6caa4f7941
cpu: set_ep{p,b} actually sets the attributes now 2025-05-22 17:42:35 +03:00
24fa53914d
cpu: store EPP and EPB 2025-05-21 18:43:44 +03:00
3212bc0ad5
cpu: store governor and available governors 2025-05-21 18:27:34 +03:00
c1a509328b
cpu: store frequency 2025-05-21 17:51:18 +03:00
dfa788009c
monitor: delete old code 2025-05-21 01:08:19 +03:00
fb5a891d42
main: use yansi::whenever 2025-05-21 00:38:25 +03:00
542c41ccbe
cpu: add TODO 2025-05-21 00:28:08 +03:00
45a9fd4749
cpu: cpu times 2025-05-21 00:24:44 +03:00
ec34526012
cpu&power: add more attributes 2025-05-20 19:53:59 +03:00
d5dbb36de4
fs: fix read() typesig 2025-05-20 19:13:09 +03:00
67e2115588
cpu&power: share fs impls 2025-05-20 19:10:20 +03:00
3a4b5fb530
daemon: delete some old code and create daemon scaffold 2025-05-20 18:55:12 +03:00
a862b0b456
daemon: implement polling_interval 2025-05-20 18:41:08 +03:00
a5151f475b
daemon: wip new impl 2025-05-19 23:58:35 +03:00
923f759533
config: better more enhanched expression 2025-05-19 22:32:42 +03:00
986e7e08b5
power_supply&cpu: kolor 2025-05-19 21:48:01 +03:00
36e4bc05af
power_supply&cpu: somewhat improve error messages 2025-05-19 21:44:45 +03:00
5016825802
main: move application to deltas, comment out broken modules for now 2025-05-19 21:37:58 +03:00
6349055c64
config: fix schema, toml does not have top level lists 2025-05-19 21:32:12 +03:00
98bbf28f3d
config: nuke old config and implement a new system 2025-05-19 21:25:59 +03:00
cc0cc23b0d
power_supply: rename is_battery to get_type and don't compare the type 2025-05-19 18:09:23 +03:00
c69aba87b6
power_supply: add derives to PowerSupply 2025-05-19 18:08:10 +03:00
5559d08f3e
main: delete historical logging code 2025-05-19 17:59:53 +03:00
d61564d5f5
wip unsound broken malfunctioning changes to make it compile 2025-05-19 17:57:07 +03:00
d1247c1570
cpu: impl Display for Cpu 2025-05-19 17:43:12 +03:00
bc343eefd9
power_supply&cpu: use objects 2025-05-19 17:40:30 +03:00
8f3abd1ca3
power_supply: don't ignore non-batteries 2025-05-19 16:17:44 +03:00
a1502009d5
cli: remove governor_persist 2025-05-18 23:21:57 +03:00
8764d3a2ac
battery: clean up, rename to power_supply 2025-05-18 23:21:57 +03:00
76370a7445
cpu: clean up, clean main too 2025-05-18 23:21:57 +03:00
13 changed files with 706 additions and 619 deletions

116
Cargo.lock generated
View file

@ -182,6 +182,27 @@ dependencies = [
"unicode-xid",
]
[[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 = "env_filter"
version = "0.1.3"
@ -211,6 +232,17 @@ 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"
@ -269,10 +301,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys",
]
[[package]]
@ -286,12 +320,37 @@ dependencies = [
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[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 = "log"
version = "0.4.27"
@ -332,6 +391,12 @@ 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 = "portable-atomic"
version = "1.11.0"
@ -365,6 +430,17 @@ 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 = "regex"
version = "1.11.1"
@ -429,6 +505,26 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "superfreq"
version = "0.3.2"
dependencies = [
"anyhow",
"clap",
"clap-verbosity-flag",
"ctrlc",
"derive_more",
"dirs",
"env_logger",
"jiff",
"log",
"num_cpus",
"serde",
"thiserror",
"toml",
"yansi",
]
[[package]]
name = "syn"
version = "2.0.101"
@ -526,22 +622,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "watt"
version = "0.4.0"
dependencies = [
"anyhow",
"clap",
"clap-verbosity-flag",
"ctrlc",
"derive_more",
"env_logger",
"log",
"num_cpus",
"serde",
"thiserror",
"toml",
"yansi",
]
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"

View file

@ -1,21 +1,23 @@
[package]
name = "watt"
name = "superfreq"
description = "Modern CPU frequency and power management utility for Linux"
version = "0.4.0"
version = "0.3.2"
edition = "2024"
authors = ["NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>"]
authors = ["NotAShelf <raf@notashelf.dev>"]
rust-version = "1.85"
[dependencies]
anyhow = "1.0"
clap = { version = "4.0", features = ["derive", "env"] }
clap-verbosity-flag = "3.0.2"
ctrlc = "3.4"
derive_more = { version = "2.0.1", features = ["full"] }
env_logger = "0.11"
log = "0.4"
num_cpus = "1.16"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0"
toml = "0.8"
dirs = "6.0"
clap = { version = "4.0", features = ["derive", "env"] }
num_cpus = "1.16"
ctrlc = "3.4"
log = "0.4"
env_logger = "0.11"
thiserror = "2.0"
anyhow = "1.0"
jiff = "0.2.13"
clap-verbosity-flag = "3.0.2"
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
derive_more = { version = "2.0.1", features = ["full"] }

View file

@ -1,51 +0,0 @@
use std::env;
use std::fs;
use std::path::PathBuf;
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=build.rs");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let target = out_dir
.parent() // target/debug/build/<pkg>-<hash>/out
.and_then(|p| p.parent()) // target/debug/build/<pkg>-<hash>
.and_then(|p| p.parent()) // target/debug/
.ok_or("failed to find target directory")?;
let main_binary_name = env::var("CARGO_PKG_NAME")?;
let main_binary_path = target.join(&main_binary_name);
let mut errored = false;
for name in MULTICALL_NAMES {
let hardlink_path = target.join(name);
if hardlink_path.exists() {
if hardlink_path.is_dir() {
fs::remove_dir_all(&hardlink_path)?;
} else {
fs::remove_file(&hardlink_path)?;
}
}
if let Err(error) = fs::hard_link(&main_binary_path, &hardlink_path) {
println!(
"cargo:warning=failed to create hard link '{path}': {error}",
path = hardlink_path.display(),
);
errored = true;
}
}
if errored {
println!(
"cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again"
);
println!("cargo:warning=keep in mind that this is for development purposes only");
}
Ok(())
}

View file

@ -1,3 +1,2 @@
[[rule]]
priority = 0
if = { value = "%cpu-usage", is-more-than = 0.7 }

View file

@ -6,38 +6,21 @@
nixpkgs,
...
} @ inputs: let
forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"];
pkgsForEach = forAllSystems (system:
import nixpkgs {
localSystem.system = system;
overlays = [self.overlays.default];
});
forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux"];
pkgsForEach = nixpkgs.legacyPackages;
in {
overlays = {
superfreq = final: _: {
superfreq = final.callPackage ./nix/package.nix {};
};
default = self.overlays.superfreq;
};
packages =
nixpkgs.lib.mapAttrs (system: pkgs: {
inherit (pkgs) superfreq;
packages = forAllSystems (system: {
superfreq = pkgsForEach.${system}.callPackage ./nix/package.nix {};
default = self.packages.${system}.superfreq;
})
pkgsForEach;
});
devShells =
nixpkgs.lib.mapAttrs (system: pkgs: {
default = pkgs.callPackage ./nix/shell.nix {};
})
pkgsForEach;
devShells = forAllSystems (system: {
default = pkgsForEach.${system}.callPackage ./nix/shell.nix {};
});
nixosModules = {
superfreq = import ./nix/module.nix inputs;
default = self.nixosModules.superfreq;
};
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra);
};
}

View file

@ -155,151 +155,92 @@ impl PowerDelta {
}
}
macro_rules! named {
($variant:ident => $value:literal) => {
pub mod $variant {
pub fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str($value)
}
pub fn deserialize<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<(), D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ();
fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result {
writer.write_str(concat!("\"", $value, "\""))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
if value != $value {
return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self));
}
Ok(())
}
}
deserializer.deserialize_str(Visitor)
}
}
};
}
mod expression {
named!(cpu_usage => "%cpu-usage");
named!(cpu_usage_volatility => "$cpu-usage-volatility");
named!(cpu_temperature => "$cpu-temperature");
named!(cpu_temperature_volatility => "$cpu-temperature-volatility");
named!(cpu_idle_seconds => "$cpu-idle-seconds");
named!(power_supply_charge => "%power-supply-charge");
named!(power_supply_discharge_rate => "%power-supply-discharge-rate");
named!(discharging => "?discharging");
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(untagged, rename_all = "kebab-case")]
pub enum Expression {
#[serde(with = "expression::cpu_usage")]
#[serde(rename = "%cpu-usage")]
CpuUsage,
#[serde(with = "expression::cpu_usage_volatility")]
#[serde(rename = "$cpu-usage-volatility")]
CpuUsageVolatility,
#[serde(with = "expression::cpu_temperature")]
#[serde(rename = "$cpu-temperature")]
CpuTemperature,
#[serde(with = "expression::cpu_temperature_volatility")]
#[serde(rename = "$cpu-temperature-volatility")]
CpuTemperatureVolatility,
#[serde(with = "expression::cpu_idle_seconds")]
#[serde(rename = "$cpu-idle-seconds")]
CpuIdleSeconds,
#[serde(with = "expression::power_supply_charge")]
#[serde(rename = "%power-supply-charge")]
PowerSupplyCharge,
#[serde(with = "expression::power_supply_discharge_rate")]
#[serde(rename = "%power-supply-discharge-rate")]
PowerSupplyDischargeRate,
#[serde(with = "expression::discharging")]
Discharging,
#[serde(rename = "?charging")]
Charging,
#[serde(rename = "?on-battery")]
OnBattery,
Boolean(bool),
#[serde(rename = "#false")]
False,
#[default]
#[serde(rename = "#true")]
True,
Number(f64),
Plus {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "plus")]
b: Box<Expression>,
value: Box<Expression>,
plus: Box<Expression>,
},
Minus {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "minus")]
b: Box<Expression>,
value: Box<Expression>,
minus: Box<Expression>,
},
Multiply {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "multiply")]
b: Box<Expression>,
value: Box<Expression>,
multiply: Box<Expression>,
},
Power {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "power")]
b: Box<Expression>,
value: Box<Expression>,
power: Box<Expression>,
},
Divide {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "divide")]
b: Box<Expression>,
value: Box<Expression>,
divide: Box<Expression>,
},
LessThan {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-less-than")]
b: Box<Expression>,
value: Box<Expression>,
is_less_than: Box<Expression>,
},
MoreThan {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-more-than")]
b: Box<Expression>,
value: Box<Expression>,
is_more_than: Box<Expression>,
},
Equal {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-equal")]
b: Box<Expression>,
value: Box<Expression>,
is_equal: Box<Expression>,
leeway: Box<Expression>,
},
And {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "and")]
b: Box<Expression>,
value: Box<Expression>,
and: Box<Expression>,
},
All {
all: Vec<Expression>,
},
Or {
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "or")]
b: Box<Expression>,
value: Box<Expression>,
or: Box<Expression>,
},
Any {
any: Vec<Expression>,
@ -310,164 +251,25 @@ pub enum Expression {
},
}
impl Default for Expression {
fn default() -> Self {
Self::Boolean(true)
}
}
impl Expression {
pub fn as_number(&self) -> anyhow::Result<f64> {
let Self::Number(number) = self else {
bail!("tried to cast '{self:?}' to a number, failed")
};
Ok(*number)
}
pub fn as_boolean(&self) -> anyhow::Result<bool> {
let Self::Boolean(boolean) = self else {
bail!("tried to cast '{self:?}' to a boolean, failed")
};
Ok(*boolean)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EvalState {
pub cpu_usage: f64,
pub cpu_usage_volatility: Option<f64>,
pub cpu_temperature: f64,
pub cpu_temperature_volatility: Option<f64>,
pub cpu_idle_seconds: f64,
pub power_supply_charge: f64,
pub power_supply_discharge_rate: Option<f64>,
pub discharging: bool,
}
impl Expression {
pub fn eval(&self, state: &EvalState) -> anyhow::Result<Option<Expression>> {
use Expression::*;
macro_rules! try_ok {
($expression:expr) => {
match $expression {
Some(value) => value,
None => return Ok(None),
}
};
}
macro_rules! eval {
($expression:expr) => {
try_ok!($expression.eval(state)?)
};
}
// [e8dax09]: This may be look inefficient, and it definitely isn't optimal,
// but expressions in rules are usually so small that it doesn't matter or
// make a perceiveable performance difference.
//
// We also want to be strict, instead of lazy in binary operations, because
// we want to catch type errors immediately.
//
// FIXME: We currently cannot catch errors that will happen when propagating None.
// You can have a type error go uncaught on first startup by using $cpu-usage-volatility
// incorrectly, for example.
Ok(Some(match self {
CpuUsage => Number(state.cpu_usage),
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
CpuTemperature => Number(state.cpu_temperature),
CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)),
CpuIdleSeconds => Number(state.cpu_idle_seconds),
PowerSupplyCharge => Number(state.cpu_idle_seconds),
PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)),
Discharging => Boolean(state.discharging),
literal @ (Boolean(_) | Number(_)) => literal.clone(),
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
Minus { a, b } => Number(eval!(a).as_number()? - eval!(b).as_number()?),
Multiply { a, b } => Number(eval!(a).as_number()? * eval!(b).as_number()?),
Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)),
Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?),
MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?),
Equal { a, b, leeway } => {
let a = eval!(a).as_number()?;
let b = eval!(b).as_number()?;
let leeway = eval!(leeway).as_number()?;
let minimum = a - leeway;
let maximum = a + leeway;
Boolean(minimum < b && b < maximum)
}
And { a, b } => {
let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?;
Boolean(a && b)
}
All { all } => {
let mut result = true;
for value in all {
let value = eval!(value).as_boolean()?;
result = result && value;
}
Boolean(result)
}
Or { a, b } => {
let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?;
Boolean(a || b)
}
Any { any } => {
let mut result = false;
for value in any {
let value = eval!(value).as_boolean()?;
result = result || value;
}
Boolean(result)
}
Not { not } => Boolean(!eval!(not).as_boolean()?),
}))
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Rule {
pub priority: u8,
priority: u8,
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
pub condition: Expression,
if_: Expression,
#[serde(default, skip_serializing_if = "is_default")]
pub cpu: CpuDelta,
cpu: CpuDelta,
#[serde(default, skip_serializing_if = "is_default")]
pub power: PowerDelta,
power: PowerDelta,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(default, rename_all = "kebab-case")]
pub struct DaemonConfig {
#[serde(rename = "rule")]
pub rules: Vec<Rule>,
rules: Vec<Rule>,
}
impl DaemonConfig {
@ -476,8 +278,7 @@ impl DaemonConfig {
format!("failed to read config from '{path}'", path = path.display())
})?;
let mut config: Self = toml::from_str(&contents)
.with_context(|| format!("failed to parse file at '{path}'", path = path.display(),))?;
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
{
let mut priorities = Vec::with_capacity(config.rules.len());
@ -491,10 +292,6 @@ impl DaemonConfig {
}
}
config.rules.sort_by_key(|rule| rule.priority);
log::debug!("loaded config: {config:#?}");
Ok(config)
}
}

51
src/core.rs Normal file
View file

@ -0,0 +1,51 @@
pub struct SystemInfo {
// Overall system details
pub cpu_model: String,
}
pub struct CpuCoreInfo {
// Per-core data
pub core_id: u32,
pub temperature_celsius: Option<f32>,
}
pub struct CpuGlobalInfo {
// System-wide CPU settings
pub epp: Option<String>, // Energy Performance Preference
pub epb: Option<String>, // Energy Performance Bias
pub average_temperature_celsius: Option<f32>, // Average temperature across all cores
}
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<String>, // e.g., "Charging", "Discharging", "Full"
pub capacity_percent: Option<u8>,
pub power_rate_watts: Option<f32>, // positive for charging, negative for discharging
pub charge_start_threshold: Option<u8>,
pub charge_stop_threshold: Option<u8>,
}
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<CpuCoreInfo>,
pub cpu_global: CpuGlobalInfo,
pub batteries: Vec<BatteryInfo>,
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,
}

View file

@ -1,14 +1,13 @@
use anyhow::{Context, bail};
use yansi::Paint as _;
use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString};
use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString};
use crate::fs;
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CpuRescanCache {
stat: OnceCell<HashMap<u32, CpuStat>>,
info: OnceCell<HashMap<u32, Rc<HashMap<String, String>>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -64,7 +63,6 @@ pub struct Cpu {
pub epb: Option<String>,
pub stat: CpuStat,
pub info: Option<Rc<HashMap<String, String>>>,
pub temperature: Option<f64>,
}
@ -106,7 +104,6 @@ impl Cpu {
softirq: 0,
steal: 0,
},
info: None,
temperature: None,
};
@ -174,7 +171,6 @@ impl Cpu {
}
self.rescan_stat(cache)?;
self.rescan_info(cache)?;
Ok(())
}
@ -349,57 +345,6 @@ impl Cpu {
Ok(())
}
fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
// OnceCell::get_or_try_init is unstable. Cope:
let info = match cache.info.get() {
Some(stat) => stat,
None => {
let content = fs::read("/proc/cpuinfo")
.context("failed to read CPU info")?
.context("/proc/cpuinfo does not exist")?;
let mut info = HashMap::new();
let mut current_number = None;
let mut current_data = HashMap::new();
macro_rules! try_save_data {
() => {
if let Some(number) = current_number.take() {
info.insert(number, Rc::new(mem::take(&mut current_data)));
}
};
}
for line in content.lines() {
let parts = line.splitn(2, ':').collect::<Vec<_>>();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
if key == "processor" {
try_save_data!();
current_number = value.parse::<u32>().ok();
} else {
current_data.insert(key.to_owned(), value.to_owned());
}
}
}
try_save_data!();
cache.info.set(info).unwrap();
cache.info.get().unwrap()
}
};
self.info = info.get(&self.number).cloned();
Ok(())
}
pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> {
let Self {
number,

View file

@ -1,18 +1,16 @@
use std::{
cell::LazyCell,
collections::{HashMap, VecDeque},
ops::Deref,
collections::VecDeque,
ops,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant},
};
use anyhow::Context;
use crate::{config, system};
use crate::config;
/// Calculate the idle time multiplier based on system idle time.
///
@ -44,8 +42,8 @@ struct Daemon {
/// The last computed polling interval.
last_polling_interval: Option<Duration>,
/// The system state.
system: system::System,
/// Whether if we are charging right now.
charging: bool,
/// CPU usage and temperature log.
cpu_log: VecDeque<CpuLog>,
@ -54,56 +52,6 @@ struct Daemon {
power_supply_log: VecDeque<PowerSupplyLog>,
}
impl Daemon {
fn rescan(&mut self) -> anyhow::Result<()> {
self.system.rescan()?;
while self.cpu_log.len() > 99 {
self.cpu_log.pop_front();
}
self.cpu_log.push_back(CpuLog {
at: Instant::now(),
usage: self
.system
.cpus
.iter()
.map(|cpu| cpu.stat.usage())
.sum::<f64>()
/ self.system.cpus.len() as f64,
temperature: self.system.cpu_temperatures.values().sum::<f64>()
/ self.system.cpu_temperatures.len() as f64,
});
let at = Instant::now();
let (charge_sum, charge_nr) =
self.system
.power_supplies
.iter()
.fold((0.0, 0u32), |(sum, count), power_supply| {
if let Some(charge_percent) = power_supply.charge_percent {
(sum + charge_percent, count + 1)
} else {
(sum, count)
}
});
while self.power_supply_log.len() > 99 {
self.power_supply_log.pop_front();
}
self.power_supply_log.push_back(PowerSupplyLog {
at,
charge: charge_sum / charge_nr as f64,
});
Ok(())
}
}
struct CpuLog {
at: Instant,
@ -115,6 +63,8 @@ struct CpuLog {
}
struct CpuVolatility {
at: ops::Range<Instant>,
usage: f64,
temperature: f64,
@ -122,17 +72,6 @@ struct CpuVolatility {
impl Daemon {
fn cpu_volatility(&self) -> Option<CpuVolatility> {
let recent_log_count = self
.cpu_log
.iter()
.rev()
.take_while(|log| log.at.elapsed() < Duration::from_secs(5 * 60))
.count();
if recent_log_count < 2 {
return None;
}
if self.cpu_log.len() < 2 {
return None;
}
@ -152,6 +91,8 @@ impl Daemon {
}
Some(CpuVolatility {
at: self.cpu_log.front().unwrap().at..self.cpu_log.back().unwrap().at,
usage: usage_change_sum / change_count as f64,
temperature: temperature_change_sum / change_count as f64,
})
@ -193,13 +134,6 @@ struct PowerSupplyLog {
}
impl Daemon {
fn discharging(&self) -> bool {
self.system
.power_supplies
.iter()
.any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging"))
}
/// Calculates the discharge rate, returns a number between 0 and 1.
///
/// The discharge rate is averaged per hour.
@ -247,7 +181,7 @@ impl Daemon {
let mut interval = Duration::from_secs(5);
// We are on battery, so we must be more conservative with our polling.
if self.discharging() {
if !self.charging {
match self.power_supply_discharge_rate() {
Some(discharge_rate) => {
if discharge_rate > 0.2 {
@ -309,8 +243,6 @@ impl Daemon {
}
pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
assert!(config.rules.is_sorted_by_key(|rule| rule.priority));
log::info!("starting daemon...");
let cancelled = Arc::new(AtomicBool::new(false));
@ -322,87 +254,7 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
})
.context("failed to set Ctrl-C handler")?;
let mut daemon = Daemon {
last_user_activity: Instant::now(),
last_polling_interval: None,
system: system::System::new()?,
cpu_log: VecDeque::new(),
power_supply_log: VecDeque::new(),
};
while !cancelled.load(Ordering::SeqCst) {
daemon.rescan()?;
let sleep_until = Instant::now() + daemon.polling_interval();
let state = config::EvalState {
cpu_usage: daemon.cpu_log.back().unwrap().usage,
cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage),
cpu_temperature: daemon.cpu_log.back().unwrap().temperature,
cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature),
cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(),
power_supply_charge: daemon.power_supply_log.back().unwrap().charge,
power_supply_discharge_rate: daemon.power_supply_discharge_rate(),
discharging: daemon.discharging(),
};
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
for rule in &config.rules {
let Some(condition) = rule.condition.eval(&state)? else {
continue;
};
let cpu_for = rule.cpu.for_.as_ref().unwrap_or_else(|| &*all_cpus);
for cpu in cpu_for {
let delta = cpu_delta_for.entry(*cpu).or_default();
delta.for_ = Some(vec![*cpu]);
if let Some(governor) = rule.cpu.governor.as_ref() {
delta.governor = Some(governor.clone());
}
if let Some(epp) = rule.cpu.energy_performance_preference.as_ref() {
delta.energy_performance_preference = Some(epp.clone());
}
if let Some(epb) = rule.cpu.energy_performance_bias.as_ref() {
delta.energy_performance_bias = Some(epb.clone());
}
if let Some(mhz_minimum) = rule.cpu.frequency_mhz_minimum {
delta.frequency_mhz_minimum = Some(mhz_minimum);
}
if let Some(mhz_maximum) = rule.cpu.frequency_mhz_maximum {
delta.frequency_mhz_maximum = Some(mhz_maximum);
}
if let Some(turbo) = rule.cpu.turbo {
delta.turbo = Some(turbo);
}
}
// TODO: Also merge this into one like CPU.
if condition.as_boolean()? {
rule.power.apply()?;
}
}
for delta in cpu_delta_for.values() {
delta.apply()?;
}
if let Some(delay) = sleep_until.checked_duration_since(Instant::now()) {
thread::sleep(delay);
}
}
while !cancelled.load(Ordering::SeqCst) {}
log::info!("exiting...");

426
src/daemon_old.rs Normal file
View file

@ -0,0 +1,426 @@
use anyhow::Context;
use anyhow::bail;
use crate::config::AppConfig;
use crate::core::SystemReport;
use crate::engine;
use crate::monitor;
use std::collections::VecDeque;
use std::fs::File;
use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
/// Tracks historical system data for "advanced" adaptive polling
#[derive(Debug)]
struct SystemHistory {
/// Last several CPU usage measurements
cpu_usage_history: VecDeque<f32>,
/// Last several temperature readings
temperature_history: VecDeque<f32>,
/// Time of last detected user activity
last_user_activity: Instant,
/// Previous battery percentage (to calculate discharge rate)
last_battery_percentage: Option<f32>,
/// Timestamp of last battery reading
last_battery_timestamp: Option<Instant>,
/// Battery discharge rate (%/hour)
battery_discharge_rate: Option<f32>,
/// Time spent in each system state
state_durations: std::collections::HashMap<SystemState, Duration>,
/// Last time a state transition happened
last_state_change: Instant,
/// Current system state
current_state: SystemState,
/// Last computed optimal polling interval
last_computed_interval: Option<u64>,
}
impl SystemHistory {
/// Update system history with new report data
fn update(&mut self, report: &SystemReport) {
// Update CPU usage history
if !report.cpu_cores.is_empty() {
let mut total_usage: f32 = 0.0;
let mut core_count: usize = 0;
for core in &report.cpu_cores {
if let Some(usage) = core.usage_percent {
total_usage += usage;
core_count += 1;
}
}
if core_count > 0 {
let avg_usage = total_usage / core_count as f32;
// Keep only the last 5 measurements
if self.cpu_usage_history.len() >= 5 {
self.cpu_usage_history.pop_front();
}
self.cpu_usage_history.push_back(avg_usage);
// Update last_user_activity if CPU usage indicates activity
// Consider significant CPU usage or sudden change as user activity
if avg_usage > 20.0
|| (self.cpu_usage_history.len() > 1
&& (avg_usage - self.cpu_usage_history[self.cpu_usage_history.len() - 2])
.abs()
> 15.0)
{
self.last_user_activity = Instant::now();
log::debug!("User activity detected based on CPU usage");
}
}
}
// Update temperature history
if let Some(temp) = report.cpu_global.average_temperature_celsius {
if self.temperature_history.len() >= 5 {
self.temperature_history.pop_front();
}
self.temperature_history.push_back(temp);
// Significant temperature increase can indicate user activity
if self.temperature_history.len() > 1 {
let temp_change =
temp - self.temperature_history[self.temperature_history.len() - 2];
if temp_change > 5.0 {
// 5°C rise in temperature
self.last_user_activity = Instant::now();
log::debug!("User activity detected based on temperature change");
}
}
}
// Update battery discharge rate
if let Some(battery) = report.batteries.first() {
// Reset when we are charging or have just connected AC
if battery.ac_connected {
// Reset discharge tracking but continue updating the rest of
// the history so we still detect activity/load changes on AC.
self.battery_discharge_rate = None;
self.last_battery_percentage = None;
self.last_battery_timestamp = None;
}
if let Some(current_percentage) = battery.capacity_percent {
let current_percent = f32::from(current_percentage);
if let (Some(last_percentage), Some(last_timestamp)) =
(self.last_battery_percentage, self.last_battery_timestamp)
{
let elapsed_hours = last_timestamp.elapsed().as_secs_f32() / 3600.0;
// Only calculate discharge rate if at least 30 seconds have passed
// and we're not on AC power
if elapsed_hours > 0.0083 && !battery.ac_connected {
// 0.0083 hours = 30 seconds
// Calculate discharge rate in percent per hour
let percent_change = last_percentage - current_percent;
if percent_change > 0.0 {
// Only if battery is discharging
let hourly_rate = percent_change / elapsed_hours;
// Clamp the discharge rate to a reasonable maximum value (100%/hour)
let clamped_rate = hourly_rate.min(100.0);
self.battery_discharge_rate = Some(clamped_rate);
}
}
}
self.last_battery_percentage = Some(current_percent);
self.last_battery_timestamp = Some(Instant::now());
}
}
// Update system state tracking
let new_state = determine_system_state(report, self);
if new_state != self.current_state {
// Record time spent in previous state
let time_in_state = self.last_state_change.elapsed();
*self
.state_durations
.entry(self.current_state.clone())
.or_insert(Duration::ZERO) += time_in_state;
// State changes (except to Idle) likely indicate user activity
if new_state != SystemState::Idle && new_state != SystemState::LowLoad {
self.last_user_activity = Instant::now();
log::debug!("User activity detected based on system state change to {new_state:?}");
}
// Update state
self.current_state = new_state;
self.last_state_change = Instant::now();
}
// Check for significant load changes
if report.system_load.load_avg_1min > 1.0 {
self.last_user_activity = Instant::now();
log::debug!("User activity detected based on system load");
}
}
/// Calculate CPU usage volatility (how much it's changing)
fn get_cpu_volatility(&self) -> f32 {
if self.cpu_usage_history.len() < 2 {
return 0.0;
}
let mut sum_of_changes = 0.0;
for i in 1..self.cpu_usage_history.len() {
sum_of_changes += (self.cpu_usage_history[i] - self.cpu_usage_history[i - 1]).abs();
}
sum_of_changes / (self.cpu_usage_history.len() - 1) as f32
}
/// Calculate temperature volatility
fn get_temperature_volatility(&self) -> f32 {
if self.temperature_history.len() < 2 {
return 0.0;
}
let mut sum_of_changes = 0.0;
for i in 1..self.temperature_history.len() {
sum_of_changes += (self.temperature_history[i] - self.temperature_history[i - 1]).abs();
}
sum_of_changes / (self.temperature_history.len() - 1) as f32
}
/// Determine if the system appears to be idle
fn is_system_idle(&self) -> bool {
if self.cpu_usage_history.is_empty() {
return false;
}
// System considered idle if the average CPU usage of last readings is below 10%
let recent_avg =
self.cpu_usage_history.iter().sum::<f32>() / self.cpu_usage_history.len() as f32;
recent_avg < 10.0 && self.get_cpu_volatility() < 5.0
}
}
/// Run the daemon
pub fn run_daemon(config: AppConfig) -> anyhow::Result<()> {
log::info!("Starting superfreq daemon...");
// Validate critical configuration values before proceeding
validate_poll_intervals(
config.daemon.min_poll_interval_sec,
config.daemon.max_poll_interval_sec,
)?;
// Create a flag that will be set to true when a signal is received
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
// Set up signal handlers
ctrlc::set_handler(move || {
log::info!("Received shutdown signal, exiting...");
r.store(false, Ordering::SeqCst);
})
.context("failed to set Ctrl-C handler")?;
log::info!(
"Daemon initialized with poll interval: {}s",
config.daemon.poll_interval_sec
);
// Set up stats file if configured
if let Some(stats_path) = &config.daemon.stats_file_path {
log::info!("Stats will be written to: {stats_path}");
}
// Variables for adaptive polling
// Make sure that the poll interval is *never* zero to prevent a busy loop
let mut current_poll_interval = config.daemon.poll_interval_sec.max(1);
if config.daemon.poll_interval_sec == 0 {
log::warn!(
"Poll interval is set to zero in config, using 1s minimum to prevent a busy loop"
);
}
let mut system_history = SystemHistory::default();
// Main loop
while running.load(Ordering::SeqCst) {
let start_time = Instant::now();
match monitor::collect_system_report(&config) {
Ok(report) => {
log::debug!("Collected system report, applying settings...");
// Store the current state before updating history
let previous_state = system_history.current_state.clone();
// Update system history with new data
system_history.update(&report);
// Update the stats file if configured
if let Some(stats_path) = &config.daemon.stats_file_path {
if let Err(e) = write_stats_file(stats_path, &report) {
log::error!("Failed to write stats file: {e}");
}
}
match engine::determine_and_apply_settings(&report, &config, None) {
Ok(()) => {
log::debug!("Successfully applied system settings");
// If system state changed, log the new state
if system_history.current_state != previous_state {
log::info!(
"System state changed to: {:?}",
system_history.current_state
);
}
}
Err(e) => {
log::error!("Error applying system settings: {e}");
}
}
// Check if we're on battery
let on_battery = !report.batteries.is_empty()
&& report.batteries.first().is_some_and(|b| !b.ac_connected);
// Calculate optimal polling interval if adaptive polling is enabled
if config.daemon.adaptive_interval {
match system_history.calculate_optimal_interval(&config, on_battery) {
Ok(optimal_interval) => {
// Store the new interval
system_history.last_computed_interval = Some(optimal_interval);
log::debug!("Recalculated optimal interval: {optimal_interval}s");
// Don't change the interval too dramatically at once
match optimal_interval.cmp(&current_poll_interval) {
std::cmp::Ordering::Greater => {
current_poll_interval =
(current_poll_interval + optimal_interval) / 2;
}
std::cmp::Ordering::Less => {
current_poll_interval = current_poll_interval
- ((current_poll_interval - optimal_interval) / 2).max(1);
}
std::cmp::Ordering::Equal => {
// No change needed when they're equal
}
}
}
Err(e) => {
// Log the error and stop the daemon when an invalid configuration is detected
log::error!("Critical configuration error: {e}");
running.store(false, Ordering::SeqCst);
break;
}
}
// Make sure that we respect the (user) configured min and max limits
current_poll_interval = current_poll_interval.clamp(
config.daemon.min_poll_interval_sec,
config.daemon.max_poll_interval_sec,
);
log::debug!("Adaptive polling: set interval to {current_poll_interval}s");
} else {
// If adaptive polling is disabled, still apply battery-saving adjustment
if config.daemon.throttle_on_battery && on_battery {
let battery_multiplier = 2; // poll half as often on battery
// We need to make sure `poll_interval_sec` is *at least* 1
// before multiplying.
let safe_interval = config.daemon.poll_interval_sec.max(1);
current_poll_interval = (safe_interval * battery_multiplier)
.min(config.daemon.max_poll_interval_sec);
log::debug!(
"On battery power, increased poll interval to {current_poll_interval}s"
);
} else {
// Use the configured poll interval
current_poll_interval = config.daemon.poll_interval_sec.max(1);
if config.daemon.poll_interval_sec == 0 {
log::debug!(
"Using minimum poll interval of 1s instead of configured 0s"
);
}
}
}
}
Err(e) => {
log::error!("Error collecting system report: {e}");
}
}
// Sleep for the remaining time in the poll interval
let elapsed = start_time.elapsed();
let poll_duration = Duration::from_secs(current_poll_interval);
if elapsed < poll_duration {
let sleep_time = poll_duration - elapsed;
log::debug!("Sleeping for {}s until next cycle", sleep_time.as_secs());
std::thread::sleep(sleep_time);
}
}
log::info!("Daemon stopped");
Ok(())
}
/// Simplified system state used for determining when to adjust polling interval
#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)]
enum SystemState {
#[default]
Unknown,
OnAC,
OnBattery,
HighLoad,
LowLoad,
HighTemp,
Idle,
}
/// Determine the current system state for adaptive polling
fn determine_system_state(report: &SystemReport, history: &SystemHistory) -> SystemState {
// Check power state first
if !report.batteries.is_empty() {
if let Some(battery) = report.batteries.first() {
if battery.ac_connected {
return SystemState::OnAC;
}
return SystemState::OnBattery;
}
}
// No batteries means desktop, so always AC
if report.batteries.is_empty() {
return SystemState::OnAC;
}
// Check temperature
if let Some(temp) = report.cpu_global.average_temperature_celsius {
if temp > 80.0 {
return SystemState::HighTemp;
}
}
// Check load first, as high load should take precedence over idle state
let avg_load = report.system_load.load_avg_1min;
if avg_load > 3.0 {
return SystemState::HighLoad;
}
// Check idle state only if we don't have high load
if history.is_system_idle() {
return SystemState::Idle;
}
// Check for low load
if avg_load < 0.5 {
return SystemState::LowLoad;
}
// Default case
SystemState::Unknown
}

View file

@ -21,52 +21,30 @@ use yansi::Paint as _;
#[derive(clap::Parser, Debug)]
#[clap(author, version, about)]
struct Cli {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
#[clap(subcommand)]
command: Command,
}
#[derive(clap::Parser, Debug)]
#[clap(multicall = true)]
enum Command {
/// Watt daemon.
Watt {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
/// Display information.
Info,
/// Start the daemon.
Start {
/// The daemon config path.
#[arg(long, env = "WATT_CONFIG")]
config: PathBuf,
},
/// CPU metadata and modification utility.
Cpu {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
#[clap(subcommand)]
command: CpuCommand,
},
/// Power supply metadata and modification utility.
Power {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
#[clap(subcommand)]
command: PowerCommand,
},
}
#[derive(clap::Parser, Debug)]
enum CpuCommand {
/// Modify CPU attributes.
Set(config::CpuDelta),
}
CpuSet(config::CpuDelta),
#[derive(clap::Parser, Debug)]
enum PowerCommand {
/// Modify power supply attributes.
Set(config::PowerDelta),
PowerSet(config::PowerDelta),
}
fn real_main() -> anyhow::Result<()> {
@ -74,33 +52,24 @@ fn real_main() -> anyhow::Result<()> {
yansi::whenever(yansi::Condition::TTY_AND_COLOR);
let (Command::Watt { verbosity, .. }
| Command::Cpu { verbosity, .. }
| Command::Power { verbosity, .. }) = cli.command;
env_logger::Builder::new()
.filter_level(verbosity.log_level_filter())
.filter_level(cli.verbosity.log_level_filter())
.format_timestamp(None)
.format_module_path(false)
.init();
match cli.command {
Command::Watt { config, .. } => {
let config =
config::DaemonConfig::load_from(&config).context("failed to load daemon config")?;
Command::Info => todo!(),
Command::Start { config } => {
let config = config::DaemonConfig::load_from(&config)
.context("failed to load daemon config file")?;
daemon::run(config)
}
Command::Cpu {
command: CpuCommand::Set(delta),
..
} => delta.apply(),
Command::Power {
command: PowerCommand::Set(delta),
..
} => delta.apply(),
Command::CpuSet(delta) => delta.apply(),
Command::PowerSet(delta) => delta.apply(),
}
}

View file

@ -1,3 +1,15 @@
use crate::config::AppConfig;
use crate::core::{BatteryInfo, CpuCoreInfo, CpuGlobalInfo, SystemInfo, SystemLoad, SystemReport};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
str::FromStr,
thread,
time::Duration,
time::SystemTime,
};
// Try /sys/devices/platform paths for thermal zones as a last resort
// if temperature_celsius.is_none() {
// if let Ok(thermal_zones) = fs::read_dir("/sys/devices/virtual/thermal") {
@ -22,3 +34,22 @@
// }
// }
// }
pub fn get_cpu_model() -> anyhow::Result<String> {
let path = Path::new("/proc/cpuinfo");
let content = fs::read_to_string(path).map_err(|_| {
SysMonitorError::ReadError(format!("Cannot read contents of {}.", path.display()))
})?;
for line in content.lines() {
if line.starts_with("model name") {
if let Some(val) = line.split(':').nth(1) {
let cpu_model = val.trim().to_string();
return Ok(cpu_model);
}
}
}
Err(SysMonitorError::ParseError(
"Could not find CPU model name in /proc/cpuinfo.".to_string(),
))
}

View file

@ -50,7 +50,6 @@ impl System {
|| self.is_desktop()?;
self.rescan_load_average()?;
self.rescan_temperatures()?;
Ok(())
}