diff --git a/.rustfmt.toml b/.rustfmt.toml index 27e03ef..df184f2 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,6 @@ +# 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 diff --git a/.taplo.toml b/.taplo.toml index 5a6456b..9abeaee 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -1,3 +1,5 @@ +# Taken from https://github.com/cull-os/carcass. + [formatting] align_entries = true column_width = 100 diff --git a/Cargo.toml b/Cargo.toml index 92845d7..12f2395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,12 @@ -[workspace] -members = [ "watt" ] -resolver = "3" - -[workspace.package] -authors = [ "NotAShelf ", "RGBCube " ] +[package] +name = "watt" description = "Modern CPU frequency and power management utility for Linux" -edition = "2024" # Keep in sync with .rustfmt.toml. -license = "MPL-2.0" -repository = "https://github.com/notashelf/watt" -rust-version = "1.85" version = "0.4.0" +edition = "2024" +authors = [ "NotAShelf ", "RGBCube " ] +rust-version = "1.85" -[workspace.dependencies] +[dependencies] anyhow = "1.0" clap = { version = "4.0", features = [ "derive", "env" ] } clap-verbosity-flag = "3.0.2" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4eaf79b --- /dev/null +++ b/build.rs @@ -0,0 +1,57 @@ +use std::{ + env, + fs, + path::PathBuf, +}; + +const MULTICALL_NAMES: &[&str] = &["cpu", "power"]; + +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=target"); + + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let target = out_dir + .parent() // target/debug/build/-/out + .and_then(|p| p.parent()) // target/debug/build/- + .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(()) +} diff --git a/watt/config.toml b/config.toml similarity index 72% rename from watt/config.toml rename to config.toml index 421aa62..293c9e8 100644 --- a/watt/config.toml +++ b/config.toml @@ -5,27 +5,28 @@ # Emergency thermal protection (highest priority). [[rule]] -if = { value = "$cpu-temperature", is-more-than = 85.0 } -priority = 100 - cpu.energy-performance-preference = "power" cpu.frequency-mhz-maximum = 2000 cpu.governor = "powersave" cpu.turbo = false +if = { value = "$cpu-temperature", is-more-than = 85.0 } +priority = 100 # Critical battery preservation. [[rule]] -if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ] -priority = 90 - 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 +if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ] power.platform-profile = "low-power" +priority = 90 # High performance mode for sustained high load. [[rule]] +cpu.energy-performance-preference = "performance" +cpu.governor = "performance" +cpu.turbo = true if.all = [ { value = "%cpu-usage", is-more-than = 0.8 }, { value = "$cpu-idle-seconds", is-less-than = 30.0 }, @@ -33,12 +34,12 @@ if.all = [ ] priority = 80 -cpu.energy-performance-preference = "performance" -cpu.governor = "performance" -cpu.turbo = true - # Performance mode when not discharging. [[rule]] +cpu.energy-performance-bias = "balance_performance" +cpu.energy-performance-preference = "performance" +cpu.governor = "performance" +cpu.turbo = true if.all = [ { not = "?discharging" }, { value = "%cpu-usage", is-more-than = 0.1 }, @@ -46,66 +47,56 @@ if.all = [ ] priority = 70 -cpu.energy-performance-bias = "balance_performance" -cpu.energy-performance-preference = "performance" -cpu.governor = "performance" -cpu.turbo = true - # Moderate performance for medium load. [[rule]] +cpu.energy-performance-preference = "balance_performance" +cpu.governor = "schedutil" if.all = [ { value = "%cpu-usage", is-more-than = 0.4 }, { value = "%cpu-usage", is-less-than = 0.8 }, ] priority = 60 -cpu.energy-performance-preference = "balance_performance" -cpu.governor = "schedutil" - # Power saving during low activity. [[rule]] +cpu.energy-performance-preference = "power" +cpu.governor = "powersave" +cpu.turbo = false if.all = [ { value = "%cpu-usage", is-less-than = 0.2 }, { value = "$cpu-idle-seconds", is-more-than = 60.0 }, ] priority = 50 -cpu.energy-performance-preference = "power" -cpu.governor = "powersave" -cpu.turbo = false - # Extended idle power optimization. [[rule]] -if = { value = "$cpu-idle-seconds", is-more-than = 300.0 } -priority = 40 - cpu.energy-performance-preference = "power" cpu.frequency-mhz-maximum = 1600 cpu.governor = "powersave" cpu.turbo = false +if = { value = "$cpu-idle-seconds", is-more-than = 300.0 } +priority = 40 # Battery conservation when discharging. [[rule]] -if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ] -priority = 30 - cpu.energy-performance-preference = "power" cpu.frequency-mhz-maximum = 2000 cpu.governor = "powersave" cpu.turbo = false +if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ] power.platform-profile = "low-power" +priority = 30 # General battery mode. [[rule]] -if = "?discharging" -priority = 20 - cpu.energy-performance-bias = "balance_power" cpu.energy-performance-preference = "power" cpu.frequency-mhz-maximum = 1800 cpu.frequency-mhz-minimum = 200 cpu.governor = "powersave" cpu.turbo = false +if = "?discharging" +priority = 20 # Balanced performance for general use. Default fallback rule. [[rule]] diff --git a/watt/config.rs b/src/config.rs similarity index 99% rename from watt/config.rs rename to src/config.rs index 17fa748..4342a62 100644 --- a/watt/config.rs +++ b/src/config.rs @@ -523,7 +523,7 @@ pub struct DaemonConfig { } impl DaemonConfig { - const DEFAULT: &str = include_str!("config.toml"); + const DEFAULT: &str = include_str!("../config.toml"); pub fn load_from(path: Option<&Path>) -> anyhow::Result { let contents = if let Some(path) = path { diff --git a/watt/cpu.rs b/src/cpu.rs similarity index 100% rename from watt/cpu.rs rename to src/cpu.rs diff --git a/watt/daemon.rs b/src/daemon.rs similarity index 100% rename from watt/daemon.rs rename to src/daemon.rs diff --git a/watt/fs.rs b/src/fs.rs similarity index 100% rename from watt/fs.rs rename to src/fs.rs diff --git a/watt/lib.rs b/src/main.rs similarity index 56% rename from watt/lib.rs rename to src/main.rs index 6b84a5b..0ec780c 100644 --- a/watt/lib.rs +++ b/src/main.rs @@ -1,27 +1,37 @@ -use std::path::PathBuf; +mod cpu; +mod power_supply; +mod system; -use anyhow::Context as _; +mod fs; + +mod config; +// mod core; +mod daemon; +// mod engine; +// mod monitor; + +use std::{ + fmt::Write as _, + io, + io::Write as _, + path::PathBuf, + process, +}; + +use anyhow::Context; use clap::Parser as _; - -pub mod cpu; -pub mod power_supply; -pub mod system; - -pub mod fs; - -pub mod config; -pub mod daemon; +use yansi::Paint as _; #[derive(clap::Parser, Debug)] #[clap(author, version, about)] -pub struct Cli { +struct Cli { #[clap(subcommand)] command: Command, } #[derive(clap::Parser, Debug)] #[clap(multicall = true)] -pub enum Command { +enum Command { /// Watt daemon. Watt { #[command(flatten)] @@ -52,18 +62,18 @@ pub enum Command { } #[derive(clap::Parser, Debug)] -pub enum CpuCommand { +enum CpuCommand { /// Modify CPU attributes. Set(config::CpuDelta), } #[derive(clap::Parser, Debug)] -pub enum PowerCommand { +enum PowerCommand { /// Modify power supply attributes. Set(config::PowerDelta), } -pub fn main() -> anyhow::Result<()> { +fn real_main() -> anyhow::Result<()> { let cli = Cli::parse(); yansi::whenever(yansi::Condition::TTY_AND_COLOR); @@ -97,3 +107,52 @@ pub fn main() -> anyhow::Result<()> { } => delta.apply(), } } + +fn main() { + let Err(error) = real_main() else { + return; + }; + + let mut err = io::stderr(); + + let mut message = String::new(); + let mut chain = error.chain().rev().peekable(); + + while let Some(error) = chain.next() { + let _ = write!( + err, + "{header} ", + header = if chain.peek().is_none() { + "error:" + } else { + "cause:" + } + .red() + .bold(), + ); + + String::clear(&mut message); + let _ = write!(message, "{error}"); + + let mut chars = message.char_indices(); + + let _ = match (chars.next(), chars.next()) { + (Some((_, first)), Some((second_start, second))) + if second.is_lowercase() => + { + writeln!( + err, + "{first_lowercase}{rest}", + first_lowercase = first.to_lowercase(), + rest = &message[second_start..], + ) + }, + + _ => { + writeln!(err, "{message}") + }, + }; + } + + process::exit(1); +} diff --git a/watt/power_supply.rs b/src/power_supply.rs similarity index 97% rename from watt/power_supply.rs rename to src/power_supply.rs index fd11712..3bd03f2 100644 --- a/watt/power_supply.rs +++ b/src/power_supply.rs @@ -227,13 +227,10 @@ impl PowerSupply { } } // Check for model name that indicates a peripheral - if let Some(model_name) = fs::read(self.path.join("model_name")) + if let Some(model) = fs::read(self.path.join("model_name")) .with_context(|| format!("failed to read the model name of {self}"))? { - let model_name_lower = model_name.to_lowercase(); - if model_name_lower.contains("bluetooth") - || model_name_lower.contains("wireless") - { + if model.contains("bluetooth") || model.contains("wireless") { break 'is_from_peripheral true; } } diff --git a/watt/system.rs b/src/system.rs similarity index 98% rename from watt/system.rs rename to src/system.rs index 19bcafd..fccf624 100644 --- a/watt/system.rs +++ b/src/system.rs @@ -295,8 +295,8 @@ impl System { "stripped content is a valid number, taking it as the core number" ); 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::(&input_path).with_context(|| { diff --git a/watt/Cargo.toml b/watt/Cargo.toml deleted file mode 100644 index 729ce63..0000000 --- a/watt/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "watt" -description.workspace = true -version.workspace = true -edition.workspace = true -authors.workspace = true -rust-version.workspace = true - -[lib] -path = "lib.rs" - -[[bin]] -name = "watt" -path = "main.rs" - -[dependencies] -anyhow.workspace = true -clap.workspace = true -clap-verbosity-flag.workspace = true -ctrlc.workspace = true -derive_more.workspace = true -env_logger.workspace = true -log.workspace = true -num_cpus.workspace = true -serde.workspace = true -thiserror.workspace = true -toml.workspace = true -yansi.workspace = true diff --git a/watt/main.rs b/watt/main.rs deleted file mode 100644 index 8cb0902..0000000 --- a/watt/main.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::{ - fmt::Write as _, - io, - io::Write as _, - process, -}; - -use yansi::Paint as _; - -fn main() { - let Err(error) = watt::main() else { - return; - }; - - let mut err = io::stderr(); - - let mut message = String::new(); - let mut chain = error.chain().rev().peekable(); - - while let Some(error) = chain.next() { - let _ = write!( - err, - "{header} ", - header = if chain.peek().is_none() { - "error:" - } else { - "cause:" - } - .red() - .bold(), - ); - - String::clear(&mut message); - let _ = write!(message, "{error}"); - - let mut chars = message.char_indices(); - - let _ = match (chars.next(), chars.next()) { - (Some((_, first)), Some((second_start, second))) - if second.is_lowercase() => - { - writeln!( - err, - "{first_lowercase}{rest}", - first_lowercase = first.to_lowercase(), - rest = &message[second_start..], - ) - }, - - _ => { - writeln!(err, "{message}") - }, - }; - } - - process::exit(1); -}