mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-30 18:37:46 +00:00
Compare commits
4 commits
496f5a7f35
...
a341d08c45
Author | SHA1 | Date | |
---|---|---|---|
a341d08c45 | |||
3c82679ada | |||
b4e5dd7fea | |||
6ea92c1624 |
14 changed files with 154 additions and 173 deletions
|
@ -1,6 +1,3 @@
|
||||||
# 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?
|
# float_literal_trailing_zero = "Always" # TODO: Warning for some reason?
|
||||||
condense_wildcard_suffixes = true
|
condense_wildcard_suffixes = true
|
||||||
doc_comment_code_block_width = 80
|
doc_comment_code_block_width = 80
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# Taken from https://github.com/cull-os/carcass.
|
|
||||||
|
|
||||||
[formatting]
|
[formatting]
|
||||||
align_entries = true
|
align_entries = true
|
||||||
column_width = 100
|
column_width = 100
|
||||||
|
|
21
Cargo.toml
21
Cargo.toml
|
@ -1,12 +1,17 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "watt"
|
members = [ "watt" ]
|
||||||
description = "Modern CPU frequency and power management utility for Linux"
|
resolver = "3"
|
||||||
version = "0.4.0"
|
|
||||||
edition = "2024"
|
|
||||||
authors = [ "NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>" ]
|
|
||||||
rust-version = "1.85"
|
|
||||||
|
|
||||||
[dependencies]
|
[workspace.package]
|
||||||
|
authors = [ "NotAShelf <raf@notashelf.dev>", "RGBCube <git@rgbcu.be>" ]
|
||||||
|
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"
|
||||||
|
|
||||||
|
[workspace.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"
|
||||||
|
|
57
build.rs
57
build.rs
|
@ -1,57 +0,0 @@
|
||||||
use std::{
|
|
||||||
env,
|
|
||||||
fs,
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MULTICALL_NAMES: &[&str] = &["cpu", "power"];
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
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/<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(())
|
|
||||||
}
|
|
28
watt/Cargo.toml
Normal file
28
watt/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[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
|
|
@ -523,7 +523,7 @@ pub struct DaemonConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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<Self> {
|
pub fn load_from(path: Option<&Path>) -> anyhow::Result<Self> {
|
||||||
let contents = if let Some(path) = path {
|
let contents = if let Some(path) = path {
|
|
@ -5,28 +5,27 @@
|
||||||
|
|
||||||
# Emergency thermal protection (highest priority).
|
# Emergency thermal protection (highest priority).
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
if = { value = "$cpu-temperature", is-more-than = 85.0 }
|
||||||
|
priority = 100
|
||||||
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.governor = "powersave"
|
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]]
|
||||||
|
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.3 } ]
|
||||||
|
priority = 90
|
||||||
|
|
||||||
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.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]]
|
||||||
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 },
|
||||||
|
@ -34,12 +33,12 @@ if.all = [
|
||||||
]
|
]
|
||||||
priority = 80
|
priority = 80
|
||||||
|
|
||||||
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.governor = "performance"
|
||||||
|
cpu.turbo = true
|
||||||
|
|
||||||
# Performance mode when not discharging.
|
# Performance mode when not discharging.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
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 },
|
||||||
|
@ -47,56 +46,66 @@ if.all = [
|
||||||
]
|
]
|
||||||
priority = 70
|
priority = 70
|
||||||
|
|
||||||
|
cpu.energy-performance-bias = "balance_performance"
|
||||||
|
cpu.energy-performance-preference = "performance"
|
||||||
|
cpu.governor = "performance"
|
||||||
|
cpu.turbo = true
|
||||||
|
|
||||||
# Moderate performance for medium load.
|
# Moderate performance for medium load.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
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 },
|
||||||
]
|
]
|
||||||
priority = 60
|
priority = 60
|
||||||
|
|
||||||
|
cpu.energy-performance-preference = "balance_performance"
|
||||||
|
cpu.governor = "schedutil"
|
||||||
|
|
||||||
# Power saving during low activity.
|
# Power saving during low activity.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
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 },
|
||||||
]
|
]
|
||||||
priority = 50
|
priority = 50
|
||||||
|
|
||||||
|
cpu.energy-performance-preference = "power"
|
||||||
|
cpu.governor = "powersave"
|
||||||
|
cpu.turbo = false
|
||||||
|
|
||||||
# Extended idle power optimization.
|
# Extended idle power optimization.
|
||||||
[[rule]]
|
[[rule]]
|
||||||
|
if = { value = "$cpu-idle-seconds", is-more-than = 300.0 }
|
||||||
|
priority = 40
|
||||||
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 1600
|
cpu.frequency-mhz-maximum = 1600
|
||||||
cpu.governor = "powersave"
|
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]]
|
||||||
|
if.all = [ "?discharging", { value = "%power-supply-charge", is-less-than = 0.5 } ]
|
||||||
|
priority = 30
|
||||||
|
|
||||||
cpu.energy-performance-preference = "power"
|
cpu.energy-performance-preference = "power"
|
||||||
cpu.frequency-mhz-maximum = 2000
|
cpu.frequency-mhz-maximum = 2000
|
||||||
cpu.governor = "powersave"
|
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]]
|
||||||
|
if = "?discharging"
|
||||||
|
priority = 20
|
||||||
|
|
||||||
cpu.energy-performance-bias = "balance_power"
|
cpu.energy-performance-bias = "balance_power"
|
||||||
cpu.energy-performance-preference = "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.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]]
|
|
@ -1,37 +1,27 @@
|
||||||
mod cpu;
|
use std::path::PathBuf;
|
||||||
mod power_supply;
|
|
||||||
mod system;
|
|
||||||
|
|
||||||
mod fs;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
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 _;
|
use clap::Parser as _;
|
||||||
use yansi::Paint as _;
|
|
||||||
|
pub mod cpu;
|
||||||
|
pub mod power_supply;
|
||||||
|
pub mod system;
|
||||||
|
|
||||||
|
pub mod fs;
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod daemon;
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[clap(author, version, about)]
|
#[clap(author, version, about)]
|
||||||
struct Cli {
|
pub struct Cli {
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[clap(multicall = true)]
|
#[clap(multicall = true)]
|
||||||
enum Command {
|
pub enum Command {
|
||||||
/// Watt daemon.
|
/// Watt daemon.
|
||||||
Watt {
|
Watt {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
|
@ -62,18 +52,18 @@ enum Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
enum CpuCommand {
|
pub enum CpuCommand {
|
||||||
/// Modify CPU attributes.
|
/// Modify CPU attributes.
|
||||||
Set(config::CpuDelta),
|
Set(config::CpuDelta),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
enum PowerCommand {
|
pub enum PowerCommand {
|
||||||
/// Modify power supply attributes.
|
/// Modify power supply attributes.
|
||||||
Set(config::PowerDelta),
|
Set(config::PowerDelta),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn real_main() -> anyhow::Result<()> {
|
pub fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
yansi::whenever(yansi::Condition::TTY_AND_COLOR);
|
yansi::whenever(yansi::Condition::TTY_AND_COLOR);
|
||||||
|
@ -107,52 +97,3 @@ fn real_main() -> anyhow::Result<()> {
|
||||||
} => delta.apply(),
|
} => 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);
|
|
||||||
}
|
|
57
watt/main.rs
Normal file
57
watt/main.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -227,10 +227,13 @@ impl PowerSupply {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check for model name that indicates a peripheral
|
// Check for model name that indicates a peripheral
|
||||||
if let Some(model) = fs::read(self.path.join("model_name"))
|
if let Some(model_name) = fs::read(self.path.join("model_name"))
|
||||||
.with_context(|| format!("failed to read the model name of {self}"))?
|
.with_context(|| format!("failed to read the model name of {self}"))?
|
||||||
{
|
{
|
||||||
if model.contains("bluetooth") || model.contains("wireless") {
|
let model_name_lower = model_name.to_lowercase();
|
||||||
|
if model_name_lower.contains("bluetooth")
|
||||||
|
|| model_name_lower.contains("wireless")
|
||||||
|
{
|
||||||
break 'is_from_peripheral true;
|
break 'is_from_peripheral true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -295,8 +295,8 @@ impl System {
|
||||||
"stripped content is a valid number, taking it as the core number"
|
"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) =
|
let Some(temperature_mc) =
|
||||||
fs::read_n::<i64>(&input_path).with_context(|| {
|
fs::read_n::<i64>(&input_path).with_context(|| {
|
Loading…
Add table
Add a link
Reference in a new issue