1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-08-01 19:37:45 +00:00

Compare commits

..

58 commits

Author SHA1 Message Date
3e235c089b
daemon: merge cpu deltas 2025-06-05 00:09:41 +03:00
0e8b40227b
config: rename rule condition field 2025-06-04 23:50:21 +03:00
2c154cd589
expr: strict eval 2025-06-04 23:45:37 +03:00
917ed77255
config: fix serde 2025-06-04 21:51:34 +03:00
b6dd9e78d4
package: make multicall 2025-06-04 21:51:34 +03:00
a25ae59bde
package: rename to watt 2025-06-04 21:51:34 +03:00
008e05b726
cpu: use only recent cpu log entries for calculating volatility 2025-06-04 21:51:34 +03:00
7503e235a3
daemon: add eval 2025-06-04 21:51:34 +03:00
c2325fa5ed
daemon: set charging 2025-06-04 21:51:34 +03:00
1283e5be14
delete core.rs 2025-06-04 21:51:34 +03:00
ee7ea6b86d
cpu: add cpu info scanning 2025-06-04 21:51:34 +03:00
303a84479c
system: cpu temperatures scanning 2025-06-04 21:51:34 +03:00
421d4aaacc
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-06-04 21:51:34 +03:00
fd3ae29dc5
cpu: cache /proc/stat 2025-06-04 21:51:34 +03:00
2812baa77b
system: check for chassis type 31 and move power saving check below
Co-Authored-By: flashrun24 <flashrun42@gmail.com>
2025-06-04 21:51:34 +03:00
a343e38d95
system: is_ac 2025-06-04 21:51:34 +03:00
004b879672
power_supply: add more stuff and store them 2025-06-04 21:51:34 +03:00
4763b54c97
cpu: add usage percent 2025-06-04 21:51:34 +03:00
571f172cc2
cpu: add global turbo querying 2025-06-04 21:51:34 +03:00
07ca582760
cpu: set_ep{p,b} actually sets the attributes now 2025-06-04 21:51:34 +03:00
99feb831a8
cpu: store EPP and EPB 2025-06-04 21:51:34 +03:00
961d1dfcd7
cpu: store governor and available governors 2025-06-04 21:51:34 +03:00
d87237165b
cpu: store frequency 2025-06-04 21:51:34 +03:00
230967832b
monitor: delete old code 2025-06-04 21:51:34 +03:00
a514f1ba7a
main: use yansi::whenever 2025-06-04 21:51:34 +03:00
058ef997c6
cpu: add TODO 2025-06-04 21:51:34 +03:00
1ab9aceced
cpu: cpu times 2025-06-04 21:51:34 +03:00
137f801d2b
cpu&power: add more attributes 2025-06-04 21:51:34 +03:00
543e5a052e
fs: fix read() typesig 2025-06-04 21:51:34 +03:00
91cef3b8b1
cpu&power: share fs impls 2025-06-04 21:51:34 +03:00
c062327457
daemon: delete some old code and create daemon scaffold 2025-06-04 21:51:34 +03:00
4fa59b7ed4
daemon: implement polling_interval 2025-06-04 21:51:34 +03:00
606cedb68a
daemon: wip new impl 2025-06-04 21:51:34 +03:00
0de8105432
config: better more enhanched expression 2025-06-04 21:51:34 +03:00
f3813230c5
power_supply&cpu: kolor 2025-06-04 21:51:34 +03:00
b6d4e09c7f
power_supply&cpu: somewhat improve error messages 2025-06-04 21:51:34 +03:00
ca4b1dbc92
main: move application to deltas, comment out broken modules for now 2025-06-04 21:51:34 +03:00
c073b640dc
config: fix schema, toml does not have top level lists 2025-06-04 21:51:34 +03:00
0d3a88be03
config: nuke old config and implement a new system 2025-06-04 21:51:34 +03:00
2995909544
power_supply: rename is_battery to get_type and don't compare the type 2025-06-04 21:51:34 +03:00
2704379b42
power_supply: add derives to PowerSupply 2025-06-04 21:51:34 +03:00
0ed0f18bb3
main: delete historical logging code 2025-06-04 21:51:34 +03:00
a14d88cee7
wip unsound broken malfunctioning changes to make it compile 2025-06-04 21:51:34 +03:00
004e8e2a9c
cpu: impl Display for Cpu 2025-06-04 21:51:34 +03:00
6ef4da9113
power_supply&cpu: use objects 2025-06-04 21:51:34 +03:00
6377480312
power_supply: don't ignore non-batteries 2025-06-04 21:51:34 +03:00
baef8af981
cli: remove governor_persist 2025-06-04 21:51:34 +03:00
d0932ae78c
battery: clean up, rename to power_supply 2025-06-04 21:51:34 +03:00
87085f913b
cpu: clean up, clean main too 2025-06-04 21:51:34 +03:00
raf
da07011b02
Merge pull request #35 from spikespaz-contrib/u/jacob/flake-output-overlays
flake:  add superfreq and default pkgs to overlay
2025-05-31 23:13:51 +03:00
Jacob Birkett
0f3d5d81dd flake: devShells: use same pkgs as for packages output 2025-05-31 13:11:25 -07:00
raf
71cd443ba7
Merge pull request #34 from spikespaz-contrib/u/jacob/flake-formatter-alejandra
flake: formatter: set to alejandra
2025-05-31 22:54:03 +03:00
Jacob Birkett
3caaa22f3e flake: packages: inherit from default overlay 2025-05-31 12:46:22 -07:00
Jacob Birkett
08c51b6296 flake: overlays: add superfreq and default 2025-05-31 12:46:22 -07:00
Jacob Birkett
6b1af5cbab flake: pkgsForEach: replace legacyPackages with manual import 2025-05-31 12:46:22 -07:00
Jacob Birkett
7b375439bb flake: formatter: set to alejandra 2025-05-31 12:19:50 -07:00
raf
017793288a
Merge pull request #32 from NotAShelf/systems-input
flake: add aarch64-linux to systems
2025-05-21 15:53:08 +03:00
Bloxx12
55e04ea09e
flake: add aarch64-linux to systems 2025-05-21 14:36:24 +02:00
13 changed files with 620 additions and 707 deletions

