use std::{
fmt, fs, io,
ops::Range,
path::{self, PathBuf},
str::FromStr,
};
use clap::{Parser, Subcommand};
use crate::{fan::Thresholds, probe::Temp};
pub(crate) type Percentage = u8;
#[derive(Clone, Debug)]
pub(crate) struct PwmSettings {
pub filepath: PathBuf,
pub thresholds: Thresholds,
}
impl FromStr for PwmSettings {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens = s.rsplitn(3, ':');
let stop = tokens
.next()
.ok_or("Missing stop value")?
.parse()
.map_err(|_| "Invalid stop value")?;
let start = tokens
.next()
.ok_or("Missing start value")?
.parse()
.map_err(|_| "Invalid start value")?;
let filepath = tokens.next().ok_or("Missing filepath")?.into();
Ok(Self {
filepath,
thresholds: Thresholds {
min_start: start,
max_stop: stop,
},
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct HwmonSettings {
pub filepath: PathBuf,
pub temp: Option<Range<Temp>>,
}
impl FromStr for HwmonSettings {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens = s.splitn(3, ':');
let filepath = tokens.next().ok_or("Missing filepath")?.into();
let start = tokens
.next()
.map(str::parse)
.transpose()
.map_err(|_| "Invalid min speed temp value")?;
let end = tokens
.next()
.map(str::parse)
.transpose()
.map_err(|_| "Invalid max speed temp value")?;
Ok(Self {
filepath,
temp: if let (Some(start), Some(end)) = (start, end) {
Some(Range { start, end })
} else {
None
},
})
}
}
fn percentage(s: &str) -> Result<u8, String> {
clap_num::number_range(s, 0, 100)
}
#[derive(Parser, Debug)]
#[command(version, about)]
pub(crate) struct Args {
#[arg(short, default_value_t = log::Level::Info)]
pub verbosity: log::Level,
#[command(subcommand)]
pub command: Command,
}
#[derive(Clone, Debug)]
pub(crate) enum DriveSelector {
Interface(String),
DrivePath(PathBuf),
}
impl DriveSelector {
pub(crate) fn to_drive_paths(&self) -> io::Result<Vec<PathBuf>> {
match self {
DriveSelector::Interface(itf) => {
let drives = fs::read_dir("/dev/disk/by-id")?.collect::<io::Result<Vec<_>>>()?;
let prefix = format!("{itf}-");
Ok(drives
.into_iter()
.map(|e| e.path())
.filter(|p| {
p.file_name().and_then(|f| f.to_str()).is_some_and(|f| {
f.starts_with(&prefix)
&& !f.trim_end_matches(char::is_numeric).ends_with("-part")
})
})
.collect())
}
DriveSelector::DrivePath(p) => Ok(vec![p.to_owned()]),
}
}
}
impl fmt::Display for DriveSelector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DriveSelector::Interface(itf) => write!(f, "{itf}"),
DriveSelector::DrivePath(p) => write!(f, "{p:?}"),
}
}
}
impl FromStr for DriveSelector {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.contains(path::MAIN_SEPARATOR) {
Ok(Self::DrivePath(s.into()))
} else {
Ok(Self::Interface(s.to_owned()))
}
}
}
#[expect(clippy::large_enum_variant)]
#[derive(Subcommand, Debug)]
pub(crate) enum Command {
Daemon {
#[arg(short, long, num_args = 1.., required = true)]
drives: Vec<DriveSelector>,
#[arg(short, long, num_args = 1.., required = true)]
pwm: Vec<PwmSettings>,
#[arg(short = 't', long, value_name = "TEMP", num_args = 2, default_values_t = vec![30.0, 50.0])]
drive_temp_range: Vec<Temp>,
#[arg(short, long, default_value_t = 20, value_parser=percentage)]
min_fan_speed_prct: Percentage,
#[arg(short, long, default_value = "20s")]
interval: humantime::Duration,
#[arg(short = 'w', long)]
hwmons: Vec<HwmonSettings>,
#[arg(long, default_value_t = 7634)]
hddtemp_daemon_port: u16,
#[arg(short, long)]
restore_fan_settings: bool,
},
PwmTest {
#[arg(short, long, num_args = 1.., required = true)]
pwm: Vec<PathBuf>,
},
}