1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

treewide: toml and rust formatter changes

This commit is contained in:
RGBCube 2025-06-12 20:19:40 +03:00
parent fb5ef3d3d2
commit e9e1df90e6
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
12 changed files with 2456 additions and 2182 deletions

30
.rustfmt.toml Normal file
View file

@ -0,0 +1,30 @@
# Taken from https://github.com/cull-os/carcass.
# Modified to have 2 space indents and 80 line width.
# float_literal_trailing_zero = "Always" # TODO: Warning for some reason?
condense_wildcard_suffixes = true
doc_comment_code_block_width = 80
edition = "2024" # Keep in sync with Cargo.toml.
enum_discrim_align_threshold = 60
force_explicit_abi = false
force_multiline_blocks = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_strings = true
group_imports = "StdExternalCrate"
hex_literal_case = "Upper"
imports_granularity = "Crate"
imports_layout = "Vertical"
inline_attribute_width = 60
match_block_trailing_comma = true
max_width = 80
newline_style = "Unix"
normalize_comments = true
normalize_doc_attributes = true
overflow_delimited_expr = true
struct_field_align_threshold = 60
tab_spaces = 2
unstable_features = true
use_field_init_shorthand = true
use_try_shorthand = true
wrap_comments = true

15
.taplo.toml Normal file
View file

@ -0,0 +1,15 @@
# Taken from https://github.com/cull-os/carcass.
[formatting]
align_entries = true
column_width = 100
compact_arrays = false
reorder_inline_tables = true
reorder_keys = true
[[rule]]
include = [ "**/Cargo.toml" ]
keys = [ "package" ]
[rule.formatting]
reorder_keys = false

View file

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

View file

@ -1,6 +1,8 @@
use std::env; use std::{
use std::fs; env,
use std::path::PathBuf; fs,
path::PathBuf,
};
const MULTICALL_NAMES: &[&str] = &["cpu", "power"]; const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
@ -43,9 +45,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if errored { if errored {
println!( println!(
"cargo:warning=this often happens because the target binary isn't built yet, try running `cargo build` again" "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"
); );
println!("cargo:warning=keep in mind that this is for development purposes only");
} }
Ok(()) Ok(())

View file

@ -5,107 +5,101 @@
# Emergency thermal protection (highest priority). # Emergency thermal protection (highest priority).
[[rule]] [[rule]]
priority = 100
if = { value = "$cpu-temperature", is-more-than = 85.0 }
cpu.governor = "powersave"
cpu.energy-performance-preference = "power" cpu.energy-performance-preference = "power"
cpu.frequency-mhz-maximum = 2000 cpu.frequency-mhz-maximum = 2000
cpu.governor = "powersave"
cpu.turbo = false cpu.turbo = false
if = { value = "$cpu-temperature", is-more-than = 85.0 }
priority = 100
# Critical battery preservation. # Critical battery preservation.
[[rule]] [[rule]]
priority = 90
if.all = [
"?discharging",
{ value = "%power-supply-charge", is-less-than = 0.3 },
]
cpu.governor = "powersave"
cpu.energy-performance-preference = "power" cpu.energy-performance-preference = "power"
cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold. cpu.frequency-mhz-maximum = 800 # More aggressive below critical threshold.
cpu.governor = "powersave"
cpu.turbo = false cpu.turbo = false
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ]
power.platform-profile = "low-power" power.platform-profile = "low-power"
priority = 90
# High performance mode for sustained high load. # High performance mode for sustained high load.
[[rule]] [[rule]]
priority = 80 cpu.energy-performance-preference = "performance"
cpu.governor = "performance"
cpu.turbo = true
if.all = [ if.all = [
{ value = "%cpu-usage", is-more-than = 0.8 }, { value = "%cpu-usage", is-more-than = 0.8 },
{ value = "$cpu-idle-seconds", is-less-than = 30.0 }, { value = "$cpu-idle-seconds", is-less-than = 30.0 },
{ value = "$cpu-temperature", is-less-than = 75.0 }, { value = "$cpu-temperature", is-less-than = 75.0 },
] ]
cpu.governor = "performance" priority = 80
cpu.energy-performance-preference = "performance"
cpu.turbo = true
# Performance mode when not discharging. # Performance mode when not discharging.
[[rule]] [[rule]]
priority = 70 cpu.energy-performance-bias = "balance_performance"
cpu.energy-performance-preference = "performance"
cpu.governor = "performance"
cpu.turbo = true
if.all = [ if.all = [
{ not = "?discharging" }, { not = "?discharging" },
{ value = "%cpu-usage", is-more-than = 0.1 }, { value = "%cpu-usage", is-more-than = 0.1 },
{ value = "$cpu-temperature", is-less-than = 80.0 }, { value = "$cpu-temperature", is-less-than = 80.0 },
] ]
cpu.governor = "performance" priority = 70
cpu.energy-performance-preference = "performance"
cpu.energy-performance-bias = "balance_performance"
cpu.turbo = true
# Moderate performance for medium load. # Moderate performance for medium load.
[[rule]] [[rule]]
priority = 60 cpu.energy-performance-preference = "balance_performance"
cpu.governor = "schedutil"
if.all = [ if.all = [
{ value = "%cpu-usage", is-more-than = 0.4 }, { value = "%cpu-usage", is-more-than = 0.4 },
{ value = "%cpu-usage", is-less-than = 0.8 }, { value = "%cpu-usage", is-less-than = 0.8 },
] ]
cpu.governor = "schedutil" priority = 60
cpu.energy-performance-preference = "balance_performance"
# Power saving during low activity. # Power saving during low activity.
[[rule]] [[rule]]
priority = 50 cpu.energy-performance-preference = "power"
cpu.governor = "powersave"
cpu.turbo = false
if.all = [ if.all = [
{ value = "%cpu-usage", is-less-than = 0.2 }, { value = "%cpu-usage", is-less-than = 0.2 },
{ value = "$cpu-idle-seconds", is-more-than = 60.0 }, { value = "$cpu-idle-seconds", is-more-than = 60.0 },
] ]
cpu.governor = "powersave" priority = 50
cpu.energy-performance-preference = "power"
cpu.turbo = false
# Extended idle power optimization. # Extended idle power optimization.
[[rule]] [[rule]]
priority = 40
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
cpu.governor = "powersave"
cpu.energy-performance-preference = "power" cpu.energy-performance-preference = "power"
cpu.frequency-mhz-maximum = 1600 cpu.frequency-mhz-maximum = 1600
cpu.governor = "powersave"
cpu.turbo = false cpu.turbo = false
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
priority = 40
# Battery conservation when discharging. # Battery conservation when discharging.
[[rule]] [[rule]]
priority = 30
if.all = [
"?discharging",
{ value = "%power-supply-charge", is-less-than = 0.5 },
]
cpu.governor = "powersave"
cpu.energy-performance-preference = "power" cpu.energy-performance-preference = "power"
cpu.frequency-mhz-maximum = 2000 cpu.frequency-mhz-maximum = 2000
cpu.governor = "powersave"
cpu.turbo = false cpu.turbo = false
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ]
power.platform-profile = "low-power" power.platform-profile = "low-power"
priority = 30
# General battery mode. # General battery mode.
[[rule]] [[rule]]
priority = 20
if = "?discharging"
cpu.governor = "powersave"
cpu.energy-performance-preference = "power"
cpu.energy-performance-bias = "balance_power" cpu.energy-performance-bias = "balance_power"
cpu.energy-performance-preference = "power"
cpu.frequency-mhz-maximum = 1800 cpu.frequency-mhz-maximum = 1800
cpu.frequency-mhz-minimum = 200 cpu.frequency-mhz-minimum = 200
cpu.governor = "powersave"
cpu.turbo = false cpu.turbo = false
if = "?discharging"
priority = 20
# Balanced performance for general use. Default fallback rule. # Balanced performance for general use. Default fallback rule.
[[rule]] [[rule]]
priority = 0
cpu.governor = "schedutil"
cpu.energy-performance-preference = "balance_performance" cpu.energy-performance-preference = "balance_performance"
cpu.governor = "schedutil"
priority = 0