116
Cargo.lock generated
View file

@ -182,27 +182,6 @@ 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"
@ -232,17 +211,6 @@ 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"
@ -301,12 +269,10 @@ 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]]
@ -320,37 +286,12 @@ 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"
@ -391,12 +332,6 @@ 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"
@ -430,17 +365,6 @@ 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"
@ -505,26 +429,6 @@ 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"
@ -622,10 +526,22 @@ 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"
name = "watt"
version = "0.4.0"
dependencies = [
"anyhow",
"clap",
"clap-verbosity-flag",
"ctrlc",
"derive_more",
"env_logger",
"log",
"num_cpus",
"serde",
"thiserror",
"toml",
"yansi",
]
[[package]]
name = "windows-sys"

View file

@ -1,23 +1,21 @@
[package]
name = "superfreq"
name = "watt"
description = "Modern CPU frequency and power management utility for Linux"
version = "0.3.2"
version = "0.4.0"
edition = "2024"
authors = ["NotAShelf <raf@notashelf.dev>"]
authors = ["NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>"]
rust-version = "1.85"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
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 = { version = "4.0", features = ["derive", "env"] }
clap-verbosity-flag = "3.0.2"
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }
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"
yansi = { version = "1.0.1", features = ["detect-env", "detect-tty"] }

51
build.rs Normal file
View file

@ -0,0 +1,51 @@
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,2 +1,3 @@
[[rule]]
priority = 0
if = { value = "%cpu-usage", is-more-than = 0.7 }

View file

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

View file

@ -155,92 +155,151 @@ impl PowerDelta {
}
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(untagged, rename_all = "kebab-case")]
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)]
pub enum Expression {
#[serde(rename = "%cpu-usage")]
#[serde(with = "expression::cpu_usage")]
CpuUsage,
#[serde(rename = "$cpu-usage-volatility")]
#[serde(with = "expression::cpu_usage_volatility")]
CpuUsageVolatility,
#[serde(rename = "$cpu-temperature")]
#[serde(with = "expression::cpu_temperature")]
CpuTemperature,
#[serde(rename = "$cpu-temperature-volatility")]
#[serde(with = "expression::cpu_temperature_volatility")]
CpuTemperatureVolatility,
#[serde(rename = "$cpu-idle-seconds")]
#[serde(with = "expression::cpu_idle_seconds")]
CpuIdleSeconds,
#[serde(rename = "%power-supply-charge")]
#[serde(with = "expression::power_supply_charge")]
PowerSupplyCharge,
#[serde(rename = "%power-supply-discharge-rate")]
#[serde(with = "expression::power_supply_discharge_rate")]
PowerSupplyDischargeRate,
#[serde(rename = "?charging")]
Charging,
#[serde(rename = "?on-battery")]
OnBattery,
#[serde(with = "expression::discharging")]
Discharging,
#[serde(rename = "#false")]
False,
#[default]
#[serde(rename = "#true")]
True,
Boolean(bool),
Number(f64),
Plus {
value: Box<Expression>,
plus: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "plus")]
b: Box<Expression>,
},
Minus {
value: Box<Expression>,
minus: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "minus")]
b: Box<Expression>,
},
Multiply {
value: Box<Expression>,
multiply: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "multiply")]
b: Box<Expression>,
},
Power {
value: Box<Expression>,
power: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "power")]
b: Box<Expression>,
},
Divide {
value: Box<Expression>,
divide: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "divide")]
b: Box<Expression>,
},
LessThan {
value: Box<Expression>,
is_less_than: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-less-than")]
b: Box<Expression>,
},
MoreThan {
value: Box<Expression>,
is_more_than: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-more-than")]
b: Box<Expression>,
},
Equal {
value: Box<Expression>,
is_equal: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "is-equal")]
b: Box<Expression>,
leeway: Box<Expression>,
},
And {
value: Box<Expression>,
and: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "and")]
b: Box<Expression>,
},
All {
all: Vec<Expression>,
},
Or {
value: Box<Expression>,
or: Box<Expression>,
#[serde(rename = "value")]
a: Box<Expression>,
#[serde(rename = "or")]
b: Box<Expression>,
},
Any {
any: Vec<Expression>,
@ -251,25 +310,164 @@ 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 {
priority: u8,
pub priority: u8,
#[serde(default, rename = "if", skip_serializing_if = "is_default")]
if_: Expression,
pub condition: Expression,
#[serde(default, skip_serializing_if = "is_default")]
cpu: CpuDelta,
pub cpu: CpuDelta,
#[serde(default, skip_serializing_if = "is_default")]
power: PowerDelta,
pub power: PowerDelta,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)]
#[serde(default, rename_all = "kebab-case")]
pub struct DaemonConfig {
#[serde(rename = "rule")]
rules: Vec<Rule>,
pub rules: Vec<Rule>,
}
impl DaemonConfig {
@ -278,7 +476,8 @@ impl DaemonConfig {
format!("failed to read config from '{path}'", path = path.display())
})?;
let config: Self = toml::from_str(&contents).context("failed to parse config file")?;
let mut config: Self = toml::from_str(&contents)
.with_context(|| format!("failed to parse file at '{path}'", path = path.display(),))?;
{
let mut priorities = Vec::with_capacity(config.rules.len());
@ -292,6 +491,10 @@ impl DaemonConfig {
}
}
config.rules.sort_by_key(|rule| rule.priority);
log::debug!("loaded config: {config:#?}");
Ok(config)
}
}

View file