View file

@ -1,18 +1,33 @@
use std::{fs, path::Path}; use std::{
fs,
path::Path,
};
use anyhow::{Context, bail}; use anyhow::{
use serde::{Deserialize, Serialize}; Context,
bail,
};
use serde::{
Deserialize,
Serialize,
};
use crate::{cpu, power_supply}; use crate::{
cpu,
power_supply,
};
fn is_default<T: Default + PartialEq>(value: &T) -> bool { fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default() *value == T::default()
} }
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[derive(
Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq,
)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct CpuDelta { pub struct CpuDelta {
/// The CPUs to apply the changes to. When unspecified, will be applied to all CPUs. /// The CPUs to apply the changes to. When unspecified, will be applied to
/// all CPUs.
#[arg(short = 'c', long = "for")] #[arg(short = 'c', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")] #[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<u32>>, pub for_: Option<Vec<u32>>,
@ -20,17 +35,20 @@ pub struct CpuDelta {
/// Set the CPU governor. /// Set the CPU governor.
#[arg(short = 'g', long)] #[arg(short = 'g', long)]
#[serde(skip_serializing_if = "is_default")] #[serde(skip_serializing_if = "is_default")]
pub governor: Option<String>, // TODO: Validate with clap for available governors. pub governor: Option<String>, /* TODO: Validate with clap for available
* governors. */
/// Set CPU Energy Performance Preference (EPP). Short form: --epp. /// Set CPU Energy Performance Preference (EPP). Short form: --epp.
#[arg(short = 'p', long, alias = "epp")] #[arg(short = 'p', long, alias = "epp")]
#[serde(skip_serializing_if = "is_default")] #[serde(skip_serializing_if = "is_default")]
pub energy_performance_preference: Option<String>, // TODO: Validate with clap for available governors. pub energy_performance_preference: Option<String>, /* TODO: Validate with
* clap for available
* governors. */
/// Set CPU Energy Performance Bias (EPB). Short form: --epb. /// Set CPU Energy Performance Bias (EPB). Short form: --epb.
#[arg(short = 'b', long, alias = "epb")] #[arg(short = 'b', long, alias = "epb")]
#[serde(skip_serializing_if = "is_default")] #[serde(skip_serializing_if = "is_default")]
pub energy_performance_bias: Option<String>, // TODO: Validate with clap for available governors. pub energy_performance_bias: Option<String>, /* TODO: Validate with clap for available governors. */
/// Set minimum CPU frequency in MHz. Short form: --freq-min. /// Set minimum CPU frequency in MHz. Short form: --freq-min.
#[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))] #[arg(short = 'f', long, alias = "freq-min", value_parser = clap::value_parser!(u64).range(1..=10_000))]
@ -60,8 +78,11 @@ impl CpuDelta {
} }
cpus cpus
} },
None => cpu::Cpu::all().context("failed to get all CPUs and their information")?, None => {
cpu::Cpu::all()
.context("failed to get all CPUs and their information")?
},
}; };
for cpu in &mut cpus { for cpu in &mut cpus {
@ -94,15 +115,19 @@ impl CpuDelta {
} }
} }
#[derive(Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq)] #[derive(
Serialize, Deserialize, clap::Parser, Default, Debug, Clone, PartialEq, Eq,
)]
#[serde(deny_unknown_fields, default, rename_all = "kebab-case")] #[serde(deny_unknown_fields, default, rename_all = "kebab-case")]
pub struct PowerDelta { pub struct PowerDelta {
/// The power supplies to apply the changes to. When unspecified, will be applied to all power supplies. /// The power supplies to apply the changes to. When unspecified, will be
/// applied to all power supplies.
#[arg(short = 'p', long = "for")] #[arg(short = 'p', long = "for")]
#[serde(rename = "for", skip_serializing_if = "is_default")] #[serde(rename = "for", skip_serializing_if = "is_default")]
pub for_: Option<Vec<String>>, pub for_: Option<Vec<String>>,
/// Set the percentage that the power supply has to drop under for charging to start. Short form: --charge-start. /// Set the percentage that the power supply has to drop under for charging
/// to start. Short form: --charge-start.
#[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))] #[arg(short = 'c', long, alias = "charge-start", value_parser = clap::value_parser!(u8).range(0..=100))]
#[serde(skip_serializing_if = "is_default")] #[serde(skip_serializing_if = "is_default")]
pub charge_threshold_start: Option<u8>, pub charge_threshold_start: Option<u8>,
@ -125,21 +150,25 @@ impl PowerDelta {
let mut power_supplies = Vec::with_capacity(names.len()); let mut power_supplies = Vec::with_capacity(names.len());
for name in names { for name in names {
power_supplies.push(power_supply::PowerSupply::from_name(name.clone())?); power_supplies
.push(power_supply::PowerSupply::from_name(name.clone())?);
} }
power_supplies power_supplies
} },
None => power_supply::PowerSupply::all()? None => {
power_supply::PowerSupply::all()?
.into_iter() .into_iter()
.filter(|power_supply| power_supply.threshold_config.is_some()) .filter(|power_supply| power_supply.threshold_config.is_some())
.collect(), .collect()
},
}; };
for power_supply in &mut power_supplies { for power_supply in &mut power_supplies {
if let Some(threshold_start) = self.charge_threshold_start { if let Some(threshold_start) = self.charge_threshold_start {
power_supply.set_charge_threshold_start(threshold_start as f64 / 100.0)?; power_supply
.set_charge_threshold_start(threshold_start as f64 / 100.0)?;
} }
if let Some(threshold_end) = self.charge_threshold_end { if let Some(threshold_end) = self.charge_threshold_end {
@ -158,7 +187,9 @@ impl PowerDelta {
macro_rules! named { macro_rules! named {
($variant:ident => $value:literal) => { ($variant:ident => $value:literal) => {
pub mod $variant { pub mod $variant {
pub fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> { pub fn serialize<S: serde::Serializer>(
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str($value) serializer.serialize_str($value)
} }
@ -170,13 +201,22 @@ macro_rules! named {
impl<'de> serde::de::Visitor<'de> for Visitor { impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = (); type Value = ();
fn expecting(&self, writer: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(
&self,
writer: &mut std::fmt::Formatter,
) -> std::fmt::Result {
writer.write_str(concat!("\"", $value, "\"")) writer.write_str(concat!("\"", $value, "\""))
} }
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> { fn visit_str<E: serde::de::Error>(
self,
value: &str,
) -> Result<Self::Value, E> {
if value != $value { if value != $value {
return Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)); return Err(E::invalid_value(
serde::de::Unexpected::Str(value),
&self,
));
} }
Ok(()) Ok(())
@ -374,18 +414,22 @@ impl Expression {
// We also want to be strict, instead of lazy in binary operations, because // We also want to be strict, instead of lazy in binary operations, because
// we want to catch type errors immediately. // we want to catch type errors immediately.
// //
// FIXME: We currently cannot catch errors that will happen when propagating None. // FIXME: We currently cannot catch errors that will happen when propagating
// You can have a type error go uncaught on first startup by using $cpu-usage-volatility // None. You can have a type error go uncaught on first startup by using
// incorrectly, for example. // $cpu-usage-volatility incorrectly, for example.
Ok(Some(match self { Ok(Some(match self {
CpuUsage => Number(state.cpu_usage), CpuUsage => Number(state.cpu_usage),
CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)), CpuUsageVolatility => Number(try_ok!(state.cpu_usage_volatility)),
CpuTemperature => Number(state.cpu_temperature), CpuTemperature => Number(state.cpu_temperature),
CpuTemperatureVolatility => Number(try_ok!(state.cpu_temperature_volatility)), CpuTemperatureVolatility => {
Number(try_ok!(state.cpu_temperature_volatility))
},
CpuIdleSeconds => Number(state.cpu_idle_seconds), CpuIdleSeconds => Number(state.cpu_idle_seconds),
PowerSupplyCharge => Number(state.cpu_idle_seconds), PowerSupplyCharge => Number(state.cpu_idle_seconds),
PowerSupplyDischargeRate => Number(try_ok!(state.power_supply_discharge_rate)), PowerSupplyDischargeRate => {
Number(try_ok!(state.power_supply_discharge_rate))
},
Discharging => Boolean(state.discharging), Discharging => Boolean(state.discharging),
@ -393,12 +437,20 @@ impl Expression {
Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?), Plus { a, b } => Number(eval!(a).as_number()? + eval!(b).as_number()?),
Minus { 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()?), Multiply { a, b } => {
Power { a, b } => Number(eval!(a).as_number()?.powf(eval!(b).as_number()?)), 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()?), Divide { a, b } => Number(eval!(a).as_number()? / eval!(b).as_number()?),
LessThan { a, b } => Boolean(eval!(a).as_number()? < eval!(b).as_number()?), LessThan { a, b } => {
MoreThan { a, b } => Boolean(eval!(a).as_number()? > eval!(b).as_number()?), 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 } => { Equal { a, b, leeway } => {
let a = eval!(a).as_number()?; let a = eval!(a).as_number()?;
let b = eval!(b).as_number()?; let b = eval!(b).as_number()?;
@ -408,14 +460,14 @@ impl Expression {
let maximum = a + leeway; let maximum = a + leeway;
Boolean(minimum < b && b < maximum) Boolean(minimum < b && b < maximum)
} },
And { a, b } => { And { a, b } => {
let a = eval!(a).as_boolean()?; let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?; let b = eval!(b).as_boolean()?;
Boolean(a && b) Boolean(a && b)
} },
All { all } => { All { all } => {
let mut result = true; let mut result = true;
@ -426,13 +478,13 @@ impl Expression {
} }
Boolean(result) Boolean(result)
} },
Or { a, b } => { Or { a, b } => {
let a = eval!(a).as_boolean()?; let a = eval!(a).as_boolean()?;
let b = eval!(b).as_boolean()?; let b = eval!(b).as_boolean()?;
Boolean(a || b) Boolean(a || b)
} },
Any { any } => { Any { any } => {
let mut result = false; let mut result = false;
@ -443,7 +495,7 @@ impl Expression {
} }
Boolean(result) Boolean(result)
} },
Not { not } => Boolean(!eval!(not).as_boolean()?), Not { not } => Boolean(!eval!(not).as_boolean()?),
})) }))
} }
@ -511,7 +563,9 @@ impl DaemonConfig {
// This is just for debug traces. // This is just for debug traces.
if log::max_level() >= log::LevelFilter::Debug { if log::max_level() >= log::LevelFilter::Debug {
if config.rules.is_sorted_by_key(|rule| rule.priority) { if config.rules.is_sorted_by_key(|rule| rule.priority) {
log::debug!("config rules are sorted by increasing priority, not doing anything"); log::debug!(
"config rules are sorted by increasing priority, not doing anything"
);
} else { } else {
log::debug!("config rules aren't sorted by priority, sorting"); log::debug!("config rules aren't sorted by priority, sorting");
} }

View file

@ -1,7 +1,17 @@
use anyhow::{Context, bail}; use std::{
use yansi::Paint as _; cell::OnceCell,
collections::HashMap,
fmt,
mem,
rc::Rc,
string::ToString,
};
use std::{cell::OnceCell, collections::HashMap, fmt, mem, rc::Rc, string::ToString}; use anyhow::{
Context,
bail,
};
use yansi::Paint as _;
use crate::fs; use crate::fs;
@ -126,7 +136,8 @@ impl Cpu {
.context("failed to read CPU entries")? .context("failed to read CPU entries")?
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{ {
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; let entry =
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_file_name = entry.file_name(); let entry_file_name = entry.file_name();
@ -164,7 +175,8 @@ impl Cpu {
bail!("{self} does not exist"); bail!("{self} does not exist");
} }
self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq =
fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
if self.has_cpufreq { if self.has_cpufreq {
self.rescan_governor()?; self.rescan_governor()?;
@ -184,7 +196,8 @@ impl Cpu {
self.available_governors = 'available_governors: { self.available_governors = 'available_governors: {
let Some(content) = fs::read(format!( let Some(content) = fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_available_governors" "/sys/devices/system/cpu/cpu{number}/cpufreq/\
scaling_available_governors"
)) ))
.with_context(|| format!("failed to read {self} available governors"))? .with_context(|| format!("failed to read {self} available governors"))?
else { else {
@ -239,8 +252,11 @@ impl Cpu {
self.available_epps = 'available_epps: { self.available_epps = 'available_epps: {
let Some(content) = fs::read(format!( let Some(content) = fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_available_preferences" "/sys/devices/system/cpu/cpu{number}/cpufreq/\
)).with_context(|| format!("failed to read {self} available EPPs"))? else { energy_performance_available_preferences"
))
.with_context(|| format!("failed to read {self} available EPPs"))?
else {
break 'available_epps Vec::new(); break 'available_epps Vec::new();
}; };
@ -252,7 +268,8 @@ impl Cpu {
self.epp = Some( self.epp = Some(
fs::read(format!( fs::read(format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference" "/sys/devices/system/cpu/cpu{number}/cpufreq/\
energy_performance_preference"
)) ))
.with_context(|| format!("failed to read {self} EPP"))? .with_context(|| format!("failed to read {self} EPP"))?
.with_context(|| format!("failed to find {self} EPP"))?, .with_context(|| format!("failed to find {self} EPP"))?,
@ -338,7 +355,7 @@ impl Cpu {
.unwrap(); .unwrap();
cache.stat.get().unwrap() cache.stat.get().unwrap()
} },
}; };
self.stat = stat self.stat = stat
@ -392,7 +409,7 @@ impl Cpu {
cache.info.set(info).unwrap(); cache.info.set(info).unwrap();
cache.info.get().unwrap() cache.info.get().unwrap()
} },
}; };
self.info = info.get(&self.number).cloned(); self.info = info.get(&self.number).cloned();
@ -412,7 +429,8 @@ impl Cpu {
.any(|avail_governor| avail_governor == governor) .any(|avail_governor| avail_governor == governor)
{ {
bail!( bail!(
"governor '{governor}' is not available for {self}. available governors: {governors}", "governor '{governor}' is not available for {self}. available \
governors: {governors}",
governors = governors.join(", "), governors = governors.join(", "),
); );
} }
@ -423,7 +441,8 @@ impl Cpu {
) )
.with_context(|| { .with_context(|| {
format!( format!(
"this probably means that {self} doesn't exist or doesn't support changing governors" "this probably means that {self} doesn't exist or doesn't support \
changing governors"
) )
})?; })?;
@ -441,17 +460,24 @@ impl Cpu {
if !epps.iter().any(|avail_epp| avail_epp == epp) { if !epps.iter().any(|avail_epp| avail_epp == epp) {
bail!( bail!(
"EPP value '{epp}' is not available for {self}. available EPP values: {epps}", "EPP value '{epp}' is not available for {self}. available EPP values: \
{epps}",
epps = epps.join(", "), epps = epps.join(", "),
); );
} }
fs::write( fs::write(
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_preference"), format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
energy_performance_preference"
),
epp, epp,
) )
.with_context(|| { .with_context(|| {
format!("this probably means that {self} doesn't exist or doesn't support changing EPP") format!(
"this probably means that {self} doesn't exist or doesn't support \
changing EPP"
)
})?; })?;
self.epp = Some(epp.to_owned()); self.epp = Some(epp.to_owned());
@ -468,17 +494,23 @@ impl Cpu {
if !epbs.iter().any(|avail_epb| avail_epb == epb) { if !epbs.iter().any(|avail_epb| avail_epb == epb) {
bail!( bail!(
"EPB value '{epb}' is not available for {self}. available EPB values: {valid}", "EPB value '{epb}' is not available for {self}. available EPB values: \
{valid}",
valid = epbs.join(", "), valid = epbs.join(", "),
); );
} }
fs::write( fs::write(
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"), format!(
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"
),
epb, epb,
) )
.with_context(|| { .with_context(|| {
format!("this probably means that {self} doesn't exist or doesn't support changing EPB") format!(
"this probably means that {self} doesn't exist or doesn't support \
changing EPB"
)
})?; })?;
self.epb = Some(epb.to_owned()); self.epb = Some(epb.to_owned());
@ -486,7 +518,10 @@ impl Cpu {
Ok(()) Ok(())
} }
pub fn set_frequency_mhz_minimum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { pub fn set_frequency_mhz_minimum(
&mut self,
frequency_mhz: u64,
) -> anyhow::Result<()> {
let Self { number, .. } = *self; let Self { number, .. } = *self;
self.validate_frequency_mhz_minimum(frequency_mhz)?; self.validate_frequency_mhz_minimum(frequency_mhz)?;
@ -500,7 +535,10 @@ impl Cpu {
&frequency_khz, &frequency_khz,
) )
.with_context(|| { .with_context(|| {
format!("this probably means that {self} doesn't exist or doesn't support changing minimum frequency") format!(
"this probably means that {self} doesn't exist or doesn't support \
changing minimum frequency"
)
})?; })?;
self.frequency_mhz_minimum = Some(frequency_mhz); self.frequency_mhz_minimum = Some(frequency_mhz);
@ -508,7 +546,10 @@ impl Cpu {
Ok(()) Ok(())
} }
fn validate_frequency_mhz_minimum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { fn validate_frequency_mhz_minimum(
&self,
new_frequency_mhz: u64,
) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = self;
let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!( let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!(
@ -522,7 +563,8 @@ impl Cpu {
if new_frequency_mhz * 1000 < minimum_frequency_khz { if new_frequency_mhz * 1000 < minimum_frequency_khz {
bail!( bail!(
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than the minimum frequency ({} MHz) for {self}", "new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than \
the minimum frequency ({} MHz) for {self}",
minimum_frequency_khz / 1000, minimum_frequency_khz / 1000,
); );
} }
@ -530,7 +572,10 @@ impl Cpu {
Ok(()) Ok(())
} }
pub fn set_frequency_mhz_maximum(&mut self, frequency_mhz: u64) -> anyhow::Result<()> { pub fn set_frequency_mhz_maximum(
&mut self,
frequency_mhz: u64,
) -> anyhow::Result<()> {
let Self { number, .. } = *self; let Self { number, .. } = *self;
self.validate_frequency_mhz_maximum(frequency_mhz)?; self.validate_frequency_mhz_maximum(frequency_mhz)?;
@ -544,7 +589,10 @@ impl Cpu {
&frequency_khz, &frequency_khz,
) )
.with_context(|| { .with_context(|| {
format!("this probably means that {self} doesn't exist or doesn't support changing maximum frequency") format!(
"this probably means that {self} doesn't exist or doesn't support \
changing maximum frequency"
)
})?; })?;
self.frequency_mhz_maximum = Some(frequency_mhz); self.frequency_mhz_maximum = Some(frequency_mhz);
@ -552,7 +600,10 @@ impl Cpu {
Ok(()) Ok(())
} }
fn validate_frequency_mhz_maximum(&self, new_frequency_mhz: u64) -> anyhow::Result<()> { fn validate_frequency_mhz_maximum(
&self,
new_frequency_mhz: u64,
) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = self;
let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!( let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
@ -566,7 +617,8 @@ impl Cpu {
if new_frequency_mhz * 1000 > maximum_frequency_khz { if new_frequency_mhz * 1000 > maximum_frequency_khz {
bail!( bail!(
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher than the maximum frequency ({} MHz) for {self}", "new maximum frequency ({new_frequency_mhz} MHz) cannot be higher \
than the maximum frequency ({} MHz) for {self}",
maximum_frequency_khz / 1000, maximum_frequency_khz / 1000,
); );
} }
@ -587,10 +639,12 @@ impl Cpu {
// AMD specific paths // AMD specific paths
let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost"; let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
let msr_boost_path = "/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost"; let msr_boost_path =
"/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
// Path priority (from most to least specific) // Path priority (from most to least specific)
let intel_boost_path_negated = "/sys/devices/system/cpu/intel_pstate/no_turbo"; let intel_boost_path_negated =
"/sys/devices/system/cpu/intel_pstate/no_turbo";
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost"; let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
// Try each boost control path in order of specificity // Try each boost control path in order of specificity
@ -624,13 +678,15 @@ impl Cpu {
} }
pub fn turbo() -> anyhow::Result<Option<bool>> { pub fn turbo() -> anyhow::Result<Option<bool>> {
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo") if let Some(content) =
fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
.context("failed to read CPU turbo boost status")? .context("failed to read CPU turbo boost status")?
{ {
return Ok(Some(content == 0)); return Ok(Some(content == 0));
} }
if let Some(content) = fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost") if let Some(content) =
fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
.context("failed to read CPU turbo boost status")? .context("failed to read CPU turbo boost status")?
{ {
return Ok(Some(content == 1)); return Ok(Some(content == 1));

View file

@ -1,17 +1,29 @@
use std::{ use std::{
cell::LazyCell, cell::LazyCell,
collections::{HashMap, VecDeque}, collections::{
HashMap,
VecDeque,
},
sync::{ sync::{
Arc, Arc,
atomic::{AtomicBool, Ordering}, atomic::{
AtomicBool,
Ordering,
},
}, },
thread, thread,
time::{Duration, Instant}, time::{
Duration,
Instant,
},
}; };
use anyhow::Context; use anyhow::Context;
use crate::{config, system}; use crate::{
config,
system,
};
/// Calculate the idle time multiplier based on system idle time. /// Calculate the idle time multiplier based on system idle time.
/// ///
@ -29,7 +41,7 @@ fn idle_multiplier(idle_for: Duration) -> f64 {
false => { false => {
let idle_minutes = idle_for.as_secs() as f64 / 60.0; let idle_minutes = idle_for.as_secs() as f64 / 60.0;
idle_minutes.log2() idle_minutes.log2()
} },
}; };
// Clamp the multiplier to avoid excessive delays. // Clamp the multiplier to avoid excessive delays.
@ -154,7 +166,8 @@ impl Daemon {
let mut temperature_change_sum = 0.0; let mut temperature_change_sum = 0.0;
for index in 0..change_count { for index in 0..change_count {
let usage_change = self.cpu_log[index + 1].usage - self.cpu_log[index].usage; let usage_change =
self.cpu_log[index + 1].usage - self.cpu_log[index].usage;
usage_change_sum += usage_change.abs(); usage_change_sum += usage_change.abs();
let temperature_change = let temperature_change =
@ -206,10 +219,9 @@ struct PowerSupplyLog {
impl Daemon { impl Daemon {
fn discharging(&self) -> bool { fn discharging(&self) -> bool {
self.system self.system.power_supplies.iter().any(|power_supply| {
.power_supplies power_supply.charge_state.as_deref() == Some("Discharging")
.iter() })
.any(|power_supply| power_supply.charge_state.as_deref() == Some("Discharging"))
} }
/// Calculates the discharge rate, returns a number between 0 and 1. /// Calculates the discharge rate, returns a number between 0 and 1.
@ -271,13 +283,13 @@ impl Daemon {
delay /= 2; delay /= 2;
delay *= 3; delay *= 3;
} }
} },
// If we can't determine the discharge rate, that means that // If we can't determine the discharge rate, that means that
// we were very recently started. Which is user activity. // we were very recently started. Which is user activity.
None => { None => {
delay *= 2; delay *= 2;
} },
} }
} }
@ -288,7 +300,8 @@ impl Daemon {
let factor = idle_multiplier(idle_for); let factor = idle_multiplier(idle_for);
log::debug!( log::debug!(
"system has been idle for {seconds} seconds (approx {minutes} minutes), applying idle factor: {factor:.2}x", "system has been idle for {seconds} seconds (approx {minutes} \
minutes), applying idle factor: {factor:.2}x",
seconds = idle_for.as_secs(), seconds = idle_for.as_secs(),
minutes = idle_for.as_secs() / 60, minutes = idle_for.as_secs() / 60,
); );
@ -304,10 +317,12 @@ impl Daemon {
} }
let delay = match self.last_polling_delay { let delay = match self.last_polling_delay {
Some(last_delay) => Duration::from_secs_f64( Some(last_delay) => {
Duration::from_secs_f64(
// 30% of current computed delay, 70% of last delay. // 30% of current computed delay, 70% of last delay.
delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7, delay.as_secs_f64() * 0.3 + last_delay.as_secs_f64() * 0.7,
), )
},
None => delay, None => delay,
}; };
@ -351,7 +366,8 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
let delay = daemon.polling_delay(); let delay = daemon.polling_delay();
log::info!( log::info!(
"next poll will be in {seconds} seconds or {minutes} minutes, possibly delayed if application of rules takes more than the polling delay", "next poll will be in {seconds} seconds or {minutes} minutes, possibly \
delayed if application of rules takes more than the polling delay",
seconds = delay.as_secs_f64(), seconds = delay.as_secs_f64(),
minutes = delay.as_secs_f64() / 60.0, minutes = delay.as_secs_f64() / 60.0,
); );
@ -364,15 +380,25 @@ pub fn run(config: config::DaemonConfig) -> anyhow::Result<()> {
cpu_usage: daemon.cpu_log.back().unwrap().usage, cpu_usage: daemon.cpu_log.back().unwrap().usage,
cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage), cpu_usage_volatility: daemon.cpu_volatility().map(|vol| vol.usage),
cpu_temperature: daemon.cpu_log.back().unwrap().temperature, cpu_temperature: daemon.cpu_log.back().unwrap().temperature,
cpu_temperature_volatility: daemon.cpu_volatility().map(|vol| vol.temperature), cpu_temperature_volatility: daemon
cpu_idle_seconds: daemon.last_user_activity.elapsed().as_secs_f64(), .cpu_volatility()
power_supply_charge: daemon.power_supply_log.back().unwrap().charge, .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(), power_supply_discharge_rate: daemon.power_supply_discharge_rate(),
discharging: daemon.discharging(), discharging: daemon.discharging(),
}; };
let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new(); let mut cpu_delta_for = HashMap::<u32, config::CpuDelta>::new();
let all_cpus = LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>()); let all_cpus =
LazyCell::new(|| (0..num_cpus::get() as u32).collect::<Vec<_>>());
for rule in &config.rules { for rule in &config.rules {
let Some(condition) = rule.condition.eval(&state)? else { let Some(condition) = rule.condition.eval(&state)? else {

View file

@ -1,4 +1,10 @@
use std::{error, fs, io, path::Path, str}; use std::{
error,
fs,
io,
path::Path,
str,
};
use anyhow::Context; use anyhow::Context;
@ -16,10 +22,12 @@ pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<Option<fs::ReadDir>> {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).context(format!( Err(error) => {
Err(error).context(format!(
"failed to read directory '{path}'", "failed to read directory '{path}'",
path = path.display() path = path.display()
)), ))
},
} }
} }
@ -31,23 +39,30 @@ pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), Err(error) => {
Err(error)
.context(format!("failed to read '{path}", path = path.display()))
},
} }
} }
pub fn read_n<N: str::FromStr>(path: impl AsRef<Path>) -> anyhow::Result<Option<N>> pub fn read_n<N: str::FromStr>(
path: impl AsRef<Path>,
) -> anyhow::Result<Option<N>>
where where
N::Err: error::Error + Send + Sync + 'static, N::Err: error::Error + Send + Sync + 'static,
{ {
let path = path.as_ref(); let path = path.as_ref();
match read(path)? { match read(path)? {
Some(content) => Ok(Some(content.trim().parse().with_context(|| { Some(content) => {
Ok(Some(content.trim().parse().with_context(|| {
format!( format!(
"failed to parse contents of '{path}' as a unsigned number", "failed to parse contents of '{path}' as a unsigned number",
path = path.display(), path = path.display(),
) )
})?)), })?))
},
None => Ok(None), None => Ok(None),
} }

View file

@ -10,12 +10,16 @@ mod daemon;
// mod engine; // mod engine;
// mod monitor; // mod monitor;
use std::{
fmt::Write as _,
io,
io::Write as _,
path::PathBuf,
process,
};
use anyhow::Context; use anyhow::Context;
use clap::Parser as _; use clap::Parser as _;
use std::fmt::Write as _;
use std::io::Write as _;
use std::path::PathBuf;
use std::{io, process};
use yansi::Paint as _; use yansi::Paint as _;
#[derive(clap::Parser, Debug)] #[derive(clap::Parser, Debug)]
@ -90,7 +94,7 @@ fn real_main() -> anyhow::Result<()> {
.context("failed to load daemon config")?; .context("failed to load daemon config")?;
daemon::run(config) daemon::run(config)
} },
Command::Cpu { Command::Cpu {
command: CpuCommand::Set(delta), command: CpuCommand::Set(delta),
@ -133,18 +137,20 @@ fn main() {
let mut chars = message.char_indices(); let mut chars = message.char_indices();
let _ = match (chars.next(), chars.next()) { let _ = match (chars.next(), chars.next()) {
(Some((_, first)), Some((second_start, second))) if second.is_lowercase() => { (Some((_, first)), Some((second_start, second)))
if second.is_lowercase() =>
{
writeln!( writeln!(
err, err,
"{first_lowercase}{rest}", "{first_lowercase}{rest}",
first_lowercase = first.to_lowercase(), first_lowercase = first.to_lowercase(),
rest = &message[second_start..], rest = &message[second_start..],
) )
} },
_ => { _ => {
writeln!(err, "{message}") writeln!(err, "{message}")
} },
}; };
} }

View file

@ -1,11 +1,18 @@
use anyhow::{Context, anyhow, bail};
use yansi::Paint as _;
use std::{ use std::{
fmt, fmt,
path::{Path, PathBuf}, path::{
Path,
PathBuf,
},
}; };
use anyhow::{
Context,
anyhow,
bail,
};
use yansi::Paint as _;
use crate::fs; use crate::fs;
/// Represents a pattern of path suffixes used to control charge thresholds /// Represents a pattern of path suffixes used to control charge thresholds
@ -155,7 +162,9 @@ impl PowerSupply {
for entry in fs::read_dir(POWER_SUPPLY_PATH) for entry in fs::read_dir(POWER_SUPPLY_PATH)
.context("failed to read power supply entries")? .context("failed to read power supply entries")?
.with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))? .with_context(|| {
format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?")
})?
{ {
let entry = match entry { let entry = match entry {
Ok(entry) => entry, Ok(entry) => entry,
@ -163,7 +172,7 @@ impl PowerSupply {
Err(error) => { Err(error) => {
log::warn!("failed to read power supply entry: {error}"); log::warn!("failed to read power supply entry: {error}");
continue; continue;
} },
}; };
power_supplies.push(PowerSupply::from_path(entry.path())?); power_supplies.push(PowerSupply::from_path(entry.path())?);
@ -181,8 +190,12 @@ impl PowerSupply {
let type_path = self.path.join("type"); let type_path = self.path.join("type");
fs::read(&type_path) fs::read(&type_path)
.with_context(|| format!("failed to read '{path}'", path = type_path.display()))? .with_context(|| {
.with_context(|| format!("'{path}' doesn't exist", path = type_path.display()))? format!("failed to read '{path}'", path = type_path.display())
})?
.with_context(|| {
format!("'{path}' doesn't exist", path = type_path.display())
})?
}; };
self.is_from_peripheral = 'is_from_peripheral: { self.is_from_peripheral = 'is_from_peripheral: {
@ -201,8 +214,10 @@ impl PowerSupply {
} }
// Small capacity batteries are likely not laptop batteries. // Small capacity batteries are likely not laptop batteries.
if let Some(energy_full) = fs::read_n::<u64>(self.path.join("energy_full")) if let Some(energy_full) =
.with_context(|| format!("failed to read the max charge {self} can hold"))? fs::read_n::<u64>(self.path.join("energy_full")).with_context(|| {
format!("failed to read the max charge {self} can hold")
})?
{ {
// Most laptop batteries are at least 20,000,000 µWh (20 Wh). // Most laptop batteries are at least 20,000,000 µWh (20 Wh).
// Peripheral batteries are typically much smaller. // Peripheral batteries are typically much smaller.
@ -233,24 +248,31 @@ impl PowerSupply {
self.charge_threshold_start = self.charge_threshold_start =
fs::read_n::<u64>(self.path.join("charge_control_start_threshold")) fs::read_n::<u64>(self.path.join("charge_control_start_threshold"))
.with_context(|| format!("failed to read {self} charge threshold start"))? .with_context(|| {
format!("failed to read {self} charge threshold start")
})?
.map_or(0.0, |percent| percent as f64 / 100.0); .map_or(0.0, |percent| percent as f64 / 100.0);
self.charge_threshold_end = self.charge_threshold_end =
fs::read_n::<u64>(self.path.join("charge_control_end_threshold")) fs::read_n::<u64>(self.path.join("charge_control_end_threshold"))
.with_context(|| format!("failed to read {self} charge threshold end"))? .with_context(|| {
format!("failed to read {self} charge threshold end")
})?
.map_or(100.0, |percent| percent as f64 / 100.0); .map_or(100.0, |percent| percent as f64 / 100.0);
self.drain_rate_watts = match fs::read_n::<i64>(self.path.join("power_now")) self.drain_rate_watts =
match fs::read_n::<i64>(self.path.join("power_now"))
.with_context(|| format!("failed to read {self} power drain"))? .with_context(|| format!("failed to read {self} power drain"))?
{ {
Some(drain) => Some(drain as f64), Some(drain) => Some(drain as f64),
None => { None => {
let current_ua = fs::read_n::<i32>(self.path.join("current_now")) let current_ua =
fs::read_n::<i32>(self.path.join("current_now"))
.with_context(|| format!("failed to read {self} current"))?; .with_context(|| format!("failed to read {self} current"))?;
let voltage_uv = fs::read_n::<i32>(self.path.join("voltage_now")) let voltage_uv =
fs::read_n::<i32>(self.path.join("voltage_now"))
.with_context(|| format!("failed to read {self} voltage"))?; .with_context(|| format!("failed to read {self} voltage"))?;
current_ua.zip(voltage_uv).map(|(current, voltage)| { current_ua.zip(voltage_uv).map(|(current, voltage)| {
@ -258,7 +280,7 @@ impl PowerSupply {
// (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W // (v / 1e6 V) * (c / 1e6 A) = (v * c / 1e12) W
current as f64 * voltage as f64 / 1e12 current as f64 * voltage as f64 / 1e12
}) })
} },
}; };
self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS self.threshold_config = POWER_SUPPLY_THRESHOLD_CONFIGS
@ -274,12 +296,14 @@ impl PowerSupply {
} }
pub fn charge_threshold_path_start(&self) -> Option<PathBuf> { pub fn charge_threshold_path_start(&self) -> Option<PathBuf> {
self.threshold_config self
.threshold_config
.map(|config| self.path.join(config.path_start)) .map(|config| self.path.join(config.path_start))
} }
pub fn charge_threshold_path_end(&self) -> Option<PathBuf> { pub fn charge_threshold_path_end(&self) -> Option<PathBuf> {
self.threshold_config self
.threshold_config
.map(|config| self.path.join(config.path_end)) .map(|config| self.path.join(config.path_end))
} }
@ -290,36 +314,49 @@ impl PowerSupply {
fs::write( fs::write(
&self.charge_threshold_path_start().ok_or_else(|| { &self.charge_threshold_path_start().ok_or_else(|| {
anyhow!( anyhow!(
"power supply '{name}' does not support changing charge threshold levels", "power supply '{name}' does not support changing charge threshold \
levels",
name = self.name, name = self.name,
) )
})?, })?,
&((charge_threshold_start * 100.0) as u8).to_string(), &((charge_threshold_start * 100.0) as u8).to_string(),
) )
.with_context(|| format!("failed to set charge threshold start for {self}"))?; .with_context(|| {
format!("failed to set charge threshold start for {self}")
})?;
self.charge_threshold_start = charge_threshold_start; self.charge_threshold_start = charge_threshold_start;
log::info!("set battery threshold start for {self} to {charge_threshold_start}%"); log::info!(
"set battery threshold start for {self} to {charge_threshold_start}%"
);
Ok(()) Ok(())
} }
pub fn set_charge_threshold_end(&mut self, charge_threshold_end: f64) -> anyhow::Result<()> { pub fn set_charge_threshold_end(
&mut self,
charge_threshold_end: f64,
) -> anyhow::Result<()> {
fs::write( fs::write(
&self.charge_threshold_path_end().ok_or_else(|| { &self.charge_threshold_path_end().ok_or_else(|| {
anyhow!( anyhow!(
"power supply '{name}' does not support changing charge threshold levels", "power supply '{name}' does not support changing charge threshold \
levels",
name = self.name, name = self.name,
) )
})?, })?,
&((charge_threshold_end * 100.0) as u8).to_string(), &((charge_threshold_end * 100.0) as u8).to_string(),
) )
.with_context(|| format!("failed to set charge threshold end for {self}"))?; .with_context(|| {
format!("failed to set charge threshold end for {self}")
})?;
self.charge_threshold_end = charge_threshold_end; self.charge_threshold_end = charge_threshold_end;
log::info!("set battery threshold end for {self} to {charge_threshold_end}%"); log::info!(
"set battery threshold end for {self} to {charge_threshold_end}%"
);
Ok(()) Ok(())
} }
@ -327,20 +364,23 @@ impl PowerSupply {
pub fn get_available_platform_profiles() -> anyhow::Result<Vec<String>> { pub fn get_available_platform_profiles() -> anyhow::Result<Vec<String>> {
let path = "/sys/firmware/acpi/platform_profile_choices"; let path = "/sys/firmware/acpi/platform_profile_choices";
let Some(content) = let Some(content) = fs::read(path)
fs::read(path).context("failed to read available ACPI platform profiles")? .context("failed to read available ACPI platform profiles")?
else { else {
return Ok(Vec::new()); return Ok(Vec::new());
}; };
Ok(content Ok(
content
.split_whitespace() .split_whitespace()
.map(ToString::to_string) .map(ToString::to_string)
.collect()) .collect(),
)
} }
/// Sets the platform profile. /// Sets the platform profile.
/// This changes the system performance, temperature, fan, and other hardware replated characteristics. /// This changes the system performance, temperature, fan, and other hardware
/// related characteristics.
/// ///
/// Also see [`The Kernel docs`] for this. /// Also see [`The Kernel docs`] for this.
/// ///
@ -353,13 +393,16 @@ impl PowerSupply {
.any(|avail_profile| avail_profile == profile) .any(|avail_profile| avail_profile == profile)
{ {
bail!( bail!(
"profile '{profile}' is not available for system. valid profiles: {profiles}", "profile '{profile}' is not available for system. valid profiles: \
{profiles}",
profiles = profiles.join(", "), profiles = profiles.join(", "),
); );
} }
fs::write("/sys/firmware/acpi/platform_profile", profile) fs::write("/sys/firmware/acpi/platform_profile", profile).context(
.context("this probably means that your system does not support changing ACPI profiles") "this probably means that your system does not support changing ACPI \
profiles",
)
} }
pub fn platform_profile() -> anyhow::Result<String> { pub fn platform_profile() -> anyhow::Result<String> {

View file

@ -1,8 +1,19 @@
use std::{collections::HashMap, path::Path, time::Instant}; use std::{
collections::HashMap,
path::Path,
time::Instant,
};
use anyhow::{Context, bail}; use anyhow::{
Context,
bail,
};
use crate::{cpu, fs, power_supply}; use crate::{
cpu,
fs,
power_supply,
};
#[derive(Debug)] #[derive(Debug)]
pub struct System { pub struct System {
@ -52,8 +63,8 @@ impl System {
{ {
let start = Instant::now(); let start = Instant::now();
self.power_supplies = self.power_supplies = power_supply::PowerSupply::all()
power_supply::PowerSupply::all().context("failed to scan power supplies")?; .context("failed to scan power supplies")?;
log::debug!( log::debug!(
"rescanned all power supplies in {millis}ms", "rescanned all power supplies in {millis}ms",
millis = start.elapsed().as_millis(), millis = start.elapsed().as_millis(),
@ -66,7 +77,8 @@ impl System {
.any(|power_supply| power_supply.is_ac()) .any(|power_supply| power_supply.is_ac())
|| { || {
log::debug!( log::debug!(
"checking whether if this device is a desktop to determine if it is AC as no power supplies are" "checking whether if this device is a desktop to determine if it is \
AC as no power supplies are"
); );
let start = Instant::now(); let start = Instant::now();
@ -118,11 +130,13 @@ impl System {
.context("failed to read hardware information")? .context("failed to read hardware information")?
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{ {
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; let entry =
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_path = entry.path(); let entry_path = entry.path();
let Some(name) = fs::read(entry_path.join("name")).with_context(|| { let Some(name) =
fs::read(entry_path.join("name")).with_context(|| {
format!( format!(
"failed to read name of hardware entry at '{path}'", "failed to read name of hardware entry at '{path}'",
path = entry_path.display(), path = entry_path.display(),
@ -136,14 +150,14 @@ impl System {
// TODO: 'zenergy' can also report those stats, I think? // TODO: 'zenergy' can also report those stats, I think?
"coretemp" | "k10temp" | "zenpower" | "amdgpu" => { "coretemp" | "k10temp" | "zenpower" | "amdgpu" => {
Self::get_temperatures(&entry_path, &mut temperatures)?; Self::get_temperatures(&entry_path, &mut temperatures)?;
} },
// Other CPU temperature drivers. // Other CPU temperature drivers.
_ if name.contains("cpu") || name.contains("temp") => { _ if name.contains("cpu") || name.contains("temp") => {
Self::get_temperatures(&entry_path, &mut temperatures)?; Self::get_temperatures(&entry_path, &mut temperatures)?;
} },
_ => {} _ => {},
} }
} }
@ -151,7 +165,8 @@ impl System {
const PATH: &str = "/sys/devices/virtual/thermal"; const PATH: &str = "/sys/devices/virtual/thermal";
log::debug!( log::debug!(
"failed to get CPU temperature information by using hwmon, falling back to '{PATH}'" "failed to get CPU temperature information by using hwmon, falling \
back to '{PATH}'"
); );
let Some(thermal_zones) = let Some(thermal_zones) =
@ -163,7 +178,8 @@ impl System {
let mut counter = 0; let mut counter = 0;
for entry in thermal_zones { for entry in thermal_zones {
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; let entry =
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_path = entry.path(); let entry_path = entry.path();
@ -174,7 +190,8 @@ impl System {
continue; continue;
} }
let Some(entry_type) = fs::read(entry_path.join("type")).with_context(|| { let Some(entry_type) =
fs::read(entry_path.join("type")).with_context(|| {
format!( format!(
"failed to read type of zone at '{path}'", "failed to read type of zone at '{path}'",
path = entry_path.display(), path = entry_path.display(),
@ -265,18 +282,24 @@ impl System {
.trim_start_matches("-") .trim_start_matches("-")
.trim(); .trim();
log::debug!("stripped 'Core' or similar identifier prefix of label content: {number}"); log::debug!(
"stripped 'Core' or similar identifier prefix of label content: \
{number}"
);
let Ok(number) = number.parse::<u32>() else { let Ok(number) = number.parse::<u32>() else {
log::debug!("stripped content not a valid number, skipping"); log::debug!("stripped content not a valid number, skipping");
continue; continue;
}; };
log::debug!("stripped content is a valid number, taking it as the core number"); log::debug!(
"stripped content is a valid number, taking it as the core number"
);
log::debug!( log::debug!(
"it is fine if this number doesn't seem accurate due to CPU binning, see a more detailed explanation at: https://rgbcu.be/blog/why-cores" "it is fine if this number doesn't seem accurate due to CPU binning, see a more detailed explanation at: https://rgbcu.be/blog/why-cores"
); );
let Some(temperature_mc) = fs::read_n::<i64>(&input_path).with_context(|| { let Some(temperature_mc) =
fs::read_n::<i64>(&input_path).with_context(|| {
format!( format!(
"failed to read CPU temperature from '{path}'", "failed to read CPU temperature from '{path}'",
path = input_path.display(), path = input_path.display(),
@ -286,8 +309,8 @@ impl System {
continue; continue;
}; };
log::debug!( log::debug!(
"temperature content: {celcius} celcius", "temperature content: {celsius} celsius",
celcius = temperature_mc as f64 / 1000.0 celsius = temperature_mc as f64 / 1000.0
); );
temperatures.insert(number, temperature_mc as f64 / 1000.0); temperatures.insert(number, temperature_mc as f64 / 1000.0);
@ -298,25 +321,25 @@ impl System {
fn is_desktop(&mut self) -> anyhow::Result<bool> { fn is_desktop(&mut self) -> anyhow::Result<bool> {
log::debug!("checking chassis type to determine if we are a desktop"); log::debug!("checking chassis type to determine if we are a desktop");
if let Some(chassis_type) = if let Some(chassis_type) = fs::read("/sys/class/dmi/id/chassis_type")
fs::read("/sys/class/dmi/id/chassis_type").context("failed to read chassis type")? .context("failed to read chassis type")?
{ {
// 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower, // 3=Desktop, 4=Low Profile Desktop, 5=Pizza Box, 6=Mini Tower,
// 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In One, // 7=Tower, 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 13=All In
// 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main Server Chassis, // One, 14=Sub Notebook, 15=Space-saving, 16=Lunch Box, 17=Main
// 31=Convertible Laptop // Server Chassis, 31=Convertible Laptop
match chassis_type.trim() { match chassis_type.trim() {
// Desktop form factors. // Desktop form factors.
"3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => { "3" | "4" | "5" | "6" | "7" | "15" | "16" | "17" => {
log::debug!("chassis is a desktop form factor, short circuting true"); log::debug!("chassis is a desktop form factor, short circuting true");
return Ok(true); return Ok(true);
} },
// Laptop form factors. // Laptop form factors.
"9" | "10" | "14" | "31" => { "9" | "10" | "14" | "31" => {
log::debug!("chassis is a laptop form factor, short circuting false"); log::debug!("chassis is a laptop form factor, short circuting false");
return Ok(false); return Ok(false);
} },
// Unknown, continue with other checks // Unknown, continue with other checks
_ => log::debug!("unknown chassis type"), _ => log::debug!("unknown chassis type"),
@ -340,7 +363,8 @@ impl System {
log::debug!("checking if power saving paths exists"); log::debug!("checking if power saving paths exists");
// Check CPU power policies, desktops often don't have these // Check CPU power policies, desktops often don't have these
let power_saving_exists = fs::exists("/sys/module/intel_pstate/parameters/no_hwp") let power_saving_exists =
fs::exists("/sys/module/intel_pstate/parameters/no_hwp")
|| fs::exists("/sys/devices/system/cpu/cpufreq/conservative"); || fs::exists("/sys/devices/system/cpu/cpufreq/conservative");
if !power_saving_exists { if !power_saving_exists {
@ -349,7 +373,9 @@ impl System {
} }
// Default to assuming desktop if we can't determine. // Default to assuming desktop if we can't determine.
log::debug!("cannot determine whether if we are a desktop, defaulting to true"); log::debug!(
"cannot determine whether if we are a desktop, defaulting to true"
);
Ok(true) Ok(true)
} }
@ -360,11 +386,15 @@ impl System {
let mut parts = content.split_whitespace(); let mut parts = content.split_whitespace();
let (Some(load_average_1min), Some(load_average_5min), Some(load_average_15min)) = let (
(parts.next(), parts.next(), parts.next()) Some(load_average_1min),
Some(load_average_5min),
Some(load_average_15min),
) = (parts.next(), parts.next(), parts.next())
else { else {
bail!( bail!(
"failed to parse first 3 load average entries due to there not being enough, content: {content}" "failed to parse first 3 load average entries due to there not being \
enough, content: {content}"
); );
}; };