@ -1,51 +0,0 @@
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,13 +1,14 @@
use anyhow::{Context, bail};
use yansi::Paint as _;
use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString};
use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, 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)]
@ -63,6 +64,7 @@ pub struct Cpu {
pub epb: Option<String>,
pub stat: CpuStat,
pub info: Option<Rc<HashMap<String, String>>>,
pub temperature: Option<f64>,
}
@ -104,6 +106,7 @@ impl Cpu {
softirq: 0,
steal: 0,
},
info: None,
temperature: None,
};
@ -171,6 +174,7 @@ impl Cpu {
}
self.rescan_stat(cache)?;
self.rescan_info(cache)?;
Ok(())
}
@ -345,6 +349,57 @@ 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,16 +1,18 @@
use std::{
collections::VecDeque,
ops,
cell::LazyCell,
collections::{HashMap, VecDeque},
ops::Deref,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant},
};
use anyhow::Context;
use crate::config;
use crate::{config, system};
/// Calculate the idle time multiplier based on system idle time.
///
@ -42,8 +44,8 @@ struct Daemon {
/// The last computed polling interval.
last_polling_interval: Option<Duration>,
/// Whether if we are charging right now.
charging: bool,
/// The system state.
system: system::System,
/// CPU usage and temperature log.
cpu_log: VecDeque<CpuLog>,
@ -52,6 +54,56 @@ 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,
@ -63,8 +115,6 @@ struct CpuLog {
}
struct CpuVolatility {
at: ops::Range<Instant>,
usage: f64,
temperature: f64,
@ -72,6 +122,17 @@ 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;
}
@ -91,8 +152,6 @@ 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,
})
@ -134,6 +193,13 @@ 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.
@ -181,7 +247,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.charging {
if self.discharging() {
match self.power_supply_discharge_rate() {
Some(discharge_rate) => {
if discharge_rate > 0.2 {
@ -243,6 +309,8 @@ 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));
@ -254,7 +322,87 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
})
.context("failed to set Ctrl-C handler")?;
while !cancelled.load(Ordering::SeqCst) {}
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);
}
}
log::info!("exiting...");

View file

@ -1,426 +0,0 @@
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,30 +21,52 @@ 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 {
/// Display information.
Info,
/// Watt daemon.
Watt {
#[command(flatten)]
verbosity: clap_verbosity_flag::Verbosity,
/// Start the daemon.
Start {
/// The daemon config path.
#[arg(long, env = "WATT_CONFIG")]
config: PathBuf,
},
/// Modify CPU attributes.
CpuSet(config::CpuDelta),
/// 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),
}
#[derive(clap::Parser, Debug)]
enum PowerCommand {
/// Modify power supply attributes.
PowerSet(config::PowerDelta),
Set(config::PowerDelta),
}
fn real_main() -> anyhow::Result<()> {
@ -52,24 +74,33 @@ 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(cli.verbosity.log_level_filter())
.filter_level(verbosity.log_level_filter())
.format_timestamp(None)
.format_module_path(false)
.init();
match cli.command {
Command::Info => todo!(),
Command::Start { config } => {
let config = config::DaemonConfig::load_from(&config)
.context("failed to load daemon config file")?;
Command::Watt { config, .. } => {
let config =
config::DaemonConfig::load_from(&config).context("failed to load daemon config")?;
daemon::run(config)
}
Command::CpuSet(delta) => delta.apply(),
Command::PowerSet(delta) => delta.apply(),
Command::Cpu {
command: CpuCommand::Set(delta),
..
} => delta.apply(),
Command::Power {
command: PowerCommand::Set(delta),
..
} => delta.apply(),
}
}

View file

@ -1,15 +1,3 @@
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") {
@ -34,22 +22,3 @@ use std::{
// }
// }
// }
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,6 +50,7 @@ impl System {
|| self.is_desktop()?;
self.rescan_load_average()?;
self.rescan_temperatures()?;
Ok(())
}