From 618f1cd9b5d9172b789916f39594cf997c8b8fa2 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 9 Jan 2024 21:04:44 +0000 Subject: [PATCH 01/28] config: Initial support for config file This is still an experiment, the final feature might look very different. Loading/parsing a config file is trivial; the hard part is merging the different sources (cli > config > default), giving clear error feedback (should be as good as cli error feedback), and keeping boiler-plate and performance in check. I've gone thru a few iterations to reach this MVP, there are a few complexities left down the road, but this seems like a good snapshot to have in git histroy. --- Cargo.lock | 75 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + emlop.toml | 11 ++++++ src/cli.rs | 34 +++++++++++------- src/commands.rs | 19 +++++----- src/config.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++++ src/datetime.rs | 14 +++++--- src/main.rs | 24 +++++++------ tests/commands.rs | 1 + 9 files changed, 233 insertions(+), 38 deletions(-) create mode 100644 emlop.toml create mode 100644 src/config.rs diff --git a/Cargo.lock b/Cargo.lock index e7fcd2d..b3806ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,7 @@ dependencies = [ "serde", "serde_json", "time", + "toml", ] [[package]] @@ -258,6 +259,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "flate2" version = "1.0.28" @@ -268,12 +275,28 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itertools" version = "0.11.0" @@ -461,6 +484,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "strsim" version = "0.10.0" @@ -535,6 +567,40 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -621,3 +687,12 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index 08db2a4..3a5b6d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ rev_lines = "0.3.0" serde = { version = "1.0.184", features = ["derive"] } serde_json = "1.0.89" time = {version = "~0.3.18", features = ["parsing", "formatting", "local-offset", "macros"]} +toml = "0.8.8" [dev-dependencies] assert_cmd = "~2.0.0" diff --git a/emlop.toml b/emlop.toml new file mode 100644 index 0000000..c0aeeae --- /dev/null +++ b/emlop.toml @@ -0,0 +1,11 @@ +# This is an example emlop config file. +# +# To use it, copy to $HOME/.config/emlop.toml (or to whatever you set $EMLOP_CONFIG to) and +# uncomment the desired lines. +# +# All config items have a corresponding command-line arg, see `emlop --help` for +# detailed format and behavior. Not all command-line args have a config item, this file lists all +# the supported items. + +######################################## +# date = "rfc2822" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index cccf839..e911828 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -194,20 +194,17 @@ pub fn build_cli_nocomplete() -> Command { .long("date") .display_order(2) .global(true) - .value_parser(value_parser!(crate::datetime::DateStyle)) - .hide_possible_values(true) - .default_value("ymdhms") .display_order(52) .help_heading("Format") .help("Output dates in different formats") .long_help("Output dates in different formats\n \ - ymd|d: 2022-01-31\n \ - ymdhms|dt: 2022-01-31 08:59:46\n \ - ymdhmso|dto: 2022-01-31 08:59:46 +00:00\n \ - rfc3339|3339: 2022-01-31T08:59:46+00:00\n \ - rfc2822|2822: Mon, 31 Jan 2022 08:59:46 +00:00\n \ - compact: 20220131085946\n \ - unix: 1643619586"); + ymd|d: 2022-01-31\n \ + (default)|ymdhms|dt: 2022-01-31 08:59:46\n \ + ymdhmso|dto: 2022-01-31 08:59:46 +00:00\n \ + rfc3339|3339: 2022-01-31T08:59:46+00:00\n \ + rfc2822|2822: Mon, 31 Jan 2022 08:59:46 +00:00\n \ + compact: 20220131085946\n \ + unix: 1643619586"); let duration = Arg::new("duration").value_name("format") .long("duration") .display_order(3) @@ -311,6 +308,17 @@ pub fn build_cli_nocomplete() -> Command { -v: show warnings\n \ -vv: show info\n \ -vvv: show debug"); + let h = "Location of emlop config file\n\ + Default is $HOME/.config/emlop.toml (or $EMLOP_CONFIG if set)\n\ + Set to an empty string to disable\n\ + Config in in TOML format, see example file in /usr/share/doc/emlop-x.y.z/"; + let config = Arg::new("config").value_name("file") + .long("config") + .global(true) + .num_args(1) + .display_order(5) + .help(h.split_once('\n').unwrap().0) + .long_help(h); //////////////////////////////////////////////////////////// // Subcommands @@ -370,9 +378,8 @@ pub fn build_cli_nocomplete() -> Command { https://github.com/vincentdephily/emlop"; let after_help = "Subcommands and long args can be abbreviated (eg `emlop l -ss --head -f1w`)\n\ - Subcommands have their own -h / --help\n\ - Exit code is 0 if sucessful, 1 if search found nothing, 2 in case of \ - other errors"; + Subcommands have their own -h / --help\n\ + Exit code is 0 if sucessful, 1 if search found nothing, 2 in case of other errors"; let styles = styling::Styles::styled().header(styling::AnsiColor::Blue.on_default() | styling::Effects::BOLD) @@ -398,6 +405,7 @@ pub fn build_cli_nocomplete() -> Command { .arg(color) .arg(output) .arg(logfile) + .arg(config) .arg(verbose) .subcommand(cmd_log) .subcommand(cmd_pred) diff --git a/src/commands.rs b/src/commands.rs index 05bb45c..0a53dbb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,8 +6,8 @@ use std::{collections::{BTreeMap, HashMap}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_list(args: &ArgMatches) -> Result { - let st = &Styles::from_args(args); +pub fn cmd_log(args: &ArgMatches, gconf: ConfigAll, sconf: ConfigLog) -> Result { + let st = &Styles::from_args(args, gconf); let show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, st.date_offset)?, @@ -15,7 +15,6 @@ pub fn cmd_list(args: &ArgMatches) -> Result { show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; - let first = *args.get_one("first").unwrap_or(&usize::MAX); let last = *args.get_one("last").unwrap_or(&usize::MAX); let stt = args.get_flag("starttime"); let mut merges: HashMap = HashMap::new(); @@ -63,7 +62,7 @@ pub fn cmd_list(args: &ArgMatches) -> Result { } }, } - if found >= first { + if found >= sconf.first { break; } } @@ -144,8 +143,8 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(args: &ArgMatches) -> Result { - let st = &Styles::from_args(args); +pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll) -> Result { + let st = &Styles::from_args(args, conf); let show = *args.get_one("show").unwrap(); let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), @@ -311,8 +310,8 @@ fn cmd_stats_group(tbls: &mut Table<5>, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches) -> Result { - let st = &Styles::from_args(args); +pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll) -> Result { + let st = &Styles::from_args(args, conf); let now = epoch_now(); let show: Show = *args.get_one("show").unwrap(); let first = *args.get_one("first").unwrap_or(&usize::MAX); @@ -451,8 +450,8 @@ pub fn cmd_predict(args: &ArgMatches) -> Result { Ok(totcount > 0) } -pub fn cmd_accuracy(args: &ArgMatches) -> Result { - let st = &Styles::from_args(args); +pub fn cmd_accuracy(args: &ArgMatches, conf: ConfigAll) -> Result { + let st = &Styles::from_args(args, conf); let show: Show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, st.date_offset)?, diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..c50a28f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,92 @@ +use crate::DateStyle; +use anyhow::{Context, Error}; +use clap::{error::{ContextKind, ContextValue, Error as ClapError}, + ArgMatches}; +use serde::Deserialize; +use std::{env::var, fs::File, io::Read}; + +#[derive(Deserialize, Debug, Default)] +pub struct Toml { + date: Option, +} +impl Toml { + pub fn load(arg: Option<&String>, env: Option) -> Result { + match arg.or(env.as_ref()) { + Some(s) if s.is_empty() => Ok(Self::default()), + Some(s) => Self::doload(s.as_str()), + _ => Self::doload(&format!("{}/.config/emlop.toml", + var("HOME").unwrap_or("".to_string()))), + } + } + fn doload(name: &str) -> Result { + log::trace!("Loading config {name:?}"); + let mut f = File::open(name).with_context(|| format!("Cannot open {name:?}"))?; + let mut buf = String::new(); + // TODO Streaming read + f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; + Ok(toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}"))?) + } +} + +fn err_src(mut err: ClapError, src: String) -> ClapError { + err.insert(ContextKind::InvalidArg, ContextValue::String(src)); + err +} +fn select(arg: Option<&String>, + argsrc: &'static str, + toml: &Option, + tomlsrc: &'static str, + def: &'static str) + -> Result + where T: for<'a> TryFrom<&'a str, Error = ClapError> +{ + if let Some(a) = arg { + T::try_from(a.as_str()).map_err(|e| err_src(e, format!("{argsrc} (argument)"))) + } else if let Some(a) = toml { + T::try_from(a.as_str()).map_err(|e| err_src(e, format!("{tomlsrc} (config)"))) + } else { + Ok(T::try_from(def).expect("default value")) + } +} + +pub enum Config<'a> { + Log(&'a ArgMatches, ConfigAll, ConfigLog), + Stats(&'a ArgMatches, ConfigAll), + Predict(&'a ArgMatches, ConfigAll), + Accuracy(&'a ArgMatches, ConfigAll), + Complete(&'a ArgMatches), +} +pub struct ConfigAll { + pub date: DateStyle, +} +pub struct ConfigLog { + pub first: usize, +} + +impl<'a> Config<'a> { + pub fn try_new(args: &'a ArgMatches) -> Result { + let toml = Toml::load(args.get_one::("config"), var("EMLOP_CONFIG").ok())?; + log::trace!("{:?}", toml); + let conf = ConfigAll::try_new(&args, &toml)?; + Ok(match args.subcommand() { + Some(("log", sub)) => Self::Log(sub, conf, ConfigLog::try_new(sub, &toml)?), + Some(("stats", sub)) => Self::Stats(sub, conf), + Some(("predict", sub)) => Self::Predict(sub, conf), + Some(("accuracy", sub)) => Self::Accuracy(sub, conf), + Some(("complete", sub)) => Self::Complete(sub), + _ => unreachable!("clap should have exited already"), + }) + } +} + +impl ConfigAll { + pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { date: select(args.get_one("date"), "--date", &toml.date, "date", "ymdhms")? }) + } +} + +impl ConfigLog { + pub fn try_new(args: &ArgMatches, _toml: &Toml) -> Result { + Ok(Self { first: *args.get_one("first").unwrap_or(&usize::MAX) }) + } +} diff --git a/src/datetime.rs b/src/datetime.rs index 4407e3d..b55864c 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,5 +1,6 @@ use crate::{table::Disp, wtb, DurationStyle, Styles}; use anyhow::{bail, Error}; +use clap::error::{ContextKind, ContextValue, ErrorKind}; use log::{debug, warn}; use regex::Regex; use std::{convert::TryFrom, @@ -26,9 +27,9 @@ pub fn get_offset(utc: bool) -> UtcOffset { // See #[derive(Clone, Copy)] pub struct DateStyle(&'static [time::format_description::FormatItem<'static>]); -impl FromStr for DateStyle { - type Err = &'static str; - fn from_str(s: &str) -> Result { +impl TryFrom<&str> for DateStyle { + type Error = clap::error::Error; + fn try_from(s: &str) -> Result { let fmt = match s { "ymd" | "d" => format_description!("[year]-[month]-[day]"), "ymdhms" | "dt" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), @@ -37,7 +38,12 @@ impl FromStr for DateStyle { "rfc2822" | "2822" => format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]"), "compact" => format_description!("[year][month][day][hour][minute][second]"), "unix" => &[], - _ => return Err("Valid values are ymd, d, ymdhms, dt, ymdhmso, dto, rfc3339, 3339, rfc2822, 2822, compact, unix"), + _ =>{ + let mut err = clap::Error::new(ErrorKind::InvalidValue); + err.insert(ContextKind::InvalidValue, ContextValue::String(s.to_owned())); + err.insert(ContextKind::ValidValue, ContextValue::Strings("ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix".split_ascii_whitespace().map(|s|s.to_string()).collect())); + return Err(err) + } }; Ok(Self(fmt)) } diff --git a/src/main.rs b/src/main.rs index f00eeef..aeadae8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,13 @@ mod cli; mod commands; +mod config; mod datetime; mod parse; mod proces; mod table; -use crate::{commands::*, datetime::*, parse::AnsiStr}; +use crate::{commands::*, config::*, datetime::*, parse::AnsiStr}; use anyhow::Error; use clap::{error::ErrorKind, ArgMatches, Error as ClapErr}; use log::*; @@ -24,13 +25,14 @@ fn main() { }; env_logger::Builder::new().filter_level(level).format_timestamp(None).init(); trace!("{:?}", args); - let res = match args.subcommand() { - Some(("log", sub_args)) => cmd_list(sub_args), - Some(("stats", sub_args)) => cmd_stats(sub_args), - Some(("predict", sub_args)) => cmd_predict(sub_args), - Some(("accuracy", sub_args)) => cmd_accuracy(sub_args), - Some(("complete", sub_args)) => cmd_complete(sub_args), - _ => unreachable!("clap should have exited already"), + + let res = match Config::try_new(&args) { + Ok(Config::Log(subargs, conf, subconf)) => cmd_log(&subargs, conf, subconf), + Ok(Config::Stats(subargs, conf)) => cmd_stats(&subargs, conf), + Ok(Config::Predict(subargs, conf)) => cmd_predict(&subargs, conf), + Ok(Config::Accuracy(subargs, conf)) => cmd_accuracy(&subargs, conf), + Ok(Config::Complete(subargs)) => cmd_complete(&subargs), + Err(e) => Err(e), }; match res { Ok(true) => std::process::exit(0), @@ -191,7 +193,7 @@ pub struct Styles { out: OutStyle, } impl Styles { - fn from_args(args: &ArgMatches) -> Self { + fn from_args(args: &ArgMatches, conf: ConfigAll) -> Self { let isterm = std::io::stdout().is_terminal(); let color = match args.get_one("color") { Some(ColorStyle::Always) => true, @@ -205,7 +207,7 @@ impl Styles { }; let header = args.get_flag("header"); let dur_t = *args.get_one("duration").unwrap(); - let date_fmt = *args.get_one("date").unwrap(); + let date_fmt = conf.date; let date_offset = get_offset(args.get_flag("utc")); Styles { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), @@ -223,6 +225,6 @@ impl Styles { #[cfg(test)] fn from_str(s: impl AsRef) -> Self { let args = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); - Self::from_args(&args) + Self::from_args(&args, ConfigAll::try_new(&args, &Toml::default()).unwrap()) } } diff --git a/tests/commands.rs b/tests/commands.rs index 632f455..1d88ce8 100644 --- a/tests/commands.rs +++ b/tests/commands.rs @@ -20,6 +20,7 @@ fn ts(secs: i64) -> i64 { fn emlop(args: &str) -> Command { let mut e = Command::new(env!("CARGO_BIN_EXE_emlop")); e.env("TZ", "UTC"); + e.env("EMLOP_CONFIG", ""); e.args(args.replace("%F", "-F tests/emerge.").split_whitespace()); e } From 37da686cd75a1cf6be72cb0a15d8f185a89fdc6a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 12 Jan 2024 21:16:15 +0000 Subject: [PATCH 02/28] conf: Add `starttime` config item, introduce `ArgParse` trait Just one extra boolean, but still an interesting case: * It's in a subsection of the toml file * Needs to be converted to a option-with-arg in the cli (so that the cli can override the config, and to make the "no cli value given" case more straightforward) The ArgParse trait simplifies returning a detailed error, and writing the `sel()` helper. --- emlop.toml | 5 +++- src/cli.rs | 4 ++- src/commands.rs | 2 +- src/config.rs | 66 +++++++++++++++++++++++++++++++++++++------------ src/datetime.rs | 24 +++++++++--------- 5 files changed, 70 insertions(+), 31 deletions(-) diff --git a/emlop.toml b/emlop.toml index c0aeeae..8bf0c15 100644 --- a/emlop.toml +++ b/emlop.toml @@ -8,4 +8,7 @@ # the supported items. ######################################## -# date = "rfc2822" \ No newline at end of file +# date = "rfc2822" + +[log] +#starttime = true diff --git a/src/cli.rs b/src/cli.rs index e911828..4526725 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -227,7 +227,9 @@ pub fn build_cli_nocomplete() -> Command { .help_heading("Format") .help("Parse/display dates in UTC instead of local time"); let starttime = Arg::new("starttime").long("starttime") - .action(SetTrue) + .num_args(..=1) + .default_missing_value("y") + .value_name("bool") .display_order(5) .help_heading("Format") .help("Display start time instead of end time"); diff --git a/src/commands.rs b/src/commands.rs index 0a53dbb..64ebc1c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -16,7 +16,7 @@ pub fn cmd_log(args: &ArgMatches, gconf: ConfigAll, sconf: ConfigLog) -> Result< args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let last = *args.get_one("last").unwrap_or(&usize::MAX); - let stt = args.get_flag("starttime"); + let stt = sconf.starttime; let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); let mut found = 0; diff --git a/src/config.rs b/src/config.rs index c50a28f..1067052 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,12 +1,17 @@ use crate::DateStyle; use anyhow::{Context, Error}; -use clap::{error::{ContextKind, ContextValue, Error as ClapError}, +use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, ArgMatches}; use serde::Deserialize; use std::{env::var, fs::File, io::Read}; +#[derive(Deserialize, Debug, Default, Clone, Copy)] +pub struct TomlLog { + starttime: Option, +} #[derive(Deserialize, Debug, Default)] pub struct Toml { + log: Option, date: Option, } impl Toml { @@ -28,24 +33,48 @@ impl Toml { } } -fn err_src(mut err: ClapError, src: String) -> ClapError { - err.insert(ContextKind::InvalidArg, ContextValue::String(src)); +pub fn err(val: String, src: &'static str, possible: &'static str) -> ClapError { + let mut err = clap::Error::new(ErrorKind::InvalidValue); + err.insert(ContextKind::InvalidValue, ContextValue::String(val)); + let p = possible.split_ascii_whitespace().map(|s| s.to_string()).collect(); + err.insert(ContextKind::ValidValue, ContextValue::Strings(p)); + err.insert(ContextKind::InvalidArg, ContextValue::String(src.to_string())); err } -fn select(arg: Option<&String>, - argsrc: &'static str, - toml: &Option, - tomlsrc: &'static str, - def: &'static str) - -> Result - where T: for<'a> TryFrom<&'a str, Error = ClapError> + +pub trait ArgParse { + fn parse(val: &T, src: &'static str) -> Result + where Self: Sized; +} +impl ArgParse for bool { + fn parse(b: &bool, _src: &'static str) -> Result { + Ok(*b) + } +} +impl ArgParse for bool { + fn parse(s: &String, src: &'static str) -> Result { + match s.as_str() { + "y" | "yes" => Ok(true), + "n" | "no" => Ok(false), + _ => Err(err(s.to_owned(), src, "y(es) n(o)")), + } + } +} + +// TODO nicer way to specify src +fn sel(arg: Option<&A>, + argsrc: &'static str, + toml: &Option, + tomlsrc: &'static str) + -> Result + where T: ArgParse + ArgParse + Default { if let Some(a) = arg { - T::try_from(a.as_str()).map_err(|e| err_src(e, format!("{argsrc} (argument)"))) + T::parse(a, argsrc) } else if let Some(a) = toml { - T::try_from(a.as_str()).map_err(|e| err_src(e, format!("{tomlsrc} (config)"))) + T::parse(a, tomlsrc) } else { - Ok(T::try_from(def).expect("default value")) + Ok(T::default()) } } @@ -60,6 +89,7 @@ pub struct ConfigAll { pub date: DateStyle, } pub struct ConfigLog { + pub starttime: bool, pub first: usize, } @@ -81,12 +111,16 @@ impl<'a> Config<'a> { impl ConfigAll { pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { date: select(args.get_one("date"), "--date", &toml.date, "date", "ymdhms")? }) + Ok(Self { date: sel(args.get_one("date"), "--date", &toml.date, "date")? }) } } impl ConfigLog { - pub fn try_new(args: &ArgMatches, _toml: &Toml) -> Result { - Ok(Self { first: *args.get_one("first").unwrap_or(&usize::MAX) }) + pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { starttime: sel(args.get_one::("starttime"), + "--starttime", + &toml.log.and_then(|l| l.starttime), + "[log] starttime")?, + first: *args.get_one("first").unwrap_or(&usize::MAX) }) } } diff --git a/src/datetime.rs b/src/datetime.rs index b55864c..a7b6c27 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,6 +1,7 @@ -use crate::{table::Disp, wtb, DurationStyle, Styles}; +use crate::{config::{err, ArgParse}, + table::Disp, + wtb, DurationStyle, Styles}; use anyhow::{bail, Error}; -use clap::error::{ContextKind, ContextValue, ErrorKind}; use log::{debug, warn}; use regex::Regex; use std::{convert::TryFrom, @@ -27,10 +28,14 @@ pub fn get_offset(utc: bool) -> UtcOffset { // See #[derive(Clone, Copy)] pub struct DateStyle(&'static [time::format_description::FormatItem<'static>]); -impl TryFrom<&str> for DateStyle { - type Error = clap::error::Error; - fn try_from(s: &str) -> Result { - let fmt = match s { +impl Default for DateStyle { + fn default() -> Self { + Self(format_description!("[year]-[month]-[day] [hour]:[minute]:[second]")) + } +} +impl ArgParse for DateStyle { + fn parse(s: &String, src: &'static str) -> Result { + let fmt = match s.as_str() { "ymd" | "d" => format_description!("[year]-[month]-[day]"), "ymdhms" | "dt" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), "ymdhmso" | "dto" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]"), @@ -38,12 +43,7 @@ impl TryFrom<&str> for DateStyle { "rfc2822" | "2822" => format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]"), "compact" => format_description!("[year][month][day][hour][minute][second]"), "unix" => &[], - _ =>{ - let mut err = clap::Error::new(ErrorKind::InvalidValue); - err.insert(ContextKind::InvalidValue, ContextValue::String(s.to_owned())); - err.insert(ContextKind::ValidValue, ContextValue::Strings("ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix".split_ascii_whitespace().map(|s|s.to_string()).collect())); - return Err(err) - } + _ => return Err(err(s.to_owned(), src, "ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix")) }; Ok(Self(fmt)) } From 06665ea2511f4ccdcacfc9125dc22c77e8332d4d Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sun, 14 Jan 2024 22:44:12 +0000 Subject: [PATCH 03/28] conf: Add `average` config item And some code cleanups. Things are still messy at this stage, but should clear up once we finish moving all items. --- emlop.toml | 11 +++- src/cli.rs | 4 +- src/commands.rs | 20 +++---- src/config.rs | 135 +++++++++++++++++++++++++++++++++++++----------- src/main.rs | 21 ++------ 5 files changed, 129 insertions(+), 62 deletions(-) diff --git a/emlop.toml b/emlop.toml index 8bf0c15..314e4d8 100644 --- a/emlop.toml +++ b/emlop.toml @@ -1,3 +1,4 @@ +######################################## # This is an example emlop config file. # # To use it, copy to $HOME/.config/emlop.toml (or to whatever you set $EMLOP_CONFIG to) and @@ -6,9 +7,15 @@ # All config items have a corresponding command-line arg, see `emlop --help` for # detailed format and behavior. Not all command-line args have a config item, this file lists all # the supported items. - ######################################## -# date = "rfc2822" + +# date = "rfc2822" [log] #starttime = true +[predict] +#average = "arith" +[stats] +#average = "arith" +[accuracy] +#average = "arith" diff --git a/src/cli.rs b/src/cli.rs index 4526725..951de9e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -159,14 +159,12 @@ pub fn build_cli_nocomplete() -> Command { Arg::new("avg").long("avg") .value_name("fn") .display_order(3) - .value_parser(value_parser!(crate::Average)) .hide_possible_values(true) - .default_value("median") .help_heading("Stats") .help("Select function used to predict durations") .long_help("Select function used to predict durations\n \ arith|a: simple 'sum/count' average\n \ - median|m: middle value, mitigates outliers\n \ + (defaut)|median|m: middle value, mitigates outliers\n \ weighted-arith|wa: 'sum/count' with more weight for recent values\n \ weighted-median|wm: \"middle\" value shifted toward recent values"); diff --git a/src/commands.rs b/src/commands.rs index 64ebc1c..0452b1c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -143,7 +143,7 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll) -> Result { +pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll, sconf: ConfigStats) -> Result { let st = &Styles::from_args(args, conf); let show = *args.get_one("show").unwrap(); let timespan_opt: Option<&Timespan> = args.get_one("group"); @@ -154,7 +154,6 @@ pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll) -> Result { args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let lim = *args.get_one("limit").unwrap(); - let avg = *args.get_one("avg").unwrap(); let tsname = timespan_opt.map_or("", |timespan| timespan.name()); let mut tbls = Table::new(st).align_left(0).align_left(1).margin(1, " "); tbls.header([tsname, "Repo", "Sync count", "Total time", "Predict time"]); @@ -190,7 +189,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll) -> Result { curts = t; } else if t > nextts { let group = timespan.at(curts, st.date_offset); - cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, st, lim, avg, show, group, + cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, st, lim, sconf.avg, show, group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); @@ -234,7 +233,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll) -> Result { } } let group = timespan_opt.map(|timespan| timespan.at(curts, st.date_offset)).unwrap_or_default(); - cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, st, lim, avg, show, group, &sync_time, + cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, st, lim, sconf.avg, show, group, &sync_time, &pkg_time); // Controlled drop to ensure table order and insert blank lines let (es, ep, et) = (!tbls.is_empty(), !tblp.is_empty(), !tblt.is_empty()); @@ -310,7 +309,7 @@ fn cmd_stats_group(tbls: &mut Table<5>, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll) -> Result { +pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll, sconf: ConfigPred) -> Result { let st = &Styles::from_args(args, conf); let now = epoch_now(); let show: Show = *args.get_one("show").unwrap(); @@ -321,7 +320,6 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll) -> Result { None => usize::MAX, }; let lim = *args.get_one("limit").unwrap(); - let avg = *args.get_one("avg").unwrap(); let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); let mut tbl = Table::new(st).align_left(0).align_left(2).margin(2, " ").last(last); @@ -399,7 +397,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll) -> Result { // Find the predicted time and adjust counters let (fmtpred, pred) = match times.get(p.ebuild()) { Some(tv) => { - let pred = tv.pred(lim, avg); + let pred = tv.pred(lim, sconf.avg); (pred, pred) }, None => { @@ -450,7 +448,10 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll) -> Result { Ok(totcount > 0) } -pub fn cmd_accuracy(args: &ArgMatches, conf: ConfigAll) -> Result { +pub fn cmd_accuracy(args: &ArgMatches, + conf: ConfigAll, + sconf: ConfigAccuracy) + -> Result { let st = &Styles::from_args(args, conf); let show: Show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), @@ -461,7 +462,6 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfigAll) -> Result { args.get_flag("exact"))?; let last = *args.get_one("last").unwrap_or(&usize::MAX); let lim = *args.get_one("limit").unwrap(); - let avg = *args.get_one("avg").unwrap(); let mut pkg_starts: HashMap = HashMap::new(); let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); @@ -479,7 +479,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfigAll) -> Result { if let Some(start) = pkg_starts.remove(key) { let times = pkg_times.entry(p.ebuild().to_owned()).or_insert_with(Times::new); let real = ts - start; - match times.pred(lim, avg) { + match times.pred(lim, sconf.avg) { -1 => { if show.merge { tbl.row([&[&FmtDate(ts)], diff --git a/src/config.rs b/src/config.rs index 1067052..d6e49c2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,17 +5,32 @@ use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, use serde::Deserialize; use std::{env::var, fs::File, io::Read}; -#[derive(Deserialize, Debug, Default, Clone, Copy)] -pub struct TomlLog { +#[derive(Deserialize, Debug, Default)] +struct TomlLog { starttime: Option, } #[derive(Deserialize, Debug, Default)] +struct TomlPred { + average: Option, +} +#[derive(Deserialize, Debug, Default)] +struct TomlStats { + average: Option, +} +#[derive(Deserialize, Debug, Default)] +struct TomlAccuracy { + average: Option, +} +#[derive(Deserialize, Debug, Default)] pub struct Toml { - log: Option, date: Option, + log: Option, + predict: Option, + stats: Option, + accuracy: Option, } impl Toml { - pub fn load(arg: Option<&String>, env: Option) -> Result { + fn load(arg: Option<&String>, env: Option) -> Result { match arg.or(env.as_ref()) { Some(s) if s.is_empty() => Ok(Self::default()), Some(s) => Self::doload(s.as_str()), @@ -29,7 +44,7 @@ impl Toml { let mut buf = String::new(); // TODO Streaming read f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; - Ok(toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}"))?) + toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}")) } } @@ -61,28 +76,32 @@ impl ArgParse for bool { } } -// TODO nicer way to specify src -fn sel(arg: Option<&A>, - argsrc: &'static str, - toml: &Option, - tomlsrc: &'static str) - -> Result - where T: ArgParse + ArgParse + Default -{ - if let Some(a) = arg { - T::parse(a, argsrc) - } else if let Some(a) = toml { - T::parse(a, tomlsrc) - } else { - Ok(T::default()) +#[derive(Clone, Copy, Default)] +pub enum Average { + Arith, + #[default] + Median, + WeightedArith, + WeightedMedian, +} +impl ArgParse for Average { + fn parse(s: &String, src: &'static str) -> Result { + use Average::*; + match s.as_str() { + "a" | "arith" => Ok(Arith), + "m" | "median" => Ok(Median), + "wa" | "weighted-arith" => Ok(WeightedArith), + "wm" | "weighted-median" => Ok(WeightedMedian), + _ => Err(err(s.to_owned(), src, "arith median weightedarith weigtedmedian a m wa wm")), + } } } pub enum Config<'a> { Log(&'a ArgMatches, ConfigAll, ConfigLog), - Stats(&'a ArgMatches, ConfigAll), - Predict(&'a ArgMatches, ConfigAll), - Accuracy(&'a ArgMatches, ConfigAll), + Stats(&'a ArgMatches, ConfigAll, ConfigStats), + Predict(&'a ArgMatches, ConfigAll, ConfigPred), + Accuracy(&'a ArgMatches, ConfigAll, ConfigAccuracy), Complete(&'a ArgMatches), } pub struct ConfigAll { @@ -92,35 +111,89 @@ pub struct ConfigLog { pub starttime: bool, pub first: usize, } +pub struct ConfigPred { + pub avg: Average, +} +pub struct ConfigStats { + pub avg: Average, +} +pub struct ConfigAccuracy { + pub avg: Average, +} impl<'a> Config<'a> { pub fn try_new(args: &'a ArgMatches) -> Result { let toml = Toml::load(args.get_one::("config"), var("EMLOP_CONFIG").ok())?; log::trace!("{:?}", toml); - let conf = ConfigAll::try_new(&args, &toml)?; + let conf = ConfigAll::try_new(args, &toml)?; Ok(match args.subcommand() { Some(("log", sub)) => Self::Log(sub, conf, ConfigLog::try_new(sub, &toml)?), - Some(("stats", sub)) => Self::Stats(sub, conf), - Some(("predict", sub)) => Self::Predict(sub, conf), - Some(("accuracy", sub)) => Self::Accuracy(sub, conf), + Some(("stats", sub)) => Self::Stats(sub, conf, ConfigStats::try_new(sub, &toml)?), + Some(("predict", sub)) => Self::Predict(sub, conf, ConfigPred::try_new(sub, &toml)?), + Some(("accuracy", sub)) => { + Self::Accuracy(sub, conf, ConfigAccuracy::try_new(sub, &toml)?) + }, Some(("complete", sub)) => Self::Complete(sub), _ => unreachable!("clap should have exited already"), }) } } +// TODO nicer way to specify src +fn sel(args: &ArgMatches, + argsrc: &'static str, + toml: Option<&T>, + tomlsrc: &'static str) + -> Result + where R: ArgParse + ArgParse + Default +{ + if let Some(a) = args.get_one::(argsrc) { + R::parse(a, argsrc) + } else if let Some(a) = toml { + R::parse(a, tomlsrc) + } else { + Ok(R::default()) + } +} + impl ConfigAll { pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { date: sel(args.get_one("date"), "--date", &toml.date, "date")? }) + Ok(Self { date: sel(args, "date", toml.date.as_ref(), "date")? }) } } impl ConfigLog { - pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { starttime: sel(args.get_one::("starttime"), - "--starttime", - &toml.log.and_then(|l| l.starttime), + fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { starttime: sel(args, + "starttime", + toml.log.as_ref().and_then(|l| l.starttime.as_ref()), "[log] starttime")?, first: *args.get_one("first").unwrap_or(&usize::MAX) }) } } + +impl ConfigPred { + fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { avg: sel(args, + "avg", + toml.predict.as_ref().and_then(|t| t.average.as_ref()), + "[predict] average")? }) + } +} + +impl ConfigStats { + fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { avg: sel(args, + "avg", + toml.stats.as_ref().and_then(|t| t.average.as_ref()), + "[predict] average")? }) + } +} +impl ConfigAccuracy { + fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { avg: sel(args, + "avg", + toml.accuracy.as_ref().and_then(|t| t.average.as_ref()), + "[predict] average")? }) + } +} diff --git a/src/main.rs b/src/main.rs index aeadae8..165626f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,11 +27,11 @@ fn main() { trace!("{:?}", args); let res = match Config::try_new(&args) { - Ok(Config::Log(subargs, conf, subconf)) => cmd_log(&subargs, conf, subconf), - Ok(Config::Stats(subargs, conf)) => cmd_stats(&subargs, conf), - Ok(Config::Predict(subargs, conf)) => cmd_predict(&subargs, conf), - Ok(Config::Accuracy(subargs, conf)) => cmd_accuracy(&subargs, conf), - Ok(Config::Complete(subargs)) => cmd_complete(&subargs), + Ok(Config::Log(subargs, conf, subconf)) => cmd_log(subargs, conf, subconf), + Ok(Config::Stats(subargs, conf, subconf)) => cmd_stats(subargs, conf, subconf), + Ok(Config::Predict(subargs, conf, subconf)) => cmd_predict(subargs, conf, subconf), + Ok(Config::Accuracy(subargs, conf, subconf)) => cmd_accuracy(subargs, conf, subconf), + Ok(Config::Complete(subargs)) => cmd_complete(subargs), Err(e) => Err(e), }; match res { @@ -122,17 +122,6 @@ impl std::fmt::Display for Show { } } -#[derive(Clone, Copy, clap::ValueEnum)] -pub enum Average { - #[clap(alias("a"))] - Arith, - #[clap(alias("m"))] - Median, - #[clap(alias("wa"))] - WeightedArith, - #[clap(alias("wm"))] - WeightedMedian, -} #[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum ResumeKind { From 9586782fd38ea0077eb0795f986cbab0e4951404 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 15 Jan 2024 20:39:03 +0000 Subject: [PATCH 04/28] conf: Refactor --- src/commands.rs | 19 ++++++--------- src/config.rs | 65 +++++++++++++++++++++++++++++-------------------- src/main.rs | 28 +++++++-------------- 3 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0452b1c..e8d02cb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,8 +6,8 @@ use std::{collections::{BTreeMap, HashMap}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_log(args: &ArgMatches, gconf: ConfigAll, sconf: ConfigLog) -> Result { - let st = &Styles::from_args(args, gconf); +pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result { + let st = &Styles::from_args(args, &conf); let show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, st.date_offset)?, @@ -143,8 +143,8 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(args: &ArgMatches, conf: ConfigAll, sconf: ConfigStats) -> Result { - let st = &Styles::from_args(args, conf); +pub fn cmd_stats(args: &ArgMatches, conf: ConfAll, sconf: ConfStats) -> Result { + let st = &Styles::from_args(args, &conf); let show = *args.get_one("show").unwrap(); let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), @@ -309,8 +309,8 @@ fn cmd_stats_group(tbls: &mut Table<5>, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll, sconf: ConfigPred) -> Result { - let st = &Styles::from_args(args, conf); +pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result { + let st = &Styles::from_args(args, &conf); let now = epoch_now(); let show: Show = *args.get_one("show").unwrap(); let first = *args.get_one("first").unwrap_or(&usize::MAX); @@ -448,11 +448,8 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfigAll, sconf: ConfigPred) -> Res Ok(totcount > 0) } -pub fn cmd_accuracy(args: &ArgMatches, - conf: ConfigAll, - sconf: ConfigAccuracy) - -> Result { - let st = &Styles::from_args(args, conf); +pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Result { + let st = &Styles::from_args(args, &conf); let show: Show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, st.date_offset)?, diff --git a/src/config.rs b/src/config.rs index d6e49c2..390102a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use crate::DateStyle; +use crate::*; use anyhow::{Context, Error}; use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, ArgMatches}; @@ -97,43 +97,54 @@ impl ArgParse for Average { } } -pub enum Config<'a> { - Log(&'a ArgMatches, ConfigAll, ConfigLog), - Stats(&'a ArgMatches, ConfigAll, ConfigStats), - Predict(&'a ArgMatches, ConfigAll, ConfigPred), - Accuracy(&'a ArgMatches, ConfigAll, ConfigAccuracy), - Complete(&'a ArgMatches), -} -pub struct ConfigAll { +pub enum Conf { + Log(ArgMatches, ConfAll, ConfLog), + Stats(ArgMatches, ConfAll, ConfStats), + Predict(ArgMatches, ConfAll, ConfPred), + Accuracy(ArgMatches, ConfAll, ConfAccuracy), + Complete(ArgMatches), +} +pub struct ConfAll { pub date: DateStyle, } -pub struct ConfigLog { +pub struct ConfLog { pub starttime: bool, pub first: usize, } -pub struct ConfigPred { +pub struct ConfPred { pub avg: Average, } -pub struct ConfigStats { +pub struct ConfStats { pub avg: Average, } -pub struct ConfigAccuracy { +pub struct ConfAccuracy { pub avg: Average, } - -impl<'a> Config<'a> { - pub fn try_new(args: &'a ArgMatches) -> Result { +impl Conf { + pub fn try_new() -> Result { + let args = cli::build_cli().get_matches(); + let level = match args.get_count("verbose") { + 0 => LevelFilter::Error, + 1 => LevelFilter::Warn, + 2 => LevelFilter::Info, + 3 => LevelFilter::Debug, + _ => LevelFilter::Trace, + }; + env_logger::Builder::new().filter_level(level).format_timestamp(None).init(); + trace!("{:?}", args); let toml = Toml::load(args.get_one::("config"), var("EMLOP_CONFIG").ok())?; log::trace!("{:?}", toml); - let conf = ConfigAll::try_new(args, &toml)?; + let conf = ConfAll::try_new(&args, &toml)?; Ok(match args.subcommand() { - Some(("log", sub)) => Self::Log(sub, conf, ConfigLog::try_new(sub, &toml)?), - Some(("stats", sub)) => Self::Stats(sub, conf, ConfigStats::try_new(sub, &toml)?), - Some(("predict", sub)) => Self::Predict(sub, conf, ConfigPred::try_new(sub, &toml)?), + Some(("log", sub)) => Self::Log(sub.clone(), conf, ConfLog::try_new(sub, &toml)?), + Some(("stats", sub)) => Self::Stats(sub.clone(), conf, ConfStats::try_new(sub, &toml)?), + Some(("predict", sub)) => { + Self::Predict(sub.clone(), conf, ConfPred::try_new(sub, &toml)?) + }, Some(("accuracy", sub)) => { - Self::Accuracy(sub, conf, ConfigAccuracy::try_new(sub, &toml)?) + Self::Accuracy(sub.clone(), conf, ConfAccuracy::try_new(sub, &toml)?) }, - Some(("complete", sub)) => Self::Complete(sub), + Some(("complete", sub)) => Self::Complete(sub.clone()), _ => unreachable!("clap should have exited already"), }) } @@ -156,13 +167,13 @@ fn sel(args: &ArgMatches, } } -impl ConfigAll { +impl ConfAll { pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { date: sel(args, "date", toml.date.as_ref(), "date")? }) } } -impl ConfigLog { +impl ConfLog { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { starttime: sel(args, "starttime", @@ -172,7 +183,7 @@ impl ConfigLog { } } -impl ConfigPred { +impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { avg: sel(args, "avg", @@ -181,7 +192,7 @@ impl ConfigPred { } } -impl ConfigStats { +impl ConfStats { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { avg: sel(args, "avg", @@ -189,7 +200,7 @@ impl ConfigStats { "[predict] average")? }) } } -impl ConfigAccuracy { +impl ConfAccuracy { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { avg: sel(args, "avg", diff --git a/src/main.rs b/src/main.rs index 165626f..e66c64b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,24 +14,14 @@ use clap::{error::ErrorKind, ArgMatches, Error as ClapErr}; use log::*; use std::{io::IsTerminal, str::FromStr}; -fn main() { - let args = cli::build_cli().get_matches(); - let level = match args.get_count("verbose") { - 0 => LevelFilter::Error, - 1 => LevelFilter::Warn, - 2 => LevelFilter::Info, - 3 => LevelFilter::Debug, - _ => LevelFilter::Trace, - }; - env_logger::Builder::new().filter_level(level).format_timestamp(None).init(); - trace!("{:?}", args); - let res = match Config::try_new(&args) { - Ok(Config::Log(subargs, conf, subconf)) => cmd_log(subargs, conf, subconf), - Ok(Config::Stats(subargs, conf, subconf)) => cmd_stats(subargs, conf, subconf), - Ok(Config::Predict(subargs, conf, subconf)) => cmd_predict(subargs, conf, subconf), - Ok(Config::Accuracy(subargs, conf, subconf)) => cmd_accuracy(subargs, conf, subconf), - Ok(Config::Complete(subargs)) => cmd_complete(subargs), +fn main() { + let res = match Conf::try_new() { + Ok(Conf::Log(args, gconf, sconf)) => cmd_log(&args, gconf, sconf), + Ok(Conf::Stats(args, gconf, sconf)) => cmd_stats(&args, gconf, sconf), + Ok(Conf::Predict(args, gconf, sconf)) => cmd_predict(&args, gconf, sconf), + Ok(Conf::Accuracy(args, gconf, sconf)) => cmd_accuracy(&args, gconf, sconf), + Ok(Conf::Complete(args)) => cmd_complete(&args), Err(e) => Err(e), }; match res { @@ -182,7 +172,7 @@ pub struct Styles { out: OutStyle, } impl Styles { - fn from_args(args: &ArgMatches, conf: ConfigAll) -> Self { + fn from_args(args: &ArgMatches, conf: &ConfAll) -> Self { let isterm = std::io::stdout().is_terminal(); let color = match args.get_one("color") { Some(ColorStyle::Always) => true, @@ -214,6 +204,6 @@ impl Styles { #[cfg(test)] fn from_str(s: impl AsRef) -> Self { let args = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); - Self::from_args(&args, ConfigAll::try_new(&args, &Toml::default()).unwrap()) + Self::from_args(&args, &ConfAll::try_new(&args, &Toml::default()).unwrap()) } } From 3824cf11fe5ffbba9f426805e65cb5471a4a281e Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 15 Jan 2024 21:59:47 +0000 Subject: [PATCH 05/28] conf: Merge `Styles` struct into `Conf` --- src/commands.rs | 107 ++++++++++++++++++++++------------------------ src/config.rs | 79 +++++++++++++++++++++++++++------- src/datetime.rs | 20 ++++----- src/main.rs | 68 +++-------------------------- src/parse/ansi.rs | 2 +- src/table.rs | 50 +++++++++++----------- 6 files changed, 157 insertions(+), 169 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index e8d02cb..1574c74 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,12 +6,11 @@ use std::{collections::{BTreeMap, HashMap}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result { - let st = &Styles::from_args(args, &conf); +pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { let show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, st.date_offset)?, - get_parse(args, "to", parse_date, st.date_offset)?, + get_parse(args, "from", parse_date, conf.date_offset)?, + get_parse(args, "to", parse_date, conf.date_offset)?, show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; @@ -21,7 +20,7 @@ pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result = HashMap::new(); let mut found = 0; let mut sync_start: Option = None; - let mut tbl = Table::new(st).align_left(0).align_left(2).margin(2, " ").last(last); + let mut tbl = Table::new(conf).align_left(0).align_left(2).margin(2, " ").last(last); tbl.header(["Date", "Duration", "Package/Repo"]); for p in hist { match p { @@ -34,7 +33,7 @@ pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result { // This'll overwrite any previous entry, if an unmerge started but never finished @@ -45,7 +44,7 @@ pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result { // Some sync starts have multiple entries in old logs @@ -56,7 +55,7 @@ pub fn cmd_log(args: &ArgMatches, conf: ConfAll, sconf: ConfLog) -> Result Result { - let st = &Styles::from_args(args, &conf); +pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { let show = *args.get_one("show").unwrap(); let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, st.date_offset)?, - get_parse(args, "to", parse_date, st.date_offset)?, + get_parse(args, "from", parse_date, conf.date_offset)?, + get_parse(args, "to", parse_date, conf.date_offset)?, show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let lim = *args.get_one("limit").unwrap(); let tsname = timespan_opt.map_or("", |timespan| timespan.name()); - let mut tbls = Table::new(st).align_left(0).align_left(1).margin(1, " "); + let mut tbls = Table::new(conf).align_left(0).align_left(1).margin(1, " "); tbls.header([tsname, "Repo", "Sync count", "Total time", "Predict time"]); - let mut tblp = Table::new(st).align_left(0).align_left(1).margin(1, " "); + let mut tblp = Table::new(conf).align_left(0).align_left(1).margin(1, " "); tblp.header([tsname, "Package", "Merge count", @@ -166,7 +164,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: ConfAll, sconf: ConfStats) -> Result Result nextts { - let group = timespan.at(curts, st.date_offset); - cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, st, lim, sconf.avg, show, group, - &sync_time, &pkg_time); + let group = timespan.at(curts, conf.date_offset); + cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, conf, lim, sconf.avg, show, + group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); - nextts = timespan.next(t, st.date_offset); + nextts = timespan.next(t, conf.date_offset); curts = t; } } @@ -232,9 +230,10 @@ pub fn cmd_stats(args: &ArgMatches, conf: ConfAll, sconf: ConfStats) -> Result Result, tblp: &mut Table<8>, tblt: &mut Table<7>, - st: &Styles, + conf: &Conf, lim: u16, avg: Average, show: Show, @@ -266,7 +265,7 @@ fn cmd_stats_group(tbls: &mut Table<5>, for (repo, time) in sync_time { tbls.row([&[&group], &[repo], - &[&st.cnt, &time.count], + &[&conf.cnt, &time.count], &[&FmtDur(time.tot)], &[&FmtDur(time.pred(lim, avg))]]); } @@ -275,11 +274,11 @@ fn cmd_stats_group(tbls: &mut Table<5>, if show.pkg && !pkg_time.is_empty() { for (pkg, (merge, unmerge)) in pkg_time { tblp.row([&[&group], - &[&st.pkg, pkg], - &[&st.cnt, &merge.count], + &[&conf.pkg, pkg], + &[&conf.cnt, &merge.count], &[&FmtDur(merge.tot)], &[&FmtDur(merge.pred(lim, avg))], - &[&st.cnt, &unmerge.count], + &[&conf.cnt, &unmerge.count], &[&FmtDur(unmerge.tot)], &[&FmtDur(unmerge.pred(lim, avg))]]); } @@ -297,10 +296,10 @@ fn cmd_stats_group(tbls: &mut Table<5>, unmerge_count += unmerge.count; } tblt.row([&[&group], - &[&st.cnt, &merge_count], + &[&conf.cnt, &merge_count], &[&FmtDur(merge_time)], &[&FmtDur(merge_time.checked_div(merge_count).unwrap_or(-1))], - &[&st.cnt, &unmerge_count], + &[&conf.cnt, &unmerge_count], &[&FmtDur(unmerge_time)], &[&FmtDur(unmerge_time.checked_div(unmerge_count).unwrap_or(-1))]]); } @@ -309,8 +308,7 @@ fn cmd_stats_group(tbls: &mut Table<5>, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result { - let st = &Styles::from_args(args, &conf); +pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result { let now = epoch_now(); let show: Show = *args.get_one("show").unwrap(); let first = *args.get_one("first").unwrap_or(&usize::MAX); @@ -322,7 +320,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result< let lim = *args.get_one("limit").unwrap(); let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); - let mut tbl = Table::new(st).align_left(0).align_left(2).margin(2, " ").last(last); + let mut tbl = Table::new(conf).align_left(0).align_left(2).margin(2, " ").last(last); let mut tmpdirs: Vec = args.get_many("tmpdir").unwrap().cloned().collect(); // Gather and print info about current merge process. @@ -343,8 +341,8 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result< // Parse emerge log. let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, st.date_offset)?, - get_parse(args, "to", parse_date, st.date_offset)?, + get_parse(args, "from", parse_date, conf.date_offset)?, + get_parse(args, "to", parse_date, conf.date_offset)?, Show { merge: true, ..Show::default() }, vec![], false)?; @@ -412,35 +410,35 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result< if show.merge && totcount <= first { if elapsed > 0 { let stage = get_buildlog(&p, &tmpdirs).unwrap_or_default(); - tbl.row([&[&st.pkg, &p.ebuild_version()], + tbl.row([&[&conf.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], - &[&st.clr, &"- ", &FmtDur(elapsed), &st.clr, &stage]]); + &[&conf.clr, &"- ", &FmtDur(elapsed), &conf.clr, &stage]]); } else { - tbl.row([&[&st.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], &[]]); + tbl.row([&[&conf.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], &[]]); } } } if totcount > 0 { if show.tot { let mut s: Vec<&dyn Disp> = vec![&"Estimate for ", - &st.cnt, + &conf.cnt, &totcount, - &st.clr, + &conf.clr, if totcount > 1 { &" ebuilds" } else { &" ebuild" }]; if totunknown > 0 { - s.extend::<[&dyn Disp; 5]>([&", ", &st.cnt, &totunknown, &st.clr, &" unknown"]); + s.extend::<[&dyn Disp; 5]>([&", ", &conf.cnt, &totunknown, &conf.clr, &" unknown"]); } let tothidden = totcount.saturating_sub(first.min(last - 1)); if tothidden > 0 { - s.extend::<[&dyn Disp; 5]>([&", ", &st.cnt, &tothidden, &st.clr, &" hidden"]); + s.extend::<[&dyn Disp; 5]>([&", ", &conf.cnt, &tothidden, &conf.clr, &" hidden"]); } let e = FmtDur(totelapsed); if totelapsed > 0 { - s.extend::<[&dyn Disp; 4]>([&", ", &e, &st.clr, &" elapsed"]); + s.extend::<[&dyn Disp; 4]>([&", ", &e, &conf.clr, &" elapsed"]); } tbl.row([&s, - &[&FmtDur(totpredict), &st.clr], - &[&"@ ", &st.dur, &FmtDate(now + totpredict)]]); + &[&FmtDur(totpredict), &conf.clr], + &[&"@ ", &conf.dur, &FmtDate(now + totpredict)]]); } } else { tbl.row([&[&"No pretended merge found"], &[], &[]]); @@ -448,12 +446,11 @@ pub fn cmd_predict(args: &ArgMatches, conf: ConfAll, sconf: ConfPred) -> Result< Ok(totcount > 0) } -pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Result { - let st = &Styles::from_args(args, &conf); +pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Result { let show: Show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, st.date_offset)?, - get_parse(args, "to", parse_date, st.date_offset)?, + get_parse(args, "from", parse_date, conf.date_offset)?, + get_parse(args, "to", parse_date, conf.date_offset)?, Show { merge: true, ..Show::default() }, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; @@ -463,7 +460,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Re let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); let mut found = false; - let mut tbl = Table::new(st).align_left(0).align_left(1).last(last); + let mut tbl = Table::new(conf).align_left(0).align_left(1).last(last); tbl.header(["Date", "Package", "Real", "Predicted", "Error"]); for p in hist { match p { @@ -480,7 +477,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Re -1 => { if show.merge { tbl.row([&[&FmtDate(ts)], - &[&st.merge, &p.ebuild_version()], + &[&conf.merge, &p.ebuild_version()], &[&FmtDur(real)], &[], &[]]) @@ -490,10 +487,10 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Re let err = (pred - real).abs() as f64 * 100.0 / real as f64; if show.merge { tbl.row([&[&FmtDate(ts)], - &[&st.merge, &p.ebuild_version()], + &[&conf.merge, &p.ebuild_version()], &[&FmtDur(real)], &[&FmtDur(pred)], - &[&st.cnt, &format!("{err:.1}%")]]) + &[&conf.cnt, &format!("{err:.1}%")]]) } let errs = pkg_errs.entry(p.ebuild().to_owned()).or_default(); errs.push(err); @@ -507,11 +504,11 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: ConfAll, sconf: ConfAccuracy) -> Re } drop(tbl); if show.tot { - let mut tbl = Table::new(st).align_left(0); + let mut tbl = Table::new(conf).align_left(0); tbl.header(["Package", "Error"]); for (p, e) in pkg_errs { let avg = e.iter().sum::() / e.len() as f64; - tbl.row([&[&st.pkg, &p], &[&st.cnt, &format!("{avg:.1}%")]]); + tbl.row([&[&conf.pkg, &p], &[&conf.cnt, &format!("{avg:.1}%")]]); } } Ok(found) diff --git a/src/config.rs b/src/config.rs index 390102a..eb4b289 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,19 +5,19 @@ use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, use serde::Deserialize; use std::{env::var, fs::File, io::Read}; -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] struct TomlLog { starttime: Option, } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] struct TomlPred { average: Option, } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] struct TomlStats { average: Option, } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug)] struct TomlAccuracy { average: Option, } @@ -97,15 +97,30 @@ impl ArgParse for Average { } } -pub enum Conf { - Log(ArgMatches, ConfAll, ConfLog), - Stats(ArgMatches, ConfAll, ConfStats), - Predict(ArgMatches, ConfAll, ConfPred), - Accuracy(ArgMatches, ConfAll, ConfAccuracy), +pub enum Configs { + Log(ArgMatches, Conf, ConfLog), + Stats(ArgMatches, Conf, ConfStats), + Predict(ArgMatches, Conf, ConfPred), + Accuracy(ArgMatches, Conf, ConfAccuracy), Complete(ArgMatches), } -pub struct ConfAll { - pub date: DateStyle, +/// Global config +/// +/// Colors use `prefix/suffix()` instead of `paint()` because `paint()` doesn't handle `'{:>9}'` +/// alignments properly. +pub struct Conf { + pub pkg: AnsiStr, + pub merge: AnsiStr, + pub unmerge: AnsiStr, + pub dur: AnsiStr, + pub cnt: AnsiStr, + pub clr: AnsiStr, + pub lineend: &'static [u8], + pub header: bool, + pub dur_t: DurationStyle, + pub date_offset: time::UtcOffset, + pub date_fmt: DateStyle, + pub out: OutStyle, } pub struct ConfLog { pub starttime: bool, @@ -120,8 +135,9 @@ pub struct ConfStats { pub struct ConfAccuracy { pub avg: Average, } -impl Conf { - pub fn try_new() -> Result { + +impl Configs { + pub fn load() -> Result { let args = cli::build_cli().get_matches(); let level = match args.get_count("verbose") { 0 => LevelFilter::Error, @@ -134,7 +150,7 @@ impl Conf { trace!("{:?}", args); let toml = Toml::load(args.get_one::("config"), var("EMLOP_CONFIG").ok())?; log::trace!("{:?}", toml); - let conf = ConfAll::try_new(&args, &toml)?; + let conf = Conf::try_new(&args, &toml)?; Ok(match args.subcommand() { Some(("log", sub)) => Self::Log(sub.clone(), conf, ConfLog::try_new(sub, &toml)?), Some(("stats", sub)) => Self::Stats(sub.clone(), conf, ConfStats::try_new(sub, &toml)?), @@ -167,9 +183,40 @@ fn sel(args: &ArgMatches, } } -impl ConfAll { +impl Conf { pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { date: sel(args, "date", toml.date.as_ref(), "date")? }) + let isterm = std::io::stdout().is_terminal(); + let color = match args.get_one("color") { + Some(ColorStyle::Always) => true, + Some(ColorStyle::Never) => false, + None => isterm, + }; + let out = match args.get_one("output") { + Some(o) => *o, + None if isterm => OutStyle::Columns, + None => OutStyle::Tab, + }; + let header = args.get_flag("header"); + let dur_t = *args.get_one("duration").unwrap(); + let date_fmt = sel(args, "date", toml.date.as_ref(), "date")?; + let date_offset = get_offset(args.get_flag("utc")); + Ok(Self { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), + merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), + unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), + dur: AnsiStr::from(if color { "\x1B[1;35m" } else { "" }), + cnt: AnsiStr::from(if color { "\x1B[2;33m" } else { "" }), + clr: AnsiStr::from(if color { "\x1B[0m" } else { "" }), + lineend: if color { b"\x1B[0m\n" } else { b"\n" }, + header, + dur_t, + date_offset, + date_fmt, + out }) + } + #[cfg(test)] + pub fn from_str(s: impl AsRef) -> Self { + let args = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); + Self::try_new(&args, &Toml::default()).unwrap() } } diff --git a/src/datetime.rs b/src/datetime.rs index a7b6c27..8e5066a 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,6 +1,6 @@ use crate::{config::{err, ArgParse}, table::Disp, - wtb, DurationStyle, Styles}; + wtb, Conf, DurationStyle}; use anyhow::{bail, Error}; use log::{debug, warn}; use regex::Regex; @@ -58,14 +58,14 @@ pub fn fmt_utctime(ts: i64) -> String { pub struct FmtDate(pub i64); /// Format dates according to user preferencess impl Disp for FmtDate { - fn out(&self, buf: &mut Vec, st: &Styles) -> usize { + fn out(&self, buf: &mut Vec, conf: &Conf) -> usize { let start = buf.len(); - if st.date_fmt.0.is_empty() { + if conf.date_fmt.0.is_empty() { write!(buf, "{}", self.0).expect("write to buf"); } else { OffsetDateTime::from_unix_timestamp(self.0).expect("unix from i64") - .to_offset(st.date_offset) - .format_into(buf, &st.date_fmt.0) + .to_offset(conf.date_offset) + .format_into(buf, &conf.date_fmt.0) .expect("write to buf"); } buf.len() - start @@ -245,13 +245,13 @@ impl Timespan { /// Wrapper around a duration (seconds) to implement `table::Disp` pub struct FmtDur(pub i64); impl crate::table::Disp for FmtDur { - fn out(&self, buf: &mut Vec, st: &Styles) -> usize { + fn out(&self, buf: &mut Vec, conf: &Conf) -> usize { use std::io::Write; use DurationStyle::*; let sec = self.0; - let dur = st.dur.val; + let dur = conf.dur.val; let start = buf.len(); - match st.dur_t { + match conf.dur_t { _ if sec < 0 => wtb!(buf, "{dur}?"), HMS if sec >= 3600 => { wtb!(buf, "{dur}{}:{:02}:{:02}", sec / 3600, sec % 3600 / 60, sec % 60) @@ -272,7 +272,7 @@ impl crate::table::Disp for FmtDur { } }, } - buf.len() - start - st.dur.val.len() + buf.len() - start - conf.dur.val.len() } } @@ -382,7 +382,7 @@ mod test { { for (st, exp) in [("hms", hms), ("hmsfixed", fixed), ("secs", secs), ("human", human)] { let mut buf = vec![]; - FmtDur(i).out(&mut buf, &Styles::from_str(format!("emlop l --color=n --dur {st}"))); + FmtDur(i).out(&mut buf, &Conf::from_str(format!("emlop l --color=n --dur {st}"))); assert_eq!(exp, &String::from_utf8(buf).unwrap()); } } diff --git a/src/main.rs b/src/main.rs index e66c64b..bbe359c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,14 +14,13 @@ use clap::{error::ErrorKind, ArgMatches, Error as ClapErr}; use log::*; use std::{io::IsTerminal, str::FromStr}; - fn main() { - let res = match Conf::try_new() { - Ok(Conf::Log(args, gconf, sconf)) => cmd_log(&args, gconf, sconf), - Ok(Conf::Stats(args, gconf, sconf)) => cmd_stats(&args, gconf, sconf), - Ok(Conf::Predict(args, gconf, sconf)) => cmd_predict(&args, gconf, sconf), - Ok(Conf::Accuracy(args, gconf, sconf)) => cmd_accuracy(&args, gconf, sconf), - Ok(Conf::Complete(args)) => cmd_complete(&args), + let res = match Configs::load() { + Ok(Configs::Log(args, conf, sconf)) => cmd_log(&args, &conf, sconf), + Ok(Configs::Stats(args, conf, sconf)) => cmd_stats(&args, &conf, sconf), + Ok(Configs::Predict(args, conf, sconf)) => cmd_predict(&args, &conf, sconf), + Ok(Configs::Accuracy(args, conf, sconf)) => cmd_accuracy(&args, &conf, sconf), + Ok(Configs::Complete(args)) => cmd_complete(&args), Err(e) => Err(e), }; match res { @@ -152,58 +151,3 @@ pub enum OutStyle { #[clap(alias("t"))] Tab, } - -/// Holds styling preferences. -/// -/// Colors use `prefix/suffix()` instead of `paint()` because `paint()` doesn't handle `'{:>9}'` -/// alignments properly. -pub struct Styles { - pkg: AnsiStr, - merge: AnsiStr, - unmerge: AnsiStr, - dur: AnsiStr, - cnt: AnsiStr, - clr: AnsiStr, - lineend: &'static [u8], - header: bool, - dur_t: DurationStyle, - date_offset: time::UtcOffset, - date_fmt: DateStyle, - out: OutStyle, -} -impl Styles { - fn from_args(args: &ArgMatches, conf: &ConfAll) -> Self { - let isterm = std::io::stdout().is_terminal(); - let color = match args.get_one("color") { - Some(ColorStyle::Always) => true, - Some(ColorStyle::Never) => false, - None => isterm, - }; - let out = match args.get_one("output") { - Some(o) => *o, - None if isterm => OutStyle::Columns, - None => OutStyle::Tab, - }; - let header = args.get_flag("header"); - let dur_t = *args.get_one("duration").unwrap(); - let date_fmt = conf.date; - let date_offset = get_offset(args.get_flag("utc")); - Styles { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), - merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), - unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), - dur: AnsiStr::from(if color { "\x1B[1;35m" } else { "" }), - cnt: AnsiStr::from(if color { "\x1B[2;33m" } else { "" }), - clr: AnsiStr::from(if color { "\x1B[0m" } else { "" }), - lineend: if color { b"\x1B[0m\n" } else { b"\n" }, - header, - dur_t, - date_offset, - date_fmt, - out } - } - #[cfg(test)] - fn from_str(s: impl AsRef) -> Self { - let args = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); - Self::from_args(&args, &ConfAll::try_new(&args, &Toml::default()).unwrap()) - } -} diff --git a/src/parse/ansi.rs b/src/parse/ansi.rs index 2e75d65..b6f6b60 100644 --- a/src/parse/ansi.rs +++ b/src/parse/ansi.rs @@ -82,7 +82,7 @@ impl From<&'static str> for AnsiStr { } } impl crate::table::Disp for AnsiStr { - fn out(&self, buf: &mut Vec, _st: &crate::Styles) -> usize { + fn out(&self, buf: &mut Vec, _conf: &crate::Conf) -> usize { buf.extend_from_slice(self.val.as_bytes()); self.len } diff --git a/src/table.rs b/src/table.rs index fa4facb..cf23152 100644 --- a/src/table.rs +++ b/src/table.rs @@ -1,13 +1,13 @@ -use crate::{OutStyle, Styles}; +use crate::{Conf, OutStyle}; use std::{collections::VecDeque, io::{stdout, BufWriter, Write as _}}; pub trait Disp { /// Write to buf and returns the number of visible chars written - fn out(&self, buf: &mut Vec, st: &Styles) -> usize; + fn out(&self, buf: &mut Vec, conf: &Conf) -> usize; } impl Disp for T { - fn out(&self, buf: &mut Vec, _st: &Styles) -> usize { + fn out(&self, buf: &mut Vec, _conf: &Conf) -> usize { let start = buf.len(); write!(buf, "{self}").expect("write to buf"); buf.len() - start @@ -33,8 +33,8 @@ pub struct Table<'a, const N: usize> { /// Whether a header has been set have_header: bool, - /// Main style - styles: &'a Styles, + /// Main config + conf: &'a Conf, /// Column alignments (defaults to Right) aligns: [Align; N], /// Margin between columns, printed left of the column, defaults to `" "` @@ -45,11 +45,11 @@ pub struct Table<'a, const N: usize> { impl<'a, const N: usize> Table<'a, N> { /// Initialize new table - pub fn new(st: &'a Styles) -> Table { + pub fn new(conf: &'a Conf) -> Table { Self { rows: VecDeque::with_capacity(32), buf: Vec::with_capacity(1024), widths: [0; N], - styles: st, + conf: conf, have_header: false, aligns: [Align::Right; N], margins: [" "; N], @@ -72,7 +72,7 @@ impl<'a, const N: usize> Table<'a, N> { } /// Add a section header pub fn header(&mut self, row: [&str; N]) { - if self.styles.header { + if self.conf.header { self.last = self.last.saturating_add(1); self.have_header = true; @@ -102,7 +102,7 @@ impl<'a, const N: usize> Table<'a, N> { let mut idxrow = [(0, 0, 0); N]; for i in 0..N { let start = self.buf.len(); - let len = row[i].iter().map(|c| c.out(&mut self.buf, self.styles)).sum(); + let len = row[i].iter().map(|c| c.out(&mut self.buf, self.conf)).sum(); self.widths[i] = usize::max(self.widths[i], len); idxrow[i] = (len, start, self.buf.len()); } @@ -131,7 +131,7 @@ impl<'a, const N: usize> Table<'a, N> { continue; } let (len, pos0, pos1) = row[i]; - if self.styles.out == OutStyle::Tab { + if self.conf.out == OutStyle::Tab { if !first { out.write_all(b"\t").unwrap_or(()); } @@ -158,7 +158,7 @@ impl<'a, const N: usize> Table<'a, N> { } first = false; } - out.write_all(self.styles.lineend).unwrap_or(()); + out.write_all(self.conf.lineend).unwrap_or(()); } } } @@ -184,24 +184,24 @@ mod test { #[test] fn last() { - let st = Styles::from_str("emlop log --color=n -H"); + let conf = Conf::from_str("emlop log --color=n -H"); // No limit - let mut t = Table::<1>::new(&st); + let mut t = Table::<1>::new(&conf); for i in 1..10 { t.row([&[&format!("{i}")]]); } check(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n"); // 5 max - let mut t = Table::<1>::new(&st).last(5); + let mut t = Table::<1>::new(&conf).last(5); for i in 1..10 { t.row([&[&format!("{i}")]]); } check(t, "5\n6\n7\n8\n9\n"); // 5 max ignoring header - let mut t = Table::new(&st).last(5); + let mut t = Table::new(&conf).last(5); t.header(["h"]); for i in 1..10 { t.row([&[&format!("{i}")]]); @@ -211,8 +211,8 @@ mod test { #[test] fn align_cols() { - let st = Styles::from_str("emlop log --color=n --output=c"); - let mut t = Table::<2>::new(&st).align_left(0); + let conf = Conf::from_str("emlop log --color=n --output=c"); + let mut t = Table::<2>::new(&conf).align_left(0); t.row([&[&"short"], &[&1]]); t.row([&[&"looooooooooooong"], &[&1]]); t.row([&[&"high"], &[&9999]]); @@ -225,8 +225,8 @@ mod test { #[test] fn align_tab() { - let st = Styles::from_str("emlop log --color=n --output=t"); - let mut t = Table::<2>::new(&st).align_left(0); + let conf = Conf::from_str("emlop log --color=n --output=t"); + let mut t = Table::<2>::new(&conf).align_left(0); t.row([&[&"short"], &[&1]]); t.row([&[&"looooooooooooong"], &[&1]]); t.row([&[&"high"], &[&9999]]); @@ -238,10 +238,10 @@ mod test { #[test] fn color() { - let st = Styles::from_str("emlop log --color=y --output=c"); - let mut t = Table::<2>::new(&st).align_left(0); + let conf = Conf::from_str("emlop log --color=y --output=c"); + let mut t = Table::<2>::new(&conf).align_left(0); t.row([&[&"123"], &[&1]]); - t.row([&[&st.merge, &1, &st.dur, &2, &st.cnt, &3, &st.clr], &[&1]]); + t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]); let res = "123 1\x1B[0m\n\ \x1B[1;32m1\x1B[1;35m2\x1B[2;33m3\x1B[0m 1\x1B[0m\n"; let (l1, l2) = res.split_once('\n').expect("two lines"); @@ -252,10 +252,10 @@ mod test { #[test] fn nocolor() { - let st = Styles::from_str("emlop log --color=n --output=c"); - let mut t = Table::<2>::new(&st).align_left(0); + let conf = Conf::from_str("emlop log --color=n --output=c"); + let mut t = Table::<2>::new(&conf).align_left(0); t.row([&[&"123"], &[&1]]); - t.row([&[&st.merge, &1, &st.dur, &2, &st.cnt, &3, &st.clr], &[&1]]); + t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]); let res = "123 1\n\ >>> 123 1\n"; check(t, res); From 15d2de4db551f17e1de1e68a39e45c52978a03ec Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 15 Jan 2024 22:47:07 +0000 Subject: [PATCH 06/28] conf: Move `Toml` to its own module --- src/config.rs | 52 ++++++---------------------------------------- src/config/toml.rs | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 src/config/toml.rs diff --git a/src/config.rs b/src/config.rs index eb4b289..8fee4d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,52 +1,11 @@ -use crate::*; -use anyhow::{Context, Error}; +mod toml; + +use crate::{*, config::toml::Toml}; +use anyhow::{Error}; use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, ArgMatches}; -use serde::Deserialize; -use std::{env::var, fs::File, io::Read}; +use std::{env::var}; -#[derive(Deserialize, Debug)] -struct TomlLog { - starttime: Option, -} -#[derive(Deserialize, Debug)] -struct TomlPred { - average: Option, -} -#[derive(Deserialize, Debug)] -struct TomlStats { - average: Option, -} -#[derive(Deserialize, Debug)] -struct TomlAccuracy { - average: Option, -} -#[derive(Deserialize, Debug, Default)] -pub struct Toml { - date: Option, - log: Option, - predict: Option, - stats: Option, - accuracy: Option, -} -impl Toml { - fn load(arg: Option<&String>, env: Option) -> Result { - match arg.or(env.as_ref()) { - Some(s) if s.is_empty() => Ok(Self::default()), - Some(s) => Self::doload(s.as_str()), - _ => Self::doload(&format!("{}/.config/emlop.toml", - var("HOME").unwrap_or("".to_string()))), - } - } - fn doload(name: &str) -> Result { - log::trace!("Loading config {name:?}"); - let mut f = File::open(name).with_context(|| format!("Cannot open {name:?}"))?; - let mut buf = String::new(); - // TODO Streaming read - f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; - toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}")) - } -} pub fn err(val: String, src: &'static str, possible: &'static str) -> ClapError { let mut err = clap::Error::new(ErrorKind::InvalidValue); @@ -104,6 +63,7 @@ pub enum Configs { Accuracy(ArgMatches, Conf, ConfAccuracy), Complete(ArgMatches), } + /// Global config /// /// Colors use `prefix/suffix()` instead of `paint()` because `paint()` doesn't handle `'{:>9}'` diff --git a/src/config/toml.rs b/src/config/toml.rs new file mode 100644 index 0000000..8d6804e --- /dev/null +++ b/src/config/toml.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; +use anyhow::{Context, Error}; +use std::{env::var, fs::File, io::Read}; + +#[derive(Deserialize, Debug)] +pub struct TomlLog { + pub starttime: Option, +} +#[derive(Deserialize, Debug)] +pub struct TomlPred { + pub average: Option, +} +#[derive(Deserialize, Debug)] +pub struct TomlStats { + pub average: Option, +} +#[derive(Deserialize, Debug)] +pub struct TomlAccuracy { + pub average: Option, +} +#[derive(Deserialize, Debug, Default)] +pub struct Toml { + pub date: Option, + pub log: Option, + pub predict: Option, + pub stats: Option, + pub accuracy: Option, +} +impl Toml { + pub fn load(arg: Option<&String>, env: Option) -> Result { + match arg.or(env.as_ref()) { + Some(s) if s.is_empty() => Ok(Self::default()), + Some(s) => Self::doload(s.as_str()), + _ => Self::doload(&format!("{}/.config/emlop.toml", + var("HOME").unwrap_or("".to_string()))), + } + } + fn doload(name: &str) -> Result { + log::trace!("Loading config {name:?}"); + let mut f = File::open(name).with_context(|| format!("Cannot open {name:?}"))?; + let mut buf = String::new(); + // TODO Streaming read + f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; + toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}")) + } +} From 04acbfe1364aa25bd7b36805dbe2677937fea74b Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Thu, 25 Jan 2024 12:06:52 +0000 Subject: [PATCH 07/28] qa: Clippy --- Cargo.toml | 3 +++ src/commands.rs | 14 +++++++------- src/datetime.rs | 2 +- src/parse/current.rs | 6 +++--- src/parse/history.rs | 2 +- src/table.rs | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3a5b6d3..3390e17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ edition = "2021" rust-version = "1.70.0" exclude = ["benches", "docs", "rustfmt.toml", ".github", ".gitignore"] +[lints.clippy] +missing_const_for_fn = "warn" + [dependencies] anyhow = "1.0.32" atoi = "2.0.0" diff --git a/src/commands.rs b/src/commands.rs index 1574c74..1dae8c3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -75,7 +75,7 @@ struct Times { tot: i64, } impl Times { - fn new() -> Self { + const fn new() -> Self { Self { vals: vec![], count: 0, tot: 0 } } /// Digest new data point @@ -202,7 +202,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { if let Some(start_ts) = merge_start.remove(key) { let (times, _) = pkg_time.entry(p.ebuild().to_owned()) - .or_insert_with(|| (Times::new(), Times::new())); + .or_insert((Times::new(), Times::new())); times.insert(ts - start_ts); } }, @@ -212,7 +212,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { if let Some(start_ts) = unmerge_start.remove(key) { let (_, times) = pkg_time.entry(p.ebuild().to_owned()) - .or_insert_with(|| (Times::new(), Times::new())); + .or_insert((Times::new(), Times::new())); times.insert(ts - start_ts); } }, @@ -222,7 +222,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { if let Some(start_ts) = sync_start.take() { - let times = sync_time.entry(repo).or_insert_with(Times::new); + let times = sync_time.entry(repo).or_insert(Times::new()); times.insert(ts - start_ts); } else { warn!("Sync stop without a start at {ts}") @@ -232,7 +232,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result Result { if let Some(start_ts) = started.remove(&Pkg::new(p.ebuild(), p.version())) { - let timevec = times.entry(p.ebuild().to_string()).or_insert_with(Times::new); + let timevec = times.entry(p.ebuild().to_string()).or_insert(Times::new()); timevec.insert(ts - start_ts); } }, @@ -471,7 +471,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu Hist::MergeStop { ts, ref key, .. } => { found = true; if let Some(start) = pkg_starts.remove(key) { - let times = pkg_times.entry(p.ebuild().to_owned()).or_insert_with(Times::new); + let times = pkg_times.entry(p.ebuild().to_owned()).or_insert(Times::new()); let real = ts - start; match times.pred(lim, sconf.avg) { -1 => { diff --git a/src/datetime.rs b/src/datetime.rs index 8e5066a..ffbb928 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -232,7 +232,7 @@ impl Timespan { } } - pub fn name(&self) -> &'static str { + pub const fn name(&self) -> &'static str { match self { Timespan::Year => "Year", Timespan::Month => "Month", diff --git a/src/parse/current.rs b/src/parse/current.rs index 984f4e4..b2a38d1 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -112,10 +112,10 @@ fn read_buildlog(file: File, max: usize) -> String { for line in rev_lines::RevLines::new(BufReader::new(file)).map_while(Result::ok) { if line.starts_with(">>>") { let tag = line.split_ascii_whitespace().skip(1).take(2).collect::>().join(" "); - if last.is_empty() { - return format!(" ({})", tag.trim_matches('.')); + return if last.is_empty() { + format!(" ({})", tag.trim_matches('.')) } else { - return format!(" ({}: {})", tag.trim_matches('.'), last); + format!(" ({}: {})", tag.trim_matches('.'), last) } } if last.is_empty() { diff --git a/src/parse/history.rs b/src/parse/history.rs index 36b8459..a955ab7 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -57,7 +57,7 @@ impl Hist { _ => unreachable!("No ebuild/version for {:?}", self), } } - pub fn ts(&self) -> i64 { + pub const fn ts(&self) -> i64 { match self { Self::MergeStart { ts, .. } => *ts, Self::MergeStop { ts, .. } => *ts, diff --git a/src/table.rs b/src/table.rs index cf23152..127c203 100644 --- a/src/table.rs +++ b/src/table.rs @@ -49,7 +49,7 @@ impl<'a, const N: usize> Table<'a, N> { Self { rows: VecDeque::with_capacity(32), buf: Vec::with_capacity(1024), widths: [0; N], - conf: conf, + conf, have_header: false, aligns: [Align::Right; N], margins: [" "; N], From 76896e45fb753575c20fcc5f5fc798a84721c052 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 24 Jan 2024 12:36:49 +0000 Subject: [PATCH 08/28] conf: Move code to a types submodule, make `ArgParse` and `sel()` more versatile * We'll have lots of small parsable types, this should be more tidy. * Some future types need configurable parsing and default. --- src/config.rs | 98 ++++++++++++++------------------------------ src/config/toml.rs | 2 +- src/config/types.rs | 49 ++++++++++++++++++++++ src/datetime.rs | 9 ++-- src/parse/current.rs | 2 +- 5 files changed, 85 insertions(+), 75 deletions(-) create mode 100644 src/config/types.rs diff --git a/src/config.rs b/src/config.rs index 8fee4d0..eb7a67d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,61 +1,13 @@ mod toml; +mod types; -use crate::{*, config::toml::Toml}; -use anyhow::{Error}; -use clap::{error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}, - ArgMatches}; -use std::{env::var}; +use crate::{config::toml::Toml, *}; +use anyhow::Error; +use clap::{error::Error as ClapError, ArgMatches}; +use std::env::var; +pub use types::*; -pub fn err(val: String, src: &'static str, possible: &'static str) -> ClapError { - let mut err = clap::Error::new(ErrorKind::InvalidValue); - err.insert(ContextKind::InvalidValue, ContextValue::String(val)); - let p = possible.split_ascii_whitespace().map(|s| s.to_string()).collect(); - err.insert(ContextKind::ValidValue, ContextValue::Strings(p)); - err.insert(ContextKind::InvalidArg, ContextValue::String(src.to_string())); - err -} - -pub trait ArgParse { - fn parse(val: &T, src: &'static str) -> Result - where Self: Sized; -} -impl ArgParse for bool { - fn parse(b: &bool, _src: &'static str) -> Result { - Ok(*b) - } -} -impl ArgParse for bool { - fn parse(s: &String, src: &'static str) -> Result { - match s.as_str() { - "y" | "yes" => Ok(true), - "n" | "no" => Ok(false), - _ => Err(err(s.to_owned(), src, "y(es) n(o)")), - } - } -} - -#[derive(Clone, Copy, Default)] -pub enum Average { - Arith, - #[default] - Median, - WeightedArith, - WeightedMedian, -} -impl ArgParse for Average { - fn parse(s: &String, src: &'static str) -> Result { - use Average::*; - match s.as_str() { - "a" | "arith" => Ok(Arith), - "m" | "median" => Ok(Median), - "wa" | "weighted-arith" => Ok(WeightedArith), - "wm" | "weighted-median" => Ok(WeightedMedian), - _ => Err(err(s.to_owned(), src, "arith median weightedarith weigtedmedian a m wa wm")), - } - } -} - pub enum Configs { Log(ArgMatches, Conf, ConfLog), Stats(ArgMatches, Conf, ConfStats), @@ -127,19 +79,21 @@ impl Configs { } // TODO nicer way to specify src -fn sel(args: &ArgMatches, - argsrc: &'static str, - toml: Option<&T>, - tomlsrc: &'static str) - -> Result - where R: ArgParse + ArgParse + Default +fn sel(args: &ArgMatches, + argsrc: &'static str, + toml: Option<&T>, + tomlsrc: &'static str, + parg: A, + def: R) + -> Result + where R: ArgParse + ArgParse { if let Some(a) = args.get_one::(argsrc) { - R::parse(a, argsrc) + R::parse(a, parg, argsrc) } else if let Some(a) = toml { - R::parse(a, tomlsrc) + R::parse(a, parg, tomlsrc) } else { - Ok(R::default()) + Ok(def) } } @@ -158,7 +112,7 @@ impl Conf { }; let header = args.get_flag("header"); let dur_t = *args.get_one("duration").unwrap(); - let date_fmt = sel(args, "date", toml.date.as_ref(), "date")?; + let date_fmt = sel(args, "date", toml.date.as_ref(), "date", (), DateStyle::default())?; let date_offset = get_offset(args.get_flag("utc")); Ok(Self { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), @@ -185,7 +139,9 @@ impl ConfLog { Ok(Self { starttime: sel(args, "starttime", toml.log.as_ref().and_then(|l| l.starttime.as_ref()), - "[log] starttime")?, + "[log] starttime", + (), + false)?, first: *args.get_one("first").unwrap_or(&usize::MAX) }) } } @@ -195,7 +151,9 @@ impl ConfPred { Ok(Self { avg: sel(args, "avg", toml.predict.as_ref().and_then(|t| t.average.as_ref()), - "[predict] average")? }) + "[predict] average", + (), + Average::Median)? }) } } @@ -204,7 +162,9 @@ impl ConfStats { Ok(Self { avg: sel(args, "avg", toml.stats.as_ref().and_then(|t| t.average.as_ref()), - "[predict] average")? }) + "[stats] average", + (), + Average::Median)? }) } } impl ConfAccuracy { @@ -212,6 +172,8 @@ impl ConfAccuracy { Ok(Self { avg: sel(args, "avg", toml.accuracy.as_ref().and_then(|t| t.average.as_ref()), - "[predict] average")? }) + "[predict] average", + (), + Average::Median)? }) } } diff --git a/src/config/toml.rs b/src/config/toml.rs index 8d6804e..fd32293 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -1,5 +1,5 @@ -use serde::Deserialize; use anyhow::{Context, Error}; +use serde::Deserialize; use std::{env::var, fs::File, io::Read}; #[derive(Deserialize, Debug)] diff --git a/src/config/types.rs b/src/config/types.rs new file mode 100644 index 0000000..be594ee --- /dev/null +++ b/src/config/types.rs @@ -0,0 +1,49 @@ +use clap::error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}; + +pub fn err(val: String, src: &'static str, possible: &'static str) -> ClapError { + let mut err = ClapError::new(ErrorKind::InvalidValue); + err.insert(ContextKind::InvalidValue, ContextValue::String(val)); + let p = possible.split_ascii_whitespace().map(|s| s.to_string()).collect(); + err.insert(ContextKind::ValidValue, ContextValue::Strings(p)); + err.insert(ContextKind::InvalidArg, ContextValue::String(src.to_string())); + err +} + +pub trait ArgParse { + fn parse(val: &T, arg: A, src: &'static str) -> Result + where Self: Sized; +} +impl ArgParse for bool { + fn parse(b: &bool, _: (), _src: &'static str) -> Result { + Ok(*b) + } +} +impl ArgParse for bool { + fn parse(s: &String, _: (), src: &'static str) -> Result { + match s.as_str() { + "y" | "yes" => Ok(true), + "n" | "no" => Ok(false), + _ => Err(err(s.to_owned(), src, "y(es) n(o)")), + } + } +} + +#[derive(Clone, Copy)] +pub enum Average { + Arith, + Median, + WeightedArith, + WeightedMedian, +} +impl ArgParse for Average { + fn parse(s: &String, _: (), src: &'static str) -> Result { + use Average::*; + match s.as_str() { + "a" | "arith" => Ok(Arith), + "m" | "median" => Ok(Median), + "wa" | "weighted-arith" => Ok(WeightedArith), + "wm" | "weighted-median" => Ok(WeightedMedian), + _ => Err(err(s.to_owned(), src, "arith median weightedarith weigtedmedian a m wa wm")), + } + } +} diff --git a/src/datetime.rs b/src/datetime.rs index ffbb928..5b9d4f0 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -33,9 +33,9 @@ impl Default for DateStyle { Self(format_description!("[year]-[month]-[day] [hour]:[minute]:[second]")) } } -impl ArgParse for DateStyle { - fn parse(s: &String, src: &'static str) -> Result { - let fmt = match s.as_str() { +impl ArgParse for DateStyle { + fn parse(s: &String, _: (), src: &'static str) -> Result { + Ok(Self(match s.as_str() { "ymd" | "d" => format_description!("[year]-[month]-[day]"), "ymdhms" | "dt" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), "ymdhmso" | "dto" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]"), @@ -44,8 +44,7 @@ impl ArgParse for DateStyle { "compact" => format_description!("[year][month][day][hour][minute][second]"), "unix" => &[], _ => return Err(err(s.to_owned(), src, "ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix")) - }; - Ok(Self(fmt)) + })) } } diff --git a/src/parse/current.rs b/src/parse/current.rs index b2a38d1..2e6201e 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -116,7 +116,7 @@ fn read_buildlog(file: File, max: usize) -> String { format!(" ({})", tag.trim_matches('.')) } else { format!(" ({}: {})", tag.trim_matches('.'), last) - } + }; } if last.is_empty() { let stripped = Ansi::strip(&line, max); From 8c397ea4fd0183022a10038e79003171f5254226 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 26 Jan 2024 20:08:21 +0000 Subject: [PATCH 09/28] conf: Make `show` arg configurable The same type is used for different purposes, so this required to make the code a bit more versatile (see previous commit). Took a few trial and errors to arrive at this neat solution. --- src/cli.rs | 8 ------- src/commands.rs | 30 ++++++++++------------- src/config.rs | 36 ++++++++++++++++++++++++---- src/config/toml.rs | 4 ++++ src/config/types.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 43 --------------------------------- src/parse/history.rs | 8 ++++--- 7 files changed, 111 insertions(+), 75 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 951de9e..87e0255 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -34,8 +34,6 @@ pub fn build_cli_nocomplete() -> Command { let show_l = Arg::new("show").short('s') .long("show") .value_name("m,u,s,a") - .value_parser(|s: &str| crate::Show::parse(s, "musa")) - .default_value("m") .display_order(3) .help_heading("Filter") .help("Show (m)erges, (u)nmerges, (s)yncs, and/or (a)ll") @@ -47,8 +45,6 @@ pub fn build_cli_nocomplete() -> Command { let show_s = Arg::new("show").short('s') .long("show") .value_name("p,t,s,a") - .value_parser(|s: &str| crate::Show::parse(s, "ptsa")) - .default_value("p") .display_order(3) .help_heading("Filter") .help("Show (p)ackages, (t)otals, (s)yncs, and/or (a)ll") @@ -60,8 +56,6 @@ pub fn build_cli_nocomplete() -> Command { let show_p = Arg::new("show").short('s') .long("show") .value_name("e,m,t,a") - .value_parser(|s: &str| crate::Show::parse(s, "emta")) - .default_value("emt") .display_order(3) .help_heading("Filter") .help("Show (e)emerge processes, (m)erges, (t)otal, and/or (a)ll") @@ -73,8 +67,6 @@ pub fn build_cli_nocomplete() -> Command { let show_a = Arg::new("show").short('s') .long("show") .value_name("m,t,a") - .value_parser(|s: &str| crate::Show::parse(s, "mta")) - .default_value("mt") .display_order(3) .help_heading("Filter") .help("Show (m)erges, (t)otals, and/or (a)ll") diff --git a/src/commands.rs b/src/commands.rs index 1dae8c3..5760d13 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,11 +7,10 @@ use std::{collections::{BTreeMap, HashMap}, /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { - let show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, conf.date_offset)?, get_parse(args, "to", parse_date, conf.date_offset)?, - show, + sconf.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let last = *args.get_one("last").unwrap_or(&usize::MAX); @@ -143,12 +142,11 @@ impl Times { /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { - let show = *args.get_one("show").unwrap(); let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, conf.date_offset)?, get_parse(args, "to", parse_date, conf.date_offset)?, - show, + sconf.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let lim = *args.get_one("limit").unwrap(); @@ -187,7 +185,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result nextts { let group = timespan.at(curts, conf.date_offset); - cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, conf, lim, sconf.avg, show, + cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, conf, lim, sconf.avg, sconf.show, group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); @@ -232,7 +230,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result, /// Very similar to cmd_summary except we want total build time for a list of ebuilds. pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result { let now = epoch_now(); - let show: Show = *args.get_one("show").unwrap(); let first = *args.get_one("first").unwrap_or(&usize::MAX); let last = match args.get_one("last") { - Some(&n) if show.tot => n + 1, + Some(&n) if sconf.show.tot => n + 1, Some(&n) => n, None => usize::MAX, }; @@ -327,7 +324,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result Result("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, conf.date_offset)?, get_parse(args, "to", parse_date, conf.date_offset)?, - Show { merge: true, ..Show::default() }, + Show::m(), vec![], false)?; let mut started: BTreeMap = BTreeMap::new(); @@ -407,7 +404,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result 0 { let stage = get_buildlog(&p, &tmpdirs).unwrap_or_default(); tbl.row([&[&conf.pkg, &p.ebuild_version()], @@ -419,7 +416,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result 0 { - if show.tot { + if sconf.show.tot { let mut s: Vec<&dyn Disp> = vec![&"Estimate for ", &conf.cnt, &totcount, @@ -447,11 +444,10 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result Result { - let show: Show = *args.get_one("show").unwrap(); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), get_parse(args, "from", parse_date, conf.date_offset)?, get_parse(args, "to", parse_date, conf.date_offset)?, - Show { merge: true, ..Show::default() }, + Show::m(), args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let last = *args.get_one("last").unwrap_or(&usize::MAX); @@ -475,7 +471,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu let real = ts - start; match times.pred(lim, sconf.avg) { -1 => { - if show.merge { + if sconf.show.merge { tbl.row([&[&FmtDate(ts)], &[&conf.merge, &p.ebuild_version()], &[&FmtDur(real)], @@ -485,7 +481,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu }, pred => { let err = (pred - real).abs() as f64 * 100.0 / real as f64; - if show.merge { + if sconf.show.merge { tbl.row([&[&FmtDate(ts)], &[&conf.merge, &p.ebuild_version()], &[&FmtDur(real)], @@ -503,7 +499,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu } } drop(tbl); - if show.tot { + if sconf.show.tot { let mut tbl = Table::new(conf).align_left(0); tbl.header(["Package", "Error"]); for (p, e) in pkg_errs { diff --git a/src/config.rs b/src/config.rs index eb7a67d..9930caf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,16 +35,20 @@ pub struct Conf { pub out: OutStyle, } pub struct ConfLog { + pub show: Show, pub starttime: bool, pub first: usize, } pub struct ConfPred { + pub show: Show, pub avg: Average, } pub struct ConfStats { + pub show: Show, pub avg: Average, } pub struct ConfAccuracy { + pub show: Show, pub avg: Average, } @@ -136,7 +140,13 @@ impl Conf { impl ConfLog { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { starttime: sel(args, + Ok(Self { show: sel(args, + "show", + toml.log.as_ref().and_then(|t| t.show.as_ref()), + "[log] show", + "musa", + Show::m())?, + starttime: sel(args, "starttime", toml.log.as_ref().and_then(|l| l.starttime.as_ref()), "[log] starttime", @@ -148,7 +158,13 @@ impl ConfLog { impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { avg: sel(args, + Ok(Self { show: sel(args, + "show", + toml.predict.as_ref().and_then(|t| t.show.as_ref()), + "[predict] show", + "emta", + Show::emt())?, + avg: sel(args, "avg", toml.predict.as_ref().and_then(|t| t.average.as_ref()), "[predict] average", @@ -159,7 +175,13 @@ impl ConfPred { impl ConfStats { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { avg: sel(args, + Ok(Self { show: sel(args, + "show", + toml.predict.as_ref().and_then(|t| t.show.as_ref()), + "[stats] show", + "ptsa", + Show::p())?, + avg: sel(args, "avg", toml.stats.as_ref().and_then(|t| t.average.as_ref()), "[stats] average", @@ -169,7 +191,13 @@ impl ConfStats { } impl ConfAccuracy { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { avg: sel(args, + Ok(Self { show: sel(args, + "show", + toml.predict.as_ref().and_then(|t| t.show.as_ref()), + "[accuracy] show", + "mta", + Show::mt())?, + avg: sel(args, "avg", toml.accuracy.as_ref().and_then(|t| t.average.as_ref()), "[predict] average", diff --git a/src/config/toml.rs b/src/config/toml.rs index fd32293..2278865 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -4,18 +4,22 @@ use std::{env::var, fs::File, io::Read}; #[derive(Deserialize, Debug)] pub struct TomlLog { + pub show: Option, pub starttime: Option, } #[derive(Deserialize, Debug)] pub struct TomlPred { + pub show: Option, pub average: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { + pub show: Option, pub average: Option, } #[derive(Deserialize, Debug)] pub struct TomlAccuracy { + pub show: Option, pub average: Option, } #[derive(Deserialize, Debug, Default)] diff --git a/src/config/types.rs b/src/config/types.rs index be594ee..ec4b4f9 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -47,3 +47,60 @@ impl ArgParse for Average { } } } + +#[derive(Clone, Copy)] +pub struct Show { + pub pkg: bool, + pub tot: bool, + pub sync: bool, + pub merge: bool, + pub unmerge: bool, + pub emerge: bool, +} +impl Show { + pub const fn m() -> Self { + Self { pkg: false, tot: false, sync: false, merge: true, unmerge: false, emerge: false } + } + pub const fn emt() -> Self { + Self { pkg: false, tot: true, sync: false, merge: true, unmerge: false, emerge: true } + } + pub const fn p() -> Self { + Self { pkg: true, tot: false, sync: false, merge: false, unmerge: false, emerge: false } + } + pub const fn mt() -> Self { + Self { pkg: false, tot: true, sync: false, merge: true, unmerge: false, emerge: false } + } +} +impl ArgParse for Show { + fn parse(show: &String, valid: &'static str, src: &'static str) -> Result { + debug_assert!(valid.is_ascii()); // Because we use `chars()` we need to stick to ascii for `valid`. + if show.chars().all(|c| valid.contains(c)) { + Ok(Self { pkg: show.contains('p') || show.contains('a'), + tot: show.contains('t') || show.contains('a'), + sync: show.contains('s') || show.contains('a'), + merge: show.contains('m') || show.contains('a'), + unmerge: show.contains('u') || show.contains('a'), + emerge: show.contains('e') || show.contains('a') }) + } else { + Err(err(show.to_string(), src, valid)) + } + } +} +impl std::fmt::Display for Show { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut sep = ""; + for (b, s) in [(self.pkg, "pkg"), + (self.tot, "total"), + (self.sync, "sync"), + (self.merge, "merge"), + (self.unmerge, "unmerge"), + (self.emerge, "emerge")] + { + if b { + write!(f, "{sep}{s}")?; + sep = ","; + } + } + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index bbe359c..690528e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,49 +68,6 @@ pub fn get_parse(args: &ArgMatches, #[macro_export] macro_rules! wtb { ($b:ident, $($arg:expr),+) => {write!($b, $($arg),+).expect("write to buf")} } -#[derive(Clone, Copy, Default)] -pub struct Show { - pub pkg: bool, - pub tot: bool, - pub sync: bool, - pub merge: bool, - pub unmerge: bool, - pub emerge: bool, -} -impl Show { - fn parse(show: &str, valid: &str) -> Result { - debug_assert!(valid.is_ascii()); // Because we use `chars()` we need to stick to ascii for `valid`. - if show.chars().all(|c| valid.contains(c)) { - Ok(Self { pkg: show.contains('p') || show.contains('a'), - tot: show.contains('t') || show.contains('a'), - sync: show.contains('s') || show.contains('a'), - merge: show.contains('m') || show.contains('a'), - unmerge: show.contains('u') || show.contains('a'), - emerge: show.contains('e') || show.contains('a') }) - } else { - Err(format!("Valid values are letters of '{valid}'")) - } - } -} -impl std::fmt::Display for Show { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut sep = ""; - for (b, s) in [(self.pkg, "pkg"), - (self.tot, "total"), - (self.sync, "sync"), - (self.merge, "merge"), - (self.unmerge, "unmerge"), - (self.emerge, "emerge")] - { - if b { - write!(f, "{sep}{s}")?; - sep = ","; - } - } - Ok(()) - } -} - #[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum ResumeKind { diff --git a/src/parse/history.rs b/src/parse/history.rs index a955ab7..0695721 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -345,10 +345,12 @@ mod tests { let hist = get_hist(format!("tests/emerge.{}.log", file), filter_mints, filter_maxts, - Show { merge: parse_merge, - unmerge: parse_unmerge, + Show { pkg: false, + tot: false, sync: parse_sync, - ..Show::default() }, + merge: parse_merge, + unmerge: parse_unmerge, + emerge: false }, filter_terms.clone(), exact).unwrap(); let re_atom = Regex::new("^[a-zA-Z0-9-]+/[a-zA-Z0-9_+-]+$").unwrap(); From 084741a0f2d6bb3bd14382ecbb6b55c5dd78f6f4 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Fri, 26 Jan 2024 20:49:59 +0000 Subject: [PATCH 10/28] conf: Refactor conf var names --- src/commands.rs | 150 +++++++++++++++++++++++------------------------- src/main.rs | 8 +-- 2 files changed, 77 insertions(+), 81 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 5760d13..abef347 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,20 +6,19 @@ use std::{collections::{BTreeMap, HashMap}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { +pub fn cmd_log(args: &ArgMatches, gc: &Conf, sc: &ConfLog) -> Result { let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, conf.date_offset)?, - get_parse(args, "to", parse_date, conf.date_offset)?, - sconf.show, + get_parse(args, "from", parse_date, gc.date_offset)?, + get_parse(args, "to", parse_date, gc.date_offset)?, + sc.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let last = *args.get_one("last").unwrap_or(&usize::MAX); - let stt = sconf.starttime; let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); let mut found = 0; let mut sync_start: Option = None; - let mut tbl = Table::new(conf).align_left(0).align_left(2).margin(2, " ").last(last); + let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); tbl.header(["Date", "Duration", "Package/Repo"]); for p in hist { match p { @@ -30,9 +29,9 @@ pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { found += 1; let started = merges.remove(key).unwrap_or(ts + 1); - tbl.row([&[&FmtDate(if stt { started } else { ts })], + tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], &[&FmtDur(ts - started)], - &[&conf.merge, &p.ebuild_version()]]); + &[&gc.merge, &p.ebuild_version()]]); }, Hist::UnmergeStart { ts, key, .. } => { // This'll overwrite any previous entry, if an unmerge started but never finished @@ -41,9 +40,9 @@ pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { found += 1; let started = unmerges.remove(key).unwrap_or(ts + 1); - tbl.row([&[&FmtDate(if stt { started } else { ts })], + tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], &[&FmtDur(ts - started)], - &[&conf.unmerge, &p.ebuild_version()]]); + &[&gc.unmerge, &p.ebuild_version()]]); }, Hist::SyncStart { ts } => { // Some sync starts have multiple entries in old logs @@ -52,15 +51,15 @@ pub fn cmd_log(args: &ArgMatches, conf: &Conf, sconf: ConfLog) -> Result { if let Some(started) = sync_start.take() { found += 1; - tbl.row([&[&FmtDate(if stt { started } else { ts })], + tbl.row([&[&FmtDate(if sc.starttime { started } else { ts })], &[&FmtDur(ts - started)], - &[&conf.clr, &"Sync ", &repo]]); + &[&gc.clr, &"Sync ", &repo]]); } else { warn!("Sync stop without a start at {ts}"); } }, } - if found >= sconf.first { + if found >= sc.first { break; } } @@ -141,19 +140,19 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result { +pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, conf.date_offset)?, - get_parse(args, "to", parse_date, conf.date_offset)?, - sconf.show, + get_parse(args, "from", parse_date, gc.date_offset)?, + get_parse(args, "to", parse_date, gc.date_offset)?, + sc.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; let lim = *args.get_one("limit").unwrap(); let tsname = timespan_opt.map_or("", |timespan| timespan.name()); - let mut tbls = Table::new(conf).align_left(0).align_left(1).margin(1, " "); + let mut tbls = Table::new(gc).align_left(0).align_left(1).margin(1, " "); tbls.header([tsname, "Repo", "Sync count", "Total time", "Predict time"]); - let mut tblp = Table::new(conf).align_left(0).align_left(1).margin(1, " "); + let mut tblp = Table::new(gc).align_left(0).align_left(1).margin(1, " "); tblp.header([tsname, "Package", "Merge count", @@ -162,7 +161,7 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result Result nextts { - let group = timespan.at(curts, conf.date_offset); - cmd_stats_group(&mut tbls, &mut tblp, &mut tblt, conf, lim, sconf.avg, sconf.show, - group, &sync_time, &pkg_time); + let group = timespan.at(curts, gc.date_offset); + cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, lim, group, &sync_time, + &pkg_time); sync_time.clear(); pkg_time.clear(); - nextts = timespan.next(t, conf.date_offset); + nextts = timespan.next(t, gc.date_offset); curts = t; } } @@ -228,10 +227,8 @@ pub fn cmd_stats(args: &ArgMatches, conf: &Conf, sconf: ConfStats) -> Result Result, +fn cmd_stats_group(gc: &Conf, + sc: &ConfStats, + tbls: &mut Table<5>, tblp: &mut Table<8>, tblt: &mut Table<7>, - conf: &Conf, lim: u16, - avg: Average, - show: Show, group: String, sync_time: &BTreeMap, pkg_time: &BTreeMap) { // Syncs - if show.sync && !sync_time.is_empty() { + if sc.show.sync && !sync_time.is_empty() { for (repo, time) in sync_time { tbls.row([&[&group], &[repo], - &[&conf.cnt, &time.count], + &[&gc.cnt, &time.count], &[&FmtDur(time.tot)], - &[&FmtDur(time.pred(lim, avg))]]); + &[&FmtDur(time.pred(lim, sc.avg))]]); } } // Packages - if show.pkg && !pkg_time.is_empty() { + if sc.show.pkg && !pkg_time.is_empty() { for (pkg, (merge, unmerge)) in pkg_time { tblp.row([&[&group], - &[&conf.pkg, pkg], - &[&conf.cnt, &merge.count], + &[&gc.pkg, pkg], + &[&gc.cnt, &merge.count], &[&FmtDur(merge.tot)], - &[&FmtDur(merge.pred(lim, avg))], - &[&conf.cnt, &unmerge.count], + &[&FmtDur(merge.pred(lim, sc.avg))], + &[&gc.cnt, &unmerge.count], &[&FmtDur(unmerge.tot)], - &[&FmtDur(unmerge.pred(lim, avg))]]); + &[&FmtDur(unmerge.pred(lim, sc.avg))]]); } } // Totals - if show.tot && !pkg_time.is_empty() { + if sc.show.tot && !pkg_time.is_empty() { let mut merge_time = 0; let mut merge_count = 0; let mut unmerge_time = 0; @@ -294,10 +290,10 @@ fn cmd_stats_group(tbls: &mut Table<5>, unmerge_count += unmerge.count; } tblt.row([&[&group], - &[&conf.cnt, &merge_count], + &[&gc.cnt, &merge_count], &[&FmtDur(merge_time)], &[&FmtDur(merge_time.checked_div(merge_count).unwrap_or(-1))], - &[&conf.cnt, &unmerge_count], + &[&gc.cnt, &unmerge_count], &[&FmtDur(unmerge_time)], &[&FmtDur(unmerge_time.checked_div(unmerge_count).unwrap_or(-1))]]); } @@ -306,25 +302,25 @@ fn cmd_stats_group(tbls: &mut Table<5>, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result { +pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); let first = *args.get_one("first").unwrap_or(&usize::MAX); let last = match args.get_one("last") { - Some(&n) if sconf.show.tot => n + 1, + Some(&n) if sc.show.tot => n + 1, Some(&n) => n, None => usize::MAX, }; let lim = *args.get_one("limit").unwrap(); let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); - let mut tbl = Table::new(conf).align_left(0).align_left(2).margin(2, " ").last(last); + let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); let mut tmpdirs: Vec = args.get_many("tmpdir").unwrap().cloned().collect(); // Gather and print info about current merge process. let mut cms = std::i64::MAX; for i in get_all_info(Some("emerge"), &mut tmpdirs) { cms = std::cmp::min(cms, i.start); - if sconf.show.emerge { + if sc.show.emerge { tbl.row([&[&i], &[&FmtDur(now - i.start)], &[]]); } } @@ -338,8 +334,8 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, conf.date_offset)?, - get_parse(args, "to", parse_date, conf.date_offset)?, + get_parse(args, "from", parse_date, gc.date_offset)?, + get_parse(args, "to", parse_date, gc.date_offset)?, Show::m(), vec![], false)?; @@ -392,7 +388,7 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result { - let pred = tv.pred(lim, sconf.avg); + let pred = tv.pred(lim, sc.avg); (pred, pred) }, None => { @@ -404,38 +400,38 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result 0 { let stage = get_buildlog(&p, &tmpdirs).unwrap_or_default(); - tbl.row([&[&conf.pkg, &p.ebuild_version()], + tbl.row([&[&gc.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], - &[&conf.clr, &"- ", &FmtDur(elapsed), &conf.clr, &stage]]); + &[&gc.clr, &"- ", &FmtDur(elapsed), &gc.clr, &stage]]); } else { - tbl.row([&[&conf.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], &[]]); + tbl.row([&[&gc.pkg, &p.ebuild_version()], &[&FmtDur(fmtpred)], &[]]); } } } if totcount > 0 { - if sconf.show.tot { + if sc.show.tot { let mut s: Vec<&dyn Disp> = vec![&"Estimate for ", - &conf.cnt, + &gc.cnt, &totcount, - &conf.clr, + &gc.clr, if totcount > 1 { &" ebuilds" } else { &" ebuild" }]; if totunknown > 0 { - s.extend::<[&dyn Disp; 5]>([&", ", &conf.cnt, &totunknown, &conf.clr, &" unknown"]); + s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &totunknown, &gc.clr, &" unknown"]); } let tothidden = totcount.saturating_sub(first.min(last - 1)); if tothidden > 0 { - s.extend::<[&dyn Disp; 5]>([&", ", &conf.cnt, &tothidden, &conf.clr, &" hidden"]); + s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &tothidden, &gc.clr, &" hidden"]); } let e = FmtDur(totelapsed); if totelapsed > 0 { - s.extend::<[&dyn Disp; 4]>([&", ", &e, &conf.clr, &" elapsed"]); + s.extend::<[&dyn Disp; 4]>([&", ", &e, &gc.clr, &" elapsed"]); } tbl.row([&s, - &[&FmtDur(totpredict), &conf.clr], - &[&"@ ", &conf.dur, &FmtDate(now + totpredict)]]); + &[&FmtDur(totpredict), &gc.clr], + &[&"@ ", &gc.dur, &FmtDate(now + totpredict)]]); } } else { tbl.row([&[&"No pretended merge found"], &[], &[]]); @@ -443,10 +439,10 @@ pub fn cmd_predict(args: &ArgMatches, conf: &Conf, sconf: ConfPred) -> Result 0) } -pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Result { +pub fn cmd_accuracy(args: &ArgMatches, gc: &Conf, sc: &ConfAccuracy) -> Result { let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), - get_parse(args, "from", parse_date, conf.date_offset)?, - get_parse(args, "to", parse_date, conf.date_offset)?, + get_parse(args, "from", parse_date, gc.date_offset)?, + get_parse(args, "to", parse_date, gc.date_offset)?, Show::m(), args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; @@ -456,7 +452,7 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); let mut found = false; - let mut tbl = Table::new(conf).align_left(0).align_left(1).last(last); + let mut tbl = Table::new(gc).align_left(0).align_left(1).last(last); tbl.header(["Date", "Package", "Real", "Predicted", "Error"]); for p in hist { match p { @@ -469,11 +465,11 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu if let Some(start) = pkg_starts.remove(key) { let times = pkg_times.entry(p.ebuild().to_owned()).or_insert(Times::new()); let real = ts - start; - match times.pred(lim, sconf.avg) { + match times.pred(lim, sc.avg) { -1 => { - if sconf.show.merge { + if sc.show.merge { tbl.row([&[&FmtDate(ts)], - &[&conf.merge, &p.ebuild_version()], + &[&gc.merge, &p.ebuild_version()], &[&FmtDur(real)], &[], &[]]) @@ -481,12 +477,12 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu }, pred => { let err = (pred - real).abs() as f64 * 100.0 / real as f64; - if sconf.show.merge { + if sc.show.merge { tbl.row([&[&FmtDate(ts)], - &[&conf.merge, &p.ebuild_version()], + &[&gc.merge, &p.ebuild_version()], &[&FmtDur(real)], &[&FmtDur(pred)], - &[&conf.cnt, &format!("{err:.1}%")]]) + &[&gc.cnt, &format!("{err:.1}%")]]) } let errs = pkg_errs.entry(p.ebuild().to_owned()).or_default(); errs.push(err); @@ -499,12 +495,12 @@ pub fn cmd_accuracy(args: &ArgMatches, conf: &Conf, sconf: ConfAccuracy) -> Resu } } drop(tbl); - if sconf.show.tot { - let mut tbl = Table::new(conf).align_left(0); + if sc.show.tot { + let mut tbl = Table::new(gc).align_left(0); tbl.header(["Package", "Error"]); for (p, e) in pkg_errs { let avg = e.iter().sum::() / e.len() as f64; - tbl.row([&[&conf.pkg, &p], &[&conf.cnt, &format!("{avg:.1}%")]]); + tbl.row([&[&gc.pkg, &p], &[&gc.cnt, &format!("{avg:.1}%")]]); } } Ok(found) diff --git a/src/main.rs b/src/main.rs index 690528e..9a9d179 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,10 +16,10 @@ use std::{io::IsTerminal, str::FromStr}; fn main() { let res = match Configs::load() { - Ok(Configs::Log(args, conf, sconf)) => cmd_log(&args, &conf, sconf), - Ok(Configs::Stats(args, conf, sconf)) => cmd_stats(&args, &conf, sconf), - Ok(Configs::Predict(args, conf, sconf)) => cmd_predict(&args, &conf, sconf), - Ok(Configs::Accuracy(args, conf, sconf)) => cmd_accuracy(&args, &conf, sconf), + Ok(Configs::Log(args, gc, sc)) => cmd_log(&args, &gc, &sc), + Ok(Configs::Stats(args, gc, sc)) => cmd_stats(&args, &gc, &sc), + Ok(Configs::Predict(args, gc, sc)) => cmd_predict(&args, &gc, &sc), + Ok(Configs::Accuracy(args, gc, sc)) => cmd_accuracy(&args, &gc, &sc), Ok(Configs::Complete(args)) => cmd_complete(&args), Err(e) => Err(e), }; From cfe7c23c4c50f856d8f1ac24e7ace44c7ef3433b Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sat, 27 Jan 2024 17:11:00 +0000 Subject: [PATCH 11/28] conf: Use a `sel!()` macro to simplify code As a side effect, ensures the conf name is the same in toml and cli. --- src/config.rs | 84 ++++++++++++++++++---------------------------- src/config/toml.rs | 6 ++-- 2 files changed, 35 insertions(+), 55 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9930caf..5d4eff7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -83,16 +83,16 @@ impl Configs { } // TODO nicer way to specify src -fn sel(args: &ArgMatches, - argsrc: &'static str, +fn sel(cli: Option<&String>, toml: Option<&T>, + argsrc: &'static str, tomlsrc: &'static str, parg: A, def: R) -> Result where R: ArgParse + ArgParse { - if let Some(a) = args.get_one::(argsrc) { + if let Some(a) = cli { R::parse(a, parg, argsrc) } else if let Some(a) = toml { R::parse(a, parg, tomlsrc) @@ -101,6 +101,26 @@ fn sel(args: &ArgMatches, } } +macro_rules! sel { + ($cli: expr, $toml: expr, $name: ident, $arg: expr, $def: expr) => { + sel($cli.get_one::(stringify!($name)), + $toml.$name.as_ref(), + concat!("--", stringify!($name)), + stringify!($name), + $arg, + $def) + }; + ($cli: expr, $toml: expr, $section: ident, $name: ident, $arg: expr, $def: expr) => { + sel($cli.get_one::(stringify!($name)), + $toml.$section.as_ref().and_then(|t| t.$name.as_ref()), + concat!("--", stringify!($name)), + stringify!([$section] $name), + $arg, + $def) + } +} + + impl Conf { pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { let isterm = std::io::stdout().is_terminal(); @@ -116,7 +136,7 @@ impl Conf { }; let header = args.get_flag("header"); let dur_t = *args.get_one("duration").unwrap(); - let date_fmt = sel(args, "date", toml.date.as_ref(), "date", (), DateStyle::default())?; + let date_fmt = sel!(args, toml, date, (), DateStyle::default())?; let date_offset = get_offset(args.get_flag("utc")); Ok(Self { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), @@ -140,68 +160,28 @@ impl Conf { impl ConfLog { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel(args, - "show", - toml.log.as_ref().and_then(|t| t.show.as_ref()), - "[log] show", - "musa", - Show::m())?, - starttime: sel(args, - "starttime", - toml.log.as_ref().and_then(|l| l.starttime.as_ref()), - "[log] starttime", - (), - false)?, + Ok(Self { show: sel!(args, toml, log, show, "musa", Show::m())?, + starttime: sel!(args, toml, log, starttime, (), false)?, first: *args.get_one("first").unwrap_or(&usize::MAX) }) } } impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel(args, - "show", - toml.predict.as_ref().and_then(|t| t.show.as_ref()), - "[predict] show", - "emta", - Show::emt())?, - avg: sel(args, - "avg", - toml.predict.as_ref().and_then(|t| t.average.as_ref()), - "[predict] average", - (), - Average::Median)? }) + Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, + avg: sel!(args, toml, predict, avg, (), Average::Median)? }) } } impl ConfStats { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel(args, - "show", - toml.predict.as_ref().and_then(|t| t.show.as_ref()), - "[stats] show", - "ptsa", - Show::p())?, - avg: sel(args, - "avg", - toml.stats.as_ref().and_then(|t| t.average.as_ref()), - "[stats] average", - (), - Average::Median)? }) + Ok(Self { show: sel!(args, toml, stats, show, "ptsa", Show::p())?, + avg: sel!(args, toml, stats, avg, (), Average::Median)? }) } } impl ConfAccuracy { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel(args, - "show", - toml.predict.as_ref().and_then(|t| t.show.as_ref()), - "[accuracy] show", - "mta", - Show::mt())?, - avg: sel(args, - "avg", - toml.accuracy.as_ref().and_then(|t| t.average.as_ref()), - "[predict] average", - (), - Average::Median)? }) + Ok(Self { show: sel!(args, toml, accuracy, show, "mta", Show::mt())?, + avg: sel!(args, toml, accuracy, avg, (), Average::Median)? }) } } diff --git a/src/config/toml.rs b/src/config/toml.rs index 2278865..ffb6d9b 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -10,17 +10,17 @@ pub struct TomlLog { #[derive(Deserialize, Debug)] pub struct TomlPred { pub show: Option, - pub average: Option, + pub avg: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { pub show: Option, - pub average: Option, + pub avg: Option, } #[derive(Deserialize, Debug)] pub struct TomlAccuracy { pub show: Option, - pub average: Option, + pub avg: Option, } #[derive(Deserialize, Debug, Default)] pub struct Toml { From ad059ec158b3685466e4227bf960ea8b7a233999 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sat, 27 Jan 2024 17:47:31 +0000 Subject: [PATCH 12/28] conf: Make logfile configurable --- emlop.toml | 11 ++++++++--- src/cli.rs | 1 - src/commands.rs | 8 ++++---- src/config.rs | 7 ++++--- src/config/toml.rs | 1 + src/config/types.rs | 5 +++++ src/parse/history.rs | 10 +++++----- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/emlop.toml b/emlop.toml index 314e4d8..fa7dfd3 100644 --- a/emlop.toml +++ b/emlop.toml @@ -10,12 +10,17 @@ ######################################## +# logfile = "/var/log/emerge.log" # date = "rfc2822" [log] +#show = "mus" #starttime = true [predict] -#average = "arith" +#show = "emt" +#avg = "arith" [stats] -#average = "arith" +#show = "pts" +#avg = "arith" [accuracy] -#average = "arith" +#show = "mt" +#avg = "arith" diff --git a/src/cli.rs b/src/cli.rs index 87e0255..f846f07 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -261,7 +261,6 @@ pub fn build_cli_nocomplete() -> Command { .short('F') .global(true) .num_args(1) - .default_value("/var/log/emerge.log") .display_order(1) .help("Location of emerge log file"); let tmpdir = Arg::new("tmpdir").value_name("dir") diff --git a/src/commands.rs b/src/commands.rs index abef347..55a8123 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,7 +7,7 @@ use std::{collections::{BTreeMap, HashMap}, /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. pub fn cmd_log(args: &ArgMatches, gc: &Conf, sc: &ConfLog) -> Result { - let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), + let hist = get_hist(&gc.logfile, get_parse(args, "from", parse_date, gc.date_offset)?, get_parse(args, "to", parse_date, gc.date_offset)?, sc.show, @@ -142,7 +142,7 @@ impl Times { /// Then we compute the stats per ebuild, and print that. pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { let timespan_opt: Option<&Timespan> = args.get_one("group"); - let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), + let hist = get_hist(&gc.logfile, get_parse(args, "from", parse_date, gc.date_offset)?, get_parse(args, "to", parse_date, gc.date_offset)?, sc.show, @@ -333,7 +333,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result("logfile").unwrap().to_owned(), + let hist = get_hist(&gc.logfile, get_parse(args, "from", parse_date, gc.date_offset)?, get_parse(args, "to", parse_date, gc.date_offset)?, Show::m(), @@ -440,7 +440,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result Result { - let hist = get_hist(args.get_one::("logfile").unwrap().to_owned(), + let hist = get_hist(&gc.logfile, get_parse(args, "from", parse_date, gc.date_offset)?, get_parse(args, "to", parse_date, gc.date_offset)?, Show::m(), diff --git a/src/config.rs b/src/config.rs index 5d4eff7..bd19cbf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,6 +33,7 @@ pub struct Conf { pub date_offset: time::UtcOffset, pub date_fmt: DateStyle, pub out: OutStyle, + pub logfile: String, } pub struct ConfLog { pub show: Show, @@ -136,9 +137,9 @@ impl Conf { }; let header = args.get_flag("header"); let dur_t = *args.get_one("duration").unwrap(); - let date_fmt = sel!(args, toml, date, (), DateStyle::default())?; let date_offset = get_offset(args.get_flag("utc")); - Ok(Self { pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), + Ok(Self { logfile: sel!(args, toml, logfile, (), String::from("/var/log/emerge.log"))?, + pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), dur: AnsiStr::from(if color { "\x1B[1;35m" } else { "" }), @@ -148,7 +149,7 @@ impl Conf { header, dur_t, date_offset, - date_fmt, + date_fmt: sel!(args, toml, date, (), DateStyle::default())?, out }) } #[cfg(test)] diff --git a/src/config/toml.rs b/src/config/toml.rs index ffb6d9b..68c908b 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -24,6 +24,7 @@ pub struct TomlAccuracy { } #[derive(Deserialize, Debug, Default)] pub struct Toml { + pub logfile: Option, pub date: Option, pub log: Option, pub predict: Option, diff --git a/src/config/types.rs b/src/config/types.rs index ec4b4f9..f53beb2 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -18,6 +18,11 @@ impl ArgParse for bool { Ok(*b) } } +impl ArgParse for String { + fn parse(s: &String, _: (), _src: &'static str) -> Result { + Ok((*s).clone()) + } +} impl ArgParse for bool { fn parse(s: &String, _: (), src: &'static str) -> Result { match s.as_str() { diff --git a/src/parse/history.rs b/src/parse/history.rs index 0695721..733aad5 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -83,7 +83,7 @@ fn open_any_buffered(name: &str) -> Result, max_ts: Option, show: Show, @@ -92,7 +92,7 @@ pub fn get_hist(file: String, -> Result, Error> { debug!("File: {file}"); debug!("Show: {show}"); - let mut buf = open_any_buffered(&file)?; + let mut buf = open_any_buffered(file)?; let (ts_min, ts_max) = filter_ts(min_ts, max_ts)?; let filter = FilterStr::try_new(search_terms, search_exact)?; let (tx, rx): (Sender, Receiver) = bounded(256); @@ -110,7 +110,7 @@ pub fn get_hist(file: String, Ok(_) => { if let Some((t, s)) = parse_ts(&line, ts_min, ts_max) { if prev_t > t { - warn!("{file}:{curline}: System clock jump: {} -> {}", + warn!("logfile:{curline}: System clock jump: {} -> {}", fmt_utctime(prev_t), fmt_utctime(t)); } @@ -144,7 +144,7 @@ pub fn get_hist(file: String, } }, // Could be invalid UTF8, system read error... - Err(e) => warn!("{file}:{curline}: {e}"), + Err(e) => warn!("logfile:{curline}: {e}"), } line.clear(); curline += 1; @@ -342,7 +342,7 @@ mod tests { "shortline" => (1327867709, 1327871057), o => unimplemented!("Unknown test log file {:?}", o), }; - let hist = get_hist(format!("tests/emerge.{}.log", file), + let hist = get_hist(&format!("tests/emerge.{}.log", file), filter_mints, filter_maxts, Show { pkg: false, From 4646d9d7159ca0acea466293157c802aee76d6e6 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 30 Jan 2024 21:00:38 +0000 Subject: [PATCH 13/28] conf: Move `--from/--to` to `Conf`, use our own error type instead of clap's Struggled for a while to get the desired output with clap errors, so I introduced our own error type. We'll need to maintain it to match the style of clap errors, but it's not a lot of code, call sites are cleaner, and the error strings are better (see `--from` and `--show`). --- src/commands.rs | 20 ++++----- src/config.rs | 14 ++++-- src/config/types.rs | 102 +++++++++++++++++++++++++++++++++----------- src/datetime.rs | 42 ++++++++++-------- src/main.rs | 37 ++++------------ 5 files changed, 128 insertions(+), 87 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 55a8123..0d773eb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,4 +1,5 @@ use crate::{datetime::*, parse::*, proces::*, table::*, *}; +use clap::ArgMatches; use std::{collections::{BTreeMap, HashMap}, io::stdin, path::PathBuf}; @@ -8,8 +9,8 @@ use std::{collections::{BTreeMap, HashMap}, /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. pub fn cmd_log(args: &ArgMatches, gc: &Conf, sc: &ConfLog) -> Result { let hist = get_hist(&gc.logfile, - get_parse(args, "from", parse_date, gc.date_offset)?, - get_parse(args, "to", parse_date, gc.date_offset)?, + gc.from, + gc.to, sc.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; @@ -143,8 +144,8 @@ impl Times { pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(&gc.logfile, - get_parse(args, "from", parse_date, gc.date_offset)?, - get_parse(args, "to", parse_date, gc.date_offset)?, + gc.from, + gc.to, sc.show, args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; @@ -333,12 +334,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result = BTreeMap::new(); let mut times: HashMap = HashMap::new(); for p in hist { @@ -441,8 +437,8 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result Result { let hist = get_hist(&gc.logfile, - get_parse(args, "from", parse_date, gc.date_offset)?, - get_parse(args, "to", parse_date, gc.date_offset)?, + gc.from, + gc.to, Show::m(), args.get_many::("search").unwrap_or_default().cloned().collect(), args.get_flag("exact"))?; diff --git a/src/config.rs b/src/config.rs index bd19cbf..9337ba3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ mod types; use crate::{config::toml::Toml, *}; use anyhow::Error; -use clap::{error::Error as ClapError, ArgMatches}; +use clap::ArgMatches; use std::env::var; pub use types::*; @@ -34,6 +34,8 @@ pub struct Conf { pub date_fmt: DateStyle, pub out: OutStyle, pub logfile: String, + pub from: Option, + pub to: Option, } pub struct ConfLog { pub show: Show, @@ -90,7 +92,7 @@ fn sel(cli: Option<&String>, tomlsrc: &'static str, parg: A, def: R) - -> Result + -> Result where R: ArgParse + ArgParse { if let Some(a) = cli { @@ -137,8 +139,12 @@ impl Conf { }; let header = args.get_flag("header"); let dur_t = *args.get_one("duration").unwrap(); - let date_offset = get_offset(args.get_flag("utc")); + let offset = get_offset(args.get_flag("utc")); Ok(Self { logfile: sel!(args, toml, logfile, (), String::from("/var/log/emerge.log"))?, + from: args.get_one("from") + .map(|d| i64::parse(d, offset, "--from")) + .transpose()?, + to: args.get_one("to").map(|d| i64::parse(d, offset, "--to")).transpose()?, pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), @@ -148,7 +154,7 @@ impl Conf { lineend: if color { b"\x1B[0m\n" } else { b"\n" }, header, dur_t, - date_offset, + date_offset: offset, date_fmt: sel!(args, toml, date, (), DateStyle::default())?, out }) } diff --git a/src/config/types.rs b/src/config/types.rs index f53beb2..8f73f65 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,38 +1,88 @@ -use clap::error::{ContextKind, ContextValue, Error as ClapError, ErrorKind}; - -pub fn err(val: String, src: &'static str, possible: &'static str) -> ClapError { - let mut err = ClapError::new(ErrorKind::InvalidValue); - err.insert(ContextKind::InvalidValue, ContextValue::String(val)); - let p = possible.split_ascii_whitespace().map(|s| s.to_string()).collect(); - err.insert(ContextKind::ValidValue, ContextValue::Strings(p)); - err.insert(ContextKind::InvalidArg, ContextValue::String(src.to_string())); - err -} - +/// Parsing trait for args +/// +/// Similar to std::convert::From but takes extra context and returns a custom error pub trait ArgParse { - fn parse(val: &T, arg: A, src: &'static str) -> Result + fn parse(val: &T, arg: A, src: &'static str) -> Result where Self: Sized; } impl ArgParse for bool { - fn parse(b: &bool, _: (), _src: &'static str) -> Result { + fn parse(b: &bool, _: (), _src: &'static str) -> Result { Ok(*b) } } impl ArgParse for String { - fn parse(s: &String, _: (), _src: &'static str) -> Result { + fn parse(s: &String, _: (), _src: &'static str) -> Result { Ok((*s).clone()) } } impl ArgParse for bool { - fn parse(s: &String, _: (), src: &'static str) -> Result { + fn parse(s: &String, _: (), src: &'static str) -> Result { match s.as_str() { "y" | "yes" => Ok(true), "n" | "no" => Ok(false), - _ => Err(err(s.to_owned(), src, "y(es) n(o)")), + _ => Err(ArgError::new(s, src).pos("y(es) n(o)")), + } + } +} + + +/// Argument parsing error +/// +/// Designed to look like clap::Error, but more tailored to our usecase +#[derive(Debug, PartialEq)] +pub struct ArgError { + val: String, + src: &'static str, + msg: String, + possible: &'static str, +} +impl ArgError { + /// Instantiate basic error with value and source + pub fn new(val: impl Into, src: &'static str) -> Self { + Self { val: val.into(), src, msg: String::new(), possible: "" } + } + /// Set extra error message + pub fn msg(mut self, msg: impl Into) -> Self { + self.msg = msg.into(); + self + } + /// Set possible values (as a space-delimited string or as a set of letters) + pub const fn pos(mut self, possible: &'static str) -> Self { + self.possible = possible; + self + } +} +impl std::error::Error for ArgError {} +impl std::fmt::Display for ArgError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let r = "\x1B[1;31m"; + let g = "\x1B[32m"; + let b = "\x1B[33m"; + let c = "\x1B[m"; + write!(f, "{r}error{c}: invalid value '{b}{}{c}' for '{g}{}{c}'", self.val, self.src)?; + if !self.msg.is_empty() { + write!(f, ": {}", self.msg)?; } + if !self.possible.is_empty() { + if self.possible.contains(' ') { + let mut sep = "\n possible values: "; + for p in self.possible.split_ascii_whitespace() { + write!(f, "{sep}{g}{p}{c}")?; + sep = ", "; + } + } else { + let mut sep = "\n possible value: combination of "; + for p in self.possible.chars() { + write!(f, "{sep}{g}{p}{c}")?; + sep = ", "; + } + } + } + write!(f, "\n\nFor more information, try '{g}--help{c}'.") } } + #[derive(Clone, Copy)] pub enum Average { Arith, @@ -41,18 +91,18 @@ pub enum Average { WeightedMedian, } impl ArgParse for Average { - fn parse(s: &String, _: (), src: &'static str) -> Result { - use Average::*; - match s.as_str() { - "a" | "arith" => Ok(Arith), - "m" | "median" => Ok(Median), - "wa" | "weighted-arith" => Ok(WeightedArith), - "wm" | "weighted-median" => Ok(WeightedMedian), - _ => Err(err(s.to_owned(), src, "arith median weightedarith weigtedmedian a m wa wm")), + fn parse(v: &String, _: (), s: &'static str) -> Result { + match v.as_str() { + "a" | "arith" => Ok(Self::Arith), + "m" | "median" => Ok(Self::Median), + "wa" | "weighted-arith" => Ok(Self::WeightedArith), + "wm" | "weighted-median" => Ok(Self::WeightedMedian), + _ => Err(ArgError::new(v, s).pos("arith median weightedarith weigtedmedian a m wa wm")), } } } + #[derive(Clone, Copy)] pub struct Show { pub pkg: bool, @@ -77,7 +127,7 @@ impl Show { } } impl ArgParse for Show { - fn parse(show: &String, valid: &'static str, src: &'static str) -> Result { + fn parse(show: &String, valid: &'static str, src: &'static str) -> Result { debug_assert!(valid.is_ascii()); // Because we use `chars()` we need to stick to ascii for `valid`. if show.chars().all(|c| valid.contains(c)) { Ok(Self { pkg: show.contains('p') || show.contains('a'), @@ -87,7 +137,7 @@ impl ArgParse for Show { unmerge: show.contains('u') || show.contains('a'), emerge: show.contains('e') || show.contains('a') }) } else { - Err(err(show.to_string(), src, valid)) + Err(ArgError::new(show, src).msg("Invalid letter").pos(valid)) } } } diff --git a/src/datetime.rs b/src/datetime.rs index 5b9d4f0..f64221d 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,4 +1,4 @@ -use crate::{config::{err, ArgParse}, +use crate::{config::{ArgError, ArgParse}, table::Disp, wtb, Conf, DurationStyle}; use anyhow::{bail, Error}; @@ -34,7 +34,7 @@ impl Default for DateStyle { } } impl ArgParse for DateStyle { - fn parse(s: &String, _: (), src: &'static str) -> Result { + fn parse(s: &String, _: (), src: &'static str) -> Result { Ok(Self(match s.as_str() { "ymd" | "d" => format_description!("[year]-[month]-[day]"), "ymdhms" | "dt" => format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), @@ -43,7 +43,7 @@ impl ArgParse for DateStyle { "rfc2822" | "2822" => format_description!("[weekday repr:short], [day] [month repr:short] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory]:[offset_minute]"), "compact" => format_description!("[year][month][day][hour][minute][second]"), "unix" => &[], - _ => return Err(err(s.to_owned(), src, "ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix")) + _ => return Err(ArgError::new(s, src).pos("ymd d ymdhms dt ymdhmso dto rfc3339 3339 rfc2822 2822 compact unix")) })) } } @@ -76,20 +76,25 @@ pub fn epoch_now() -> i64 { } /// Parse datetime in various formats, returning unix timestamp -pub fn parse_date(s: &str, offset: UtcOffset) -> Result { - let s = s.trim(); - i64::from_str(s).or_else(|e| { - debug!("{s}: bad timestamp: {e}"); - parse_date_yyyymmdd(s, offset) - }) - .or_else(|e| { - debug!("{s}: bad absolute date: {e}"); - parse_date_ago(s) - }) - .map_err(|e| { - debug!("{s}: bad relative date: {e}"); - "Enable debug log level for details" - }) +impl ArgParse for i64 { + fn parse(val: &String, offset: UtcOffset, src: &'static str) -> Result { + let s = val.trim(); + let et = match i64::from_str(s) { + Ok(i) => return Ok(i), + Err(et) => et, + }; + let ea = match parse_date_yyyymmdd(s, offset) { + Ok(i) => return Ok(i), + Err(ea) => ea, + }; + match parse_date_ago(s) { + Ok(i) => Ok(i), + Err(er) => { + let m = format!("Not a unix timestamp ({et}), absolute date ({ea}), or relative date ({er})"); + Err(ArgError::new(val, src).msg(m)) + }, + } + } } /// Parse a number of day/years/hours/etc in the past, relative to current time @@ -284,6 +289,9 @@ mod test { fn parse_3339(s: &str) -> OffsetDateTime { OffsetDateTime::parse(s, &Rfc3339).expect(s) } + fn parse_date(s: &str, o: UtcOffset) -> Result { + i64::parse(&String::from(s), o, "") + } fn ts(t: OffsetDateTime) -> i64 { t.unix_timestamp() } diff --git a/src/main.rs b/src/main.rs index 9a9d179..72784dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,6 @@ mod table; use crate::{commands::*, config::*, datetime::*, parse::AnsiStr}; use anyhow::Error; -use clap::{error::ErrorKind, ArgMatches, Error as ClapErr}; use log::*; use std::{io::IsTerminal, str::FromStr}; @@ -26,12 +25,15 @@ fn main() { match res { Ok(true) => std::process::exit(0), Ok(false) => std::process::exit(1), - Err(e) => match e.downcast::() { - Ok(ce) => ce.format(&mut cli::build_cli()).exit(), - Err(e) => { - log_err(e); - std::process::exit(2) - }, + Err(e) => { + match e.downcast::() { + Ok(ce) => ce.format(&mut cli::build_cli()).print().unwrap_or(()), + Err(e) => match e.downcast::() { + Ok(ae) => eprintln!("{ae}"), + Err(e) => log_err(e), + }, + } + std::process::exit(2) }, } } @@ -43,27 +45,6 @@ pub fn log_err(e: Error) { } } -/// Parse and return optional argument from an ArgMatches -/// -/// This is similar to clap's `get_one()` with `value_parser` except it allows late parsing with an -/// argument. -pub fn get_parse(args: &ArgMatches, - name: &str, - parse: P, - arg: A) - -> Result, ClapErr> - where P: FnOnce(&str, A) -> Result -{ - match args.get_one::(name) { - None => Ok(None), - Some(s) => match parse(s, arg) { - Ok(v) => Ok(Some(v)), - Err(e) => Err(ClapErr::raw(ErrorKind::InvalidValue, - format!("\"{s}\" isn't a valid for '--{name}': {e}"))), - }, - } -} - /// Alias to `write!(...).expect("write to buf")` just to save on typing/indent. #[macro_export] macro_rules! wtb { ($b:ident, $($arg:expr),+) => {write!($b, $($arg),+).expect("write to buf")} } From f1d4c1f7673245f9e041b3d36b5ce61dc64ddb2b Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 30 Jan 2024 22:53:14 +0000 Subject: [PATCH 14/28] conf: move `search`/`exact` to `Conf` --- src/commands.rs | 23 ++++------------------- src/config.rs | 12 ++++++++++++ src/parse/history.rs | 16 +++++++++------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 0d773eb..8fba75c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,12 +8,7 @@ use std::{collections::{BTreeMap, HashMap}, /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. pub fn cmd_log(args: &ArgMatches, gc: &Conf, sc: &ConfLog) -> Result { - let hist = get_hist(&gc.logfile, - gc.from, - gc.to, - sc.show, - args.get_many::("search").unwrap_or_default().cloned().collect(), - args.get_flag("exact"))?; + let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; let last = *args.get_one("last").unwrap_or(&usize::MAX); let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); @@ -143,12 +138,7 @@ impl Times { /// Then we compute the stats per ebuild, and print that. pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { let timespan_opt: Option<&Timespan> = args.get_one("group"); - let hist = get_hist(&gc.logfile, - gc.from, - gc.to, - sc.show, - args.get_many::("search").unwrap_or_default().cloned().collect(), - args.get_flag("exact"))?; + let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; let lim = *args.get_one("limit").unwrap(); let tsname = timespan_opt.map_or("", |timespan| timespan.name()); let mut tbls = Table::new(gc).align_left(0).align_left(1).margin(1, " "); @@ -334,7 +324,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result = BTreeMap::new(); let mut times: HashMap = HashMap::new(); for p in hist { @@ -436,12 +426,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result Result { - let hist = get_hist(&gc.logfile, - gc.from, - gc.to, - Show::m(), - args.get_many::("search").unwrap_or_default().cloned().collect(), - args.get_flag("exact"))?; + let hist = get_hist(&gc.logfile, gc.from, gc.to, Show::m(), &sc.search, sc.exact)?; let last = *args.get_one("last").unwrap_or(&usize::MAX); let lim = *args.get_one("limit").unwrap(); let mut pkg_starts: HashMap = HashMap::new(); diff --git a/src/config.rs b/src/config.rs index 9337ba3..0ecdfb7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,8 @@ pub struct Conf { } pub struct ConfLog { pub show: Show, + pub search: Vec, + pub exact: bool, pub starttime: bool, pub first: usize, } @@ -48,10 +50,14 @@ pub struct ConfPred { } pub struct ConfStats { pub show: Show, + pub search: Vec, + pub exact: bool, pub avg: Average, } pub struct ConfAccuracy { pub show: Show, + pub search: Vec, + pub exact: bool, pub avg: Average, } @@ -168,6 +174,8 @@ impl Conf { impl ConfLog { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, log, show, "musa", Show::m())?, + search: args.get_many("search").unwrap_or_default().cloned().collect(), + exact: args.get_flag("exact"), starttime: sel!(args, toml, log, starttime, (), false)?, first: *args.get_one("first").unwrap_or(&usize::MAX) }) } @@ -183,12 +191,16 @@ impl ConfPred { impl ConfStats { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, stats, show, "ptsa", Show::p())?, + search: args.get_many("search").unwrap_or_default().cloned().collect(), + exact: args.get_flag("exact"), avg: sel!(args, toml, stats, avg, (), Average::Median)? }) } } impl ConfAccuracy { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, accuracy, show, "mta", Show::mt())?, + search: args.get_many("search").unwrap_or_default().cloned().collect(), + exact: args.get_flag("exact"), avg: sel!(args, toml, accuracy, avg, (), Average::Median)? }) } } diff --git a/src/parse/history.rs b/src/parse/history.rs index 733aad5..fe57957 100644 --- a/src/parse/history.rs +++ b/src/parse/history.rs @@ -87,7 +87,7 @@ pub fn get_hist(file: &str, min_ts: Option, max_ts: Option, show: Show, - search_terms: Vec, + search_terms: &Vec, search_exact: bool) -> Result, Error> { debug!("File: {file}"); @@ -179,19 +179,21 @@ enum FilterStr { Re { r: RegexSet }, } impl FilterStr { - fn try_new(terms: Vec, exact: bool) -> Result { + fn try_new(terms: &Vec, exact: bool) -> Result { debug!("Search: {terms:?} {exact}"); Ok(match (terms.len(), exact) { (0, _) => Self::True, (_, true) => { let (b, c) = terms.iter().cloned().partition(|s| s.contains('/')); - Self::Eq { a: terms, b, c: c.into_iter().map(|s| format!("/{s}")).collect() } + Self::Eq { a: terms.clone(), + b, + c: c.into_iter().map(|s| format!("/{s}")).collect() } }, (1, false) => { Self::Re1 { r: RegexBuilder::new(&terms[0]).case_insensitive(true).build()? } }, (_, false) => { - Self::Re { r: RegexSetBuilder::new(&terms).case_insensitive(true).build()? } + Self::Re { r: RegexSetBuilder::new(terms).case_insensitive(true).build()? } }, }) } @@ -351,7 +353,7 @@ mod tests { merge: parse_merge, unmerge: parse_unmerge, emerge: false }, - filter_terms.clone(), + &filter_terms, exact).unwrap(); let re_atom = Regex::new("^[a-zA-Z0-9-]+/[a-zA-Z0-9_+-]+$").unwrap(); let re_version = Regex::new("^[0-9][0-9a-z._-]*$").unwrap(); @@ -518,7 +520,7 @@ mod tests { ("a.", false, "ab", true, true),]; for (terms, e, s, mpkg, mstr) in t { let t: Vec = terms.split_whitespace().map(str::to_string).collect(); - let f = FilterStr::try_new(t.clone(), e).unwrap(); + let f = FilterStr::try_new(&t, e).unwrap(); assert_eq!(f.match_pkg(s), mpkg, "filter({t:?}, {e}).match_pkg({s:?})"); assert_eq!(f.match_str(s), mstr, "filter({t:?}, {e}).match_str({s:?})"); } @@ -526,7 +528,7 @@ mod tests { #[test] fn split_atom() { - let f = FilterStr::try_new(vec![], false).unwrap(); + let f = FilterStr::try_new(&vec![], false).unwrap(); let g = |s| find_version(s, &f).map(|n| (&s[..n - 1], &s[n..])); assert_eq!(None, g("")); assert_eq!(None, g("a")); From 54868eff8b5a54cc46fe3ac19ec4949aad1fb44a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 31 Jan 2024 18:55:56 +0000 Subject: [PATCH 15/28] conf: Move `--first`/`--last` to `Conf` --- src/commands.rs | 19 ++++++------------- src/config.rs | 19 ++++++++++++++----- src/main.rs | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 8fba75c..1f95b99 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -7,14 +7,13 @@ use std::{collections::{BTreeMap, HashMap}, /// Straightforward display of merge events /// /// We store the start times in a hashmap to compute/print the duration when we reach a stop event. -pub fn cmd_log(args: &ArgMatches, gc: &Conf, sc: &ConfLog) -> Result { +pub fn cmd_log(gc: &Conf, sc: &ConfLog) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; - let last = *args.get_one("last").unwrap_or(&usize::MAX); let mut merges: HashMap = HashMap::new(); let mut unmerges: HashMap = HashMap::new(); let mut found = 0; let mut sync_start: Option = None; - let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); + let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(sc.last); tbl.header(["Date", "Duration", "Package/Repo"]); for p in hist { match p { @@ -295,12 +294,7 @@ fn cmd_stats_group(gc: &Conf, /// Very similar to cmd_summary except we want total build time for a list of ebuilds. pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); - let first = *args.get_one("first").unwrap_or(&usize::MAX); - let last = match args.get_one("last") { - Some(&n) if sc.show.tot => n + 1, - Some(&n) => n, - None => usize::MAX, - }; + let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; let lim = *args.get_one("limit").unwrap(); let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); @@ -386,7 +380,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result 0 { let stage = get_buildlog(&p, &tmpdirs).unwrap_or_default(); tbl.row([&[&gc.pkg, &p.ebuild_version()], @@ -407,7 +401,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result 0 { s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &totunknown, &gc.clr, &" unknown"]); } - let tothidden = totcount.saturating_sub(first.min(last - 1)); + let tothidden = totcount.saturating_sub(sc.first.min(last - 1)); if tothidden > 0 { s.extend::<[&dyn Disp; 5]>([&", ", &gc.cnt, &tothidden, &gc.clr, &" hidden"]); } @@ -427,13 +421,12 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, Show::m(), &sc.search, sc.exact)?; - let last = *args.get_one("last").unwrap_or(&usize::MAX); let lim = *args.get_one("limit").unwrap(); let mut pkg_starts: HashMap = HashMap::new(); let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); let mut found = false; - let mut tbl = Table::new(gc).align_left(0).align_left(1).last(last); + let mut tbl = Table::new(gc).align_left(0).align_left(1).last(sc.last); tbl.header(["Date", "Package", "Real", "Predicted", "Error"]); for p in hist { match p { diff --git a/src/config.rs b/src/config.rs index 0ecdfb7..2488fb6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ pub use types::*; pub enum Configs { - Log(ArgMatches, Conf, ConfLog), + Log(Conf, ConfLog), Stats(ArgMatches, Conf, ConfStats), Predict(ArgMatches, Conf, ConfPred), Accuracy(ArgMatches, Conf, ConfAccuracy), @@ -43,10 +43,13 @@ pub struct ConfLog { pub exact: bool, pub starttime: bool, pub first: usize, + pub last: usize, } pub struct ConfPred { pub show: Show, pub avg: Average, + pub first: usize, + pub last: usize, } pub struct ConfStats { pub show: Show, @@ -59,6 +62,7 @@ pub struct ConfAccuracy { pub search: Vec, pub exact: bool, pub avg: Average, + pub last: usize, } impl Configs { @@ -77,7 +81,7 @@ impl Configs { log::trace!("{:?}", toml); let conf = Conf::try_new(&args, &toml)?; Ok(match args.subcommand() { - Some(("log", sub)) => Self::Log(sub.clone(), conf, ConfLog::try_new(sub, &toml)?), + Some(("log", sub)) => Self::Log(conf, ConfLog::try_new(sub, &toml)?), Some(("stats", sub)) => Self::Stats(sub.clone(), conf, ConfStats::try_new(sub, &toml)?), Some(("predict", sub)) => { Self::Predict(sub.clone(), conf, ConfPred::try_new(sub, &toml)?) @@ -177,14 +181,17 @@ impl ConfLog { search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), starttime: sel!(args, toml, log, starttime, (), false)?, - first: *args.get_one("first").unwrap_or(&usize::MAX) }) + first: *args.get_one("first").unwrap_or(&usize::MAX), + last: *args.get_one("last").unwrap_or(&usize::MAX) }) } } impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, - avg: sel!(args, toml, predict, avg, (), Average::Median)? }) + avg: sel!(args, toml, predict, avg, (), Average::Median)?, + first: *args.get_one("first").unwrap_or(&usize::MAX), + last: *args.get_one("last").unwrap_or(&usize::MAX) }) } } @@ -196,11 +203,13 @@ impl ConfStats { avg: sel!(args, toml, stats, avg, (), Average::Median)? }) } } + impl ConfAccuracy { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, accuracy, show, "mta", Show::mt())?, search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), - avg: sel!(args, toml, accuracy, avg, (), Average::Median)? }) + avg: sel!(args, toml, accuracy, avg, (), Average::Median)?, + last: *args.get_one("last").unwrap_or(&usize::MAX) }) } } diff --git a/src/main.rs b/src/main.rs index 72784dc..9258ff7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ use std::{io::IsTerminal, str::FromStr}; fn main() { let res = match Configs::load() { - Ok(Configs::Log(args, gc, sc)) => cmd_log(&args, &gc, &sc), + Ok(Configs::Log(gc, sc)) => cmd_log(&gc, &sc), Ok(Configs::Stats(args, gc, sc)) => cmd_stats(&args, &gc, &sc), Ok(Configs::Predict(args, gc, sc)) => cmd_predict(&args, &gc, &sc), Ok(Configs::Accuracy(args, gc, sc)) => cmd_accuracy(&args, &gc, &sc), From 3c84f71e09c410d4d49c03fa7eec60e251151f0a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sat, 3 Feb 2024 21:53:13 +0000 Subject: [PATCH 16/28] conf: Move `--limit` to `Conf`, make it configurable --- emlop.toml | 3 +++ src/cli.rs | 2 -- src/commands.rs | 20 ++++++++------------ src/config.rs | 12 ++++++++---- src/config/toml.rs | 3 +++ src/config/types.rs | 35 ++++++++++++++++++++++++++--------- src/main.rs | 2 +- 7 files changed, 49 insertions(+), 28 deletions(-) diff --git a/emlop.toml b/emlop.toml index fa7dfd3..4ff8341 100644 --- a/emlop.toml +++ b/emlop.toml @@ -18,9 +18,12 @@ [predict] #show = "emt" #avg = "arith" +#limit = 20 [stats] #show = "pts" #avg = "arith" +#limit = 20 [accuracy] #show = "mt" #avg = "arith" +#limit = 20 diff --git a/src/cli.rs b/src/cli.rs index f846f07..275eb2e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -143,8 +143,6 @@ pub fn build_cli_nocomplete() -> Command { .display_order(2) .num_args(1) .value_name("num") - .value_parser(value_parser!(u16).range(1..)) - .default_value("10") .help_heading("Stats") .help("Use the last merge times to predict durations"); let avg = diff --git a/src/commands.rs b/src/commands.rs index 1f95b99..7311830 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -138,7 +138,6 @@ impl Times { pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { let timespan_opt: Option<&Timespan> = args.get_one("group"); let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; - let lim = *args.get_one("limit").unwrap(); let tsname = timespan_opt.map_or("", |timespan| timespan.name()); let mut tbls = Table::new(gc).align_left(0).align_left(1).margin(1, " "); tbls.header([tsname, "Repo", "Sync count", "Total time", "Predict time"]); @@ -174,7 +173,7 @@ pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result nextts { let group = timespan.at(curts, gc.date_offset); - cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, lim, group, &sync_time, + cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); @@ -218,7 +217,7 @@ pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result, tblp: &mut Table<8>, tblt: &mut Table<7>, - lim: u16, group: String, sync_time: &BTreeMap, pkg_time: &BTreeMap) { @@ -251,7 +249,7 @@ fn cmd_stats_group(gc: &Conf, &[repo], &[&gc.cnt, &time.count], &[&FmtDur(time.tot)], - &[&FmtDur(time.pred(lim, sc.avg))]]); + &[&FmtDur(time.pred(sc.lim, sc.avg))]]); } } // Packages @@ -261,10 +259,10 @@ fn cmd_stats_group(gc: &Conf, &[&gc.pkg, pkg], &[&gc.cnt, &merge.count], &[&FmtDur(merge.tot)], - &[&FmtDur(merge.pred(lim, sc.avg))], + &[&FmtDur(merge.pred(sc.lim, sc.avg))], &[&gc.cnt, &unmerge.count], &[&FmtDur(unmerge.tot)], - &[&FmtDur(unmerge.pred(lim, sc.avg))]]); + &[&FmtDur(unmerge.pred(sc.lim, sc.avg))]]); } } // Totals @@ -295,7 +293,6 @@ fn cmd_stats_group(gc: &Conf, pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; - let lim = *args.get_one("limit").unwrap(); let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); @@ -368,7 +365,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { - let pred = tv.pred(lim, sc.avg); + let pred = tv.pred(sc.lim, sc.avg); (pred, pred) }, None => { @@ -419,9 +416,8 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result 0) } -pub fn cmd_accuracy(args: &ArgMatches, gc: &Conf, sc: &ConfAccuracy) -> Result { +pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, Show::m(), &sc.search, sc.exact)?; - let lim = *args.get_one("limit").unwrap(); let mut pkg_starts: HashMap = HashMap::new(); let mut pkg_times: BTreeMap = BTreeMap::new(); let mut pkg_errs: BTreeMap> = BTreeMap::new(); @@ -439,7 +435,7 @@ pub fn cmd_accuracy(args: &ArgMatches, gc: &Conf, sc: &ConfAccuracy) -> Result { if sc.show.merge { tbl.row([&[&FmtDate(ts)], diff --git a/src/config.rs b/src/config.rs index 2488fb6..e1001ca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ pub enum Configs { Log(Conf, ConfLog), Stats(ArgMatches, Conf, ConfStats), Predict(ArgMatches, Conf, ConfPred), - Accuracy(ArgMatches, Conf, ConfAccuracy), + Accuracy(Conf, ConfAccuracy), Complete(ArgMatches), } @@ -50,12 +50,14 @@ pub struct ConfPred { pub avg: Average, pub first: usize, pub last: usize, + pub lim: u16, } pub struct ConfStats { pub show: Show, pub search: Vec, pub exact: bool, pub avg: Average, + pub lim: u16, } pub struct ConfAccuracy { pub show: Show, @@ -63,6 +65,7 @@ pub struct ConfAccuracy { pub exact: bool, pub avg: Average, pub last: usize, + pub lim: u16, } impl Configs { @@ -86,9 +89,7 @@ impl Configs { Some(("predict", sub)) => { Self::Predict(sub.clone(), conf, ConfPred::try_new(sub, &toml)?) }, - Some(("accuracy", sub)) => { - Self::Accuracy(sub.clone(), conf, ConfAccuracy::try_new(sub, &toml)?) - }, + Some(("accuracy", sub)) => Self::Accuracy(conf, ConfAccuracy::try_new(sub, &toml)?), Some(("complete", sub)) => Self::Complete(sub.clone()), _ => unreachable!("clap should have exited already"), }) @@ -190,6 +191,7 @@ impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, avg: sel!(args, toml, predict, avg, (), Average::Median)?, + lim: sel!(args, toml, predict, limit, 1..u16::MAX, 10)?, first: *args.get_one("first").unwrap_or(&usize::MAX), last: *args.get_one("last").unwrap_or(&usize::MAX) }) } @@ -200,6 +202,7 @@ impl ConfStats { Ok(Self { show: sel!(args, toml, stats, show, "ptsa", Show::p())?, search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), + lim: sel!(args, toml, stats, limit, 1..u16::MAX, 10)?, avg: sel!(args, toml, stats, avg, (), Average::Median)? }) } } @@ -210,6 +213,7 @@ impl ConfAccuracy { search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), avg: sel!(args, toml, accuracy, avg, (), Average::Median)?, + lim: sel!(args, toml, accuracy, limit, 1..u16::MAX, 10)?, last: *args.get_one("last").unwrap_or(&usize::MAX) }) } } diff --git a/src/config/toml.rs b/src/config/toml.rs index 68c908b..5494365 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -11,16 +11,19 @@ pub struct TomlLog { pub struct TomlPred { pub show: Option, pub avg: Option, + pub limit: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { pub show: Option, pub avg: Option, + pub limit: Option, } #[derive(Deserialize, Debug)] pub struct TomlAccuracy { pub show: Option, pub avg: Option, + pub limit: Option, } #[derive(Deserialize, Debug, Default)] pub struct Toml { diff --git a/src/config/types.rs b/src/config/types.rs index 8f73f65..5f9cf72 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1,3 +1,5 @@ +use std::{ops::Range, str::FromStr}; + /// Parsing trait for args /// /// Similar to std::convert::From but takes extra context and returns a custom error @@ -5,16 +7,16 @@ pub trait ArgParse { fn parse(val: &T, arg: A, src: &'static str) -> Result where Self: Sized; } -impl ArgParse for bool { - fn parse(b: &bool, _: (), _src: &'static str) -> Result { - Ok(*b) - } -} impl ArgParse for String { fn parse(s: &String, _: (), _src: &'static str) -> Result { Ok((*s).clone()) } } +impl ArgParse for bool { + fn parse(b: &bool, _: (), _src: &'static str) -> Result { + Ok(*b) + } +} impl ArgParse for bool { fn parse(s: &String, _: (), src: &'static str) -> Result { match s.as_str() { @@ -24,6 +26,21 @@ impl ArgParse for bool { } } } +impl ArgParse> for u16 { + fn parse(s: &String, r: Range, src: &'static str) -> Result { + let i = u16::from_str(s).map_err(|_| ArgError::new(s, src).msg("Not an integer"))?; + Self::parse(&i, r, src) + } +} +impl ArgParse> for u16 { + fn parse(i: &u16, r: Range, src: &'static str) -> Result { + if r.contains(i) { + Ok(*i) + } else { + Err(ArgError::new(i, src).msg(format!("Should be between {} and {}", r.start, r.end))) + } + } +} /// Argument parsing error @@ -38,12 +55,12 @@ pub struct ArgError { } impl ArgError { /// Instantiate basic error with value and source - pub fn new(val: impl Into, src: &'static str) -> Self { - Self { val: val.into(), src, msg: String::new(), possible: "" } + pub fn new(val: impl ToString, src: &'static str) -> Self { + Self { val: val.to_string(), src, msg: String::new(), possible: "" } } /// Set extra error message - pub fn msg(mut self, msg: impl Into) -> Self { - self.msg = msg.into(); + pub fn msg(mut self, msg: impl ToString) -> Self { + self.msg = msg.to_string(); self } /// Set possible values (as a space-delimited string or as a set of letters) diff --git a/src/main.rs b/src/main.rs index 9258ff7..3a46b8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,7 @@ fn main() { Ok(Configs::Log(gc, sc)) => cmd_log(&gc, &sc), Ok(Configs::Stats(args, gc, sc)) => cmd_stats(&args, &gc, &sc), Ok(Configs::Predict(args, gc, sc)) => cmd_predict(&args, &gc, &sc), - Ok(Configs::Accuracy(args, gc, sc)) => cmd_accuracy(&args, &gc, &sc), + Ok(Configs::Accuracy(gc, sc)) => cmd_accuracy(&gc, &sc), Ok(Configs::Complete(args)) => cmd_complete(&args), Err(e) => Err(e), }; From 44fe50ababfefd42d38df79b45e5ff5694e595f8 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sun, 4 Feb 2024 14:56:11 +0000 Subject: [PATCH 17/28] conf: Move `--group` to `Conf`, make configurable Misc improvements: * Now accepting verbose group values * Now accepting `(n)one` grouping (to override config) * Simplified code by using that `None` variant instead of an `Option` --- emlop.toml | 1 + src/cli.rs | 9 ++++----- src/commands.rs | 20 +++++++++---------- src/config.rs | 8 +++++--- src/config/toml.rs | 1 + src/datetime.rs | 48 +++++++++++++++++++++++++++++----------------- src/main.rs | 2 +- 7 files changed, 51 insertions(+), 38 deletions(-) diff --git a/emlop.toml b/emlop.toml index 4ff8341..44ce846 100644 --- a/emlop.toml +++ b/emlop.toml @@ -23,6 +23,7 @@ #show = "pts" #avg = "arith" #limit = 20 +#group = "y" [accuracy] #show = "mt" #avg = "arith" diff --git a/src/cli.rs b/src/cli.rs index 275eb2e..b6e68f3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -130,13 +130,12 @@ pub fn build_cli_nocomplete() -> Command { let group = Arg::new("group").short('g') .long("groupby") .display_order(1) - .value_name("y,m,w,d") - .value_parser(value_parser!(crate::datetime::Timespan)) + .value_name("y,m,w,d,n") .hide_possible_values(true) .help_heading("Stats") - .help("Group by (y)ear, (m)onth, (w)eek, or (d)ay") - .long_help("Group by (y)ear, (m)onth, (w)eek, or (d)ay\n\ - The grouping key is displayed in the first column. \ + .help("Group by (y)ear, (m)onth, (w)eek, (d)ay, (n)one") + .long_help("Group by (y)ear, (m)onth, (w)eek, (d)ay, or (n)one\n\ + The grouping key is displayed in the first column.\n\ Weeks start on monday and are formated as \ 'year-weeknumber'."); let limit = Arg::new("limit").long("limit") diff --git a/src/commands.rs b/src/commands.rs index 7311830..8acf42f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -135,14 +135,12 @@ impl Times { /// /// First loop is like cmd_list but we store the merge time for each ebuild instead of printing it. /// Then we compute the stats per ebuild, and print that. -pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result { - let timespan_opt: Option<&Timespan> = args.get_one("group"); +pub fn cmd_stats(gc: &Conf, sc: &ConfStats) -> Result { let hist = get_hist(&gc.logfile, gc.from, gc.to, sc.show, &sc.search, sc.exact)?; - let tsname = timespan_opt.map_or("", |timespan| timespan.name()); let mut tbls = Table::new(gc).align_left(0).align_left(1).margin(1, " "); - tbls.header([tsname, "Repo", "Sync count", "Total time", "Predict time"]); + tbls.header([sc.group.name(), "Repo", "Sync count", "Total time", "Predict time"]); let mut tblp = Table::new(gc).align_left(0).align_left(1).margin(1, " "); - tblp.header([tsname, + tblp.header([sc.group.name(), "Package", "Merge count", "Total time", @@ -151,7 +149,7 @@ pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result Result nextts { - let group = timespan.at(curts, gc.date_offset); + let group = sc.group.at(curts, gc.date_offset); cmd_stats_group(gc, sc, &mut tbls, &mut tblp, &mut tblt, group, &sync_time, &pkg_time); sync_time.clear(); pkg_time.clear(); - nextts = timespan.next(t, gc.date_offset); + nextts = sc.group.next(t, gc.date_offset); curts = t; } } @@ -216,7 +214,7 @@ pub fn cmd_stats(args: &ArgMatches, gc: &Conf, sc: &ConfStats) -> Result Self::Log(conf, ConfLog::try_new(sub, &toml)?), - Some(("stats", sub)) => Self::Stats(sub.clone(), conf, ConfStats::try_new(sub, &toml)?), + Some(("stats", sub)) => Self::Stats(conf, ConfStats::try_new(sub, &toml)?), Some(("predict", sub)) => { Self::Predict(sub.clone(), conf, ConfPred::try_new(sub, &toml)?) }, @@ -203,7 +204,8 @@ impl ConfStats { search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), lim: sel!(args, toml, stats, limit, 1..u16::MAX, 10)?, - avg: sel!(args, toml, stats, avg, (), Average::Median)? }) + avg: sel!(args, toml, stats, avg, (), Average::Median)?, + group: sel!(args, toml, stats, group, (), Timespan::None)? }) } } diff --git a/src/config/toml.rs b/src/config/toml.rs index 5494365..df15f83 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -18,6 +18,7 @@ pub struct TomlStats { pub show: Option, pub avg: Option, pub limit: Option, + pub group: Option, } #[derive(Deserialize, Debug)] pub struct TomlAccuracy { diff --git a/src/datetime.rs b/src/datetime.rs index f64221d..121f2d6 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -186,28 +186,37 @@ fn parse_date_yyyymmdd(s: &str, offset: UtcOffset) -> Result { Ok(OffsetDateTime::try_from(p)?.unix_timestamp()) } -#[derive(Debug, Clone, Copy, clap::ValueEnum)] +#[derive(Clone, Copy)] pub enum Timespan { - #[clap(id("y"))] Year, - #[clap(id("m"))] Month, - #[clap(id("w"))] Week, - #[clap(id("d"))] Day, + None, +} +impl ArgParse for Timespan { + fn parse(v: &String, _: (), s: &'static str) -> Result { + match v.as_str() { + "y" | "year" => Ok(Self::Year), + "m" | "month" => Ok(Self::Month), + "w" | "week" => Ok(Self::Week), + "d" | "day" => Ok(Self::Day), + "n" | "none" => Ok(Self::None), + _ => Err(ArgError::new(v, s).pos("(y)ear (m)onth (w)eek (d)ay (n)one")), + } + } } impl Timespan { /// Given a unix timestamp, advance to the beginning of the next year/month/week/day. pub fn next(&self, ts: i64, offset: UtcOffset) -> i64 { let d = OffsetDateTime::from_unix_timestamp(ts).unwrap().to_offset(offset).date(); let d2 = match self { - Timespan::Year => Date::from_calendar_date(d.year() + 1, Month::January, 1).unwrap(), - Timespan::Month => { + Self::Year => Date::from_calendar_date(d.year() + 1, Month::January, 1).unwrap(), + Self::Month => { let year = if d.month() == Month::December { d.year() + 1 } else { d.year() }; Date::from_calendar_date(year, d.month().next(), 1).unwrap() }, - Timespan::Week => { + Self::Week => { let til_monday = match d.weekday() { Weekday::Monday => 7, Weekday::Tuesday => 6, @@ -219,29 +228,32 @@ impl Timespan { }; d.checked_add(Duration::days(til_monday)).unwrap() }, - Timespan::Day => d.checked_add(Duration::DAY).unwrap(), + Self::Day => d.checked_add(Duration::DAY).unwrap(), + Self::None => panic!("Called next() on a Timespan::None"), }; let res = d2.with_hms(0, 0, 0).unwrap().assume_offset(offset).unix_timestamp(); - debug!("{} + {:?} = {}", fmt_utctime(ts), self, fmt_utctime(res)); + debug!("{} + {} = {}", fmt_utctime(ts), self.name(), fmt_utctime(res)); res } pub fn at(&self, ts: i64, offset: UtcOffset) -> String { let d = OffsetDateTime::from_unix_timestamp(ts).unwrap().to_offset(offset); match self { - Timespan::Year => d.format(format_description!("[year]")).unwrap(), - Timespan::Month => d.format(format_description!("[year]-[month]")).unwrap(), - Timespan::Week => d.format(format_description!("[year]-[week_number]")).unwrap(), - Timespan::Day => d.format(format_description!("[year]-[month]-[day]")).unwrap(), + Self::Year => d.format(format_description!("[year]")).unwrap(), + Self::Month => d.format(format_description!("[year]-[month]")).unwrap(), + Self::Week => d.format(format_description!("[year]-[week_number]")).unwrap(), + Self::Day => d.format(format_description!("[year]-[month]-[day]")).unwrap(), + Self::None => String::new(), } } pub const fn name(&self) -> &'static str { match self { - Timespan::Year => "Year", - Timespan::Month => "Month", - Timespan::Week => "Week", - Timespan::Day => "Date", + Self::Year => "Year", + Self::Month => "Month", + Self::Week => "Week", + Self::Day => "Date", + Self::None => "", } } } diff --git a/src/main.rs b/src/main.rs index 3a46b8c..547de1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,7 @@ use std::{io::IsTerminal, str::FromStr}; fn main() { let res = match Configs::load() { Ok(Configs::Log(gc, sc)) => cmd_log(&gc, &sc), - Ok(Configs::Stats(args, gc, sc)) => cmd_stats(&args, &gc, &sc), + Ok(Configs::Stats(gc, sc)) => cmd_stats(&gc, &sc), Ok(Configs::Predict(args, gc, sc)) => cmd_predict(&args, &gc, &sc), Ok(Configs::Accuracy(gc, sc)) => cmd_accuracy(&gc, &sc), Ok(Configs::Complete(args)) => cmd_complete(&args), From ecb1e2da5537be86e08e8c2f1f9c5f8baae7cf60 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Sun, 4 Feb 2024 21:46:45 +0000 Subject: [PATCH 18/28] conf: Move `--resume` to `Conf` I initially made this toml-configurable, but I don't think anybody would want to set a different default. Added `current` enum variant to replace the use of `Option<_>`. Was needed to override a toml setting, but still seems valuable without it. The formatting difference between clap::Error and ArgError is not big, but I'll still want to harmonize things somehow. --- src/cli.rs | 2 +- src/commands.rs | 5 ++--- src/config.rs | 2 ++ src/config/types.rs | 15 ++++++++++++++- src/main.rs | 11 ----------- src/parse/current.rs | 4 ++-- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b6e68f3..81d21e6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -280,7 +280,7 @@ pub fn build_cli_nocomplete() -> Command { no|n: Never use resume list"; let resume = Arg::new("resume").long("resume") .value_name("source") - .value_parser(value_parser!(crate::ResumeKind)) + .value_parser(value_parser!(crate::config::ResumeKind)) .hide_possible_values(true) .num_args(..=1) .default_missing_value("any") diff --git a/src/commands.rs b/src/commands.rs index 8acf42f..317b223 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -291,7 +291,6 @@ fn cmd_stats_group(gc: &Conf, pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; - let resume = args.get_one("resume").copied(); let unknown_pred = *args.get_one("unknown").unwrap(); let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); let mut tmpdirs: Vec = args.get_many("tmpdir").unwrap().cloned().collect(); @@ -306,7 +305,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result Result = if std::io::stdin().is_terminal() { // From resume data + emerge.log after current merge process start time - let mut r = get_resume(resume.unwrap_or(ResumeKind::Main)); + let mut r = get_resume(sc.resume); for p in started.iter().filter(|&(_, t)| *t > cms).map(|(p, _)| p) { if !r.contains(p) { r.push(p.clone()) diff --git a/src/config.rs b/src/config.rs index cacae15..f1009f6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,6 +51,7 @@ pub struct ConfPred { pub first: usize, pub last: usize, pub lim: u16, + pub resume: ResumeKind, } pub struct ConfStats { pub show: Show, @@ -193,6 +194,7 @@ impl ConfPred { Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, avg: sel!(args, toml, predict, avg, (), Average::Median)?, lim: sel!(args, toml, predict, limit, 1..u16::MAX, 10)?, + resume: *args.get_one("resume").unwrap_or(&ResumeKind::Current), first: *args.get_one("first").unwrap_or(&usize::MAX), last: *args.get_one("last").unwrap_or(&usize::MAX) }) } diff --git a/src/config/types.rs b/src/config/types.rs index 5f9cf72..da38b69 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -114,11 +114,24 @@ impl ArgParse for Average { "m" | "median" => Ok(Self::Median), "wa" | "weighted-arith" => Ok(Self::WeightedArith), "wm" | "weighted-median" => Ok(Self::WeightedMedian), - _ => Err(ArgError::new(v, s).pos("arith median weightedarith weigtedmedian a m wa wm")), + _ => Err(ArgError::new(v, s).pos("(a)rith (m)edian wa/weightedarith wm/weigtedmedian")), } } } +#[derive(Clone, Copy, clap::ValueEnum)] +pub enum ResumeKind { + #[clap(hide(true))] + Current, + #[clap(alias("a"))] + Any, + #[clap(alias("m"))] + Main, + #[clap(alias("b"))] + Backup, + #[clap(alias("n"))] + No, +} #[derive(Clone, Copy)] pub struct Show { diff --git a/src/main.rs b/src/main.rs index 547de1e..88e4e9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,17 +50,6 @@ pub fn log_err(e: Error) { macro_rules! wtb { ($b:ident, $($arg:expr),+) => {write!($b, $($arg),+).expect("write to buf")} } -#[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)] -pub enum ResumeKind { - #[clap(alias("a"))] - Any, - #[clap(alias("m"))] - Main, - #[clap(alias("b"))] - Backup, - #[clap(alias("n"))] - No, -} #[derive(Clone, Copy, clap::ValueEnum)] pub enum DurationStyle { diff --git a/src/parse/current.rs b/src/parse/current.rs index 2e6201e..890030a 100644 --- a/src/parse/current.rs +++ b/src/parse/current.rs @@ -81,14 +81,14 @@ pub fn get_resume(kind: ResumeKind) -> Vec { get_resume_priv(kind, "/var/cache/edb/mtimedb").unwrap_or_default() } fn get_resume_priv(kind: ResumeKind, file: &str) -> Option> { - if kind == ResumeKind::No { + if matches!(kind, ResumeKind::No) { return Some(vec![]); } let reader = File::open(file).map_err(|e| warn!("Cannot open {file:?}: {e}")).ok()?; let db: Mtimedb = from_reader(reader).map_err(|e| warn!("Cannot parse {file:?}: {e}")).ok()?; let r = match kind { ResumeKind::Any => db.resume.or(db.resume_backup)?, - ResumeKind::Main => db.resume?, + ResumeKind::Main | ResumeKind::Current => db.resume?, ResumeKind::Backup => db.resume_backup?, ResumeKind::No => unreachable!(), }; From 7aabeb5846dc2b7b5b8b8440d76744d1e29462b5 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Mon, 5 Feb 2024 20:23:01 +0000 Subject: [PATCH 19/28] conf: Move `--unknown` to `Conf`, make configurable Changed integer parsing to i64, relying on runtime range-check and casts to get other integers. This avoids the need for multiple/generic integer parsing. --- emlop.toml | 1 + src/cli.rs | 2 -- src/commands.rs | 3 +-- src/config.rs | 8 +++++--- src/config/toml.rs | 7 ++++--- src/config/types.rs | 10 +++++----- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/emlop.toml b/emlop.toml index 44ce846..9d413c4 100644 --- a/emlop.toml +++ b/emlop.toml @@ -19,6 +19,7 @@ #show = "emt" #avg = "arith" #limit = 20 +#unknown = 300 [stats] #show = "pts" #avg = "arith" diff --git a/src/cli.rs b/src/cli.rs index 81d21e6..31d67dd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -161,8 +161,6 @@ pub fn build_cli_nocomplete() -> Command { .display_order(4) .num_args(1) .value_name("secs") - .value_parser(value_parser!(i64).range(0..)) - .default_value("10") .help_heading("Stats") .help("Assume unkown packages take seconds to merge"); diff --git a/src/commands.rs b/src/commands.rs index 317b223..7448d84 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -291,7 +291,6 @@ fn cmd_stats_group(gc: &Conf, pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; - let unknown_pred = *args.get_one("unknown").unwrap(); let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); let mut tmpdirs: Vec = args.get_many("tmpdir").unwrap().cloned().collect(); @@ -367,7 +366,7 @@ pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { totunknown += 1; - (-1, unknown_pred) + (-1, sc.unknown) }, }; totpredict += std::cmp::max(0, pred - elapsed); diff --git a/src/config.rs b/src/config.rs index f1009f6..0cd5b64 100644 --- a/src/config.rs +++ b/src/config.rs @@ -52,6 +52,7 @@ pub struct ConfPred { pub last: usize, pub lim: u16, pub resume: ResumeKind, + pub unknown: i64, } pub struct ConfStats { pub show: Show, @@ -193,7 +194,8 @@ impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, avg: sel!(args, toml, predict, avg, (), Average::Median)?, - lim: sel!(args, toml, predict, limit, 1..u16::MAX, 10)?, + lim: sel!(args, toml, predict, limit, 1..65000, 10)? as u16, + unknown: sel!(args, toml, predict, unknown, 0..3600, 10)?, resume: *args.get_one("resume").unwrap_or(&ResumeKind::Current), first: *args.get_one("first").unwrap_or(&usize::MAX), last: *args.get_one("last").unwrap_or(&usize::MAX) }) @@ -205,7 +207,7 @@ impl ConfStats { Ok(Self { show: sel!(args, toml, stats, show, "ptsa", Show::p())?, search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), - lim: sel!(args, toml, stats, limit, 1..u16::MAX, 10)?, + lim: sel!(args, toml, stats, limit, 1..65000, 10)? as u16, avg: sel!(args, toml, stats, avg, (), Average::Median)?, group: sel!(args, toml, stats, group, (), Timespan::None)? }) } @@ -217,7 +219,7 @@ impl ConfAccuracy { search: args.get_many("search").unwrap_or_default().cloned().collect(), exact: args.get_flag("exact"), avg: sel!(args, toml, accuracy, avg, (), Average::Median)?, - lim: sel!(args, toml, accuracy, limit, 1..u16::MAX, 10)?, + lim: sel!(args, toml, accuracy, limit, 1..65000, 10)? as u16, last: *args.get_one("last").unwrap_or(&usize::MAX) }) } } diff --git a/src/config/toml.rs b/src/config/toml.rs index df15f83..d207b40 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -11,20 +11,21 @@ pub struct TomlLog { pub struct TomlPred { pub show: Option, pub avg: Option, - pub limit: Option, + pub limit: Option, + pub unknown: Option, } #[derive(Deserialize, Debug)] pub struct TomlStats { pub show: Option, pub avg: Option, - pub limit: Option, + pub limit: Option, pub group: Option, } #[derive(Deserialize, Debug)] pub struct TomlAccuracy { pub show: Option, pub avg: Option, - pub limit: Option, + pub limit: Option, } #[derive(Deserialize, Debug, Default)] pub struct Toml { diff --git a/src/config/types.rs b/src/config/types.rs index da38b69..8e178f6 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -26,14 +26,14 @@ impl ArgParse for bool { } } } -impl ArgParse> for u16 { - fn parse(s: &String, r: Range, src: &'static str) -> Result { - let i = u16::from_str(s).map_err(|_| ArgError::new(s, src).msg("Not an integer"))?; +impl ArgParse> for i64 { + fn parse(s: &String, r: Range, src: &'static str) -> Result { + let i = i64::from_str(s).map_err(|_| ArgError::new(s, src).msg("Not an integer"))?; Self::parse(&i, r, src) } } -impl ArgParse> for u16 { - fn parse(i: &u16, r: Range, src: &'static str) -> Result { +impl ArgParse> for i64 { + fn parse(i: &i64, r: Range, src: &'static str) -> Result { if r.contains(i) { Ok(*i) } else { From f4fbf02293a137d913fe7554135a0bf4c5ec43f7 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 20:35:01 +0000 Subject: [PATCH 20/28] conf: Move `--tmpdir` to `Conf`, make configurable This is a quick solution with carefree cloning, I hope to make things leaner at the end. --- emlop.toml | 1 + src/cli.rs | 2 -- src/commands.rs | 7 +++---- src/config.rs | 17 ++++++++++++----- src/config/toml.rs | 3 ++- src/main.rs | 3 +-- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/emlop.toml b/emlop.toml index 9d413c4..90024cd 100644 --- a/emlop.toml +++ b/emlop.toml @@ -20,6 +20,7 @@ #avg = "arith" #limit = 20 #unknown = 300 +#tmpdir = ["/foo", "/bar"] [stats] #show = "pts" #avg = "arith" diff --git a/src/cli.rs b/src/cli.rs index 31d67dd..0a51640 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -262,7 +262,6 @@ pub fn build_cli_nocomplete() -> Command { .long("tmpdir") .num_args(1) .action(Append) - .default_value("/var/tmp") .value_parser(value_parser!(PathBuf)) .display_order(2) .help("Location of portage tmpdir") @@ -458,7 +457,6 @@ mod test { assert_eq!(one!(ColorStyle, "color", "l --color never"), Some(&ColorStyle::Never)); let pathvec = |s: &str| Some(s.split_whitespace().map(PathBuf::from).collect()); - assert_eq!(many!(PathBuf, "tmpdir", "p"), pathvec("/var/tmp")); assert_eq!(many!(PathBuf, "tmpdir", "p --tmpdir a"), pathvec("a")); assert_eq!(many!(PathBuf, "tmpdir", "p --tmpdir a --tmpdir b"), pathvec("a b")); } diff --git a/src/commands.rs b/src/commands.rs index 7448d84..f301ac0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,8 +1,7 @@ use crate::{datetime::*, parse::*, proces::*, table::*, *}; use clap::ArgMatches; use std::{collections::{BTreeMap, HashMap}, - io::stdin, - path::PathBuf}; + io::stdin}; /// Straightforward display of merge events /// @@ -288,11 +287,11 @@ fn cmd_stats_group(gc: &Conf, /// Predict future merge time /// /// Very similar to cmd_summary except we want total build time for a list of ebuilds. -pub fn cmd_predict(args: &ArgMatches, gc: &Conf, sc: &ConfPred) -> Result { +pub fn cmd_predict(gc: &Conf, sc: &ConfPred) -> Result { let now = epoch_now(); let last = if sc.show.tot { sc.last.saturating_add(1) } else { sc.last }; let mut tbl = Table::new(gc).align_left(0).align_left(2).margin(2, " ").last(last); - let mut tmpdirs: Vec = args.get_many("tmpdir").unwrap().cloned().collect(); + let mut tmpdirs = sc.tmpdirs.clone(); // Gather and print info about current merge process. let mut cms = std::i64::MAX; diff --git a/src/config.rs b/src/config.rs index 0cd5b64..7c90567 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,14 +4,14 @@ mod types; use crate::{config::toml::Toml, *}; use anyhow::Error; use clap::ArgMatches; -use std::env::var; +use std::{env::var, path::PathBuf}; pub use types::*; pub enum Configs { Log(Conf, ConfLog), Stats(Conf, ConfStats), - Predict(ArgMatches, Conf, ConfPred), + Predict(Conf, ConfPred), Accuracy(Conf, ConfAccuracy), Complete(ArgMatches), } @@ -53,6 +53,7 @@ pub struct ConfPred { pub lim: u16, pub resume: ResumeKind, pub unknown: i64, + pub tmpdirs: Vec, } pub struct ConfStats { pub show: Show, @@ -89,9 +90,7 @@ impl Configs { Ok(match args.subcommand() { Some(("log", sub)) => Self::Log(conf, ConfLog::try_new(sub, &toml)?), Some(("stats", sub)) => Self::Stats(conf, ConfStats::try_new(sub, &toml)?), - Some(("predict", sub)) => { - Self::Predict(sub.clone(), conf, ConfPred::try_new(sub, &toml)?) - }, + Some(("predict", sub)) => Self::Predict(conf, ConfPred::try_new(sub, &toml)?), Some(("accuracy", sub)) => Self::Accuracy(conf, ConfAccuracy::try_new(sub, &toml)?), Some(("complete", sub)) => Self::Complete(sub.clone()), _ => unreachable!("clap should have exited already"), @@ -192,11 +191,19 @@ impl ConfLog { impl ConfPred { fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + let tmpdirs = if let Some(a) = args.get_many::("tmpdir") { + a.cloned().collect() + } else if let Some(a) = toml.predict.as_ref().and_then(|t| t.tmpdir.as_ref()) { + a.to_vec() + } else { + vec![PathBuf::from("/var/tmp")] + }; Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, avg: sel!(args, toml, predict, avg, (), Average::Median)?, lim: sel!(args, toml, predict, limit, 1..65000, 10)? as u16, unknown: sel!(args, toml, predict, unknown, 0..3600, 10)?, resume: *args.get_one("resume").unwrap_or(&ResumeKind::Current), + tmpdirs, first: *args.get_one("first").unwrap_or(&usize::MAX), last: *args.get_one("last").unwrap_or(&usize::MAX) }) } diff --git a/src/config/toml.rs b/src/config/toml.rs index d207b40..4050042 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error}; use serde::Deserialize; -use std::{env::var, fs::File, io::Read}; +use std::{env::var, fs::File, io::Read, path::PathBuf}; #[derive(Deserialize, Debug)] pub struct TomlLog { @@ -13,6 +13,7 @@ pub struct TomlPred { pub avg: Option, pub limit: Option, pub unknown: Option, + pub tmpdir: Option>, } #[derive(Deserialize, Debug)] pub struct TomlStats { diff --git a/src/main.rs b/src/main.rs index 88e4e9e..ea72511 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,7 @@ fn main() { let res = match Configs::load() { Ok(Configs::Log(gc, sc)) => cmd_log(&gc, &sc), Ok(Configs::Stats(gc, sc)) => cmd_stats(&gc, &sc), - Ok(Configs::Predict(args, gc, sc)) => cmd_predict(&args, &gc, &sc), + Ok(Configs::Predict(gc, sc)) => cmd_predict(&gc, &sc), Ok(Configs::Accuracy(gc, sc)) => cmd_accuracy(&gc, &sc), Ok(Configs::Complete(args)) => cmd_complete(&args), Err(e) => Err(e), @@ -50,7 +50,6 @@ pub fn log_err(e: Error) { macro_rules! wtb { ($b:ident, $($arg:expr),+) => {write!($b, $($arg),+).expect("write to buf")} } - #[derive(Clone, Copy, clap::ValueEnum)] pub enum DurationStyle { HMS, From 9a4096492908adb69052e93c62e2aa3f316537d9 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 20:48:07 +0000 Subject: [PATCH 21/28] qa: s/args/cli/ --- src/config.rs | 108 +++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7c90567..ee7307e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -74,8 +74,8 @@ pub struct ConfAccuracy { impl Configs { pub fn load() -> Result { - let args = cli::build_cli().get_matches(); - let level = match args.get_count("verbose") { + let cli = cli::build_cli().get_matches(); + let level = match cli.get_count("verbose") { 0 => LevelFilter::Error, 1 => LevelFilter::Warn, 2 => LevelFilter::Info, @@ -83,11 +83,11 @@ impl Configs { _ => LevelFilter::Trace, }; env_logger::Builder::new().filter_level(level).format_timestamp(None).init(); - trace!("{:?}", args); - let toml = Toml::load(args.get_one::("config"), var("EMLOP_CONFIG").ok())?; + trace!("{:?}", cli); + let toml = Toml::load(cli.get_one::("config"), var("EMLOP_CONFIG").ok())?; log::trace!("{:?}", toml); - let conf = Conf::try_new(&args, &toml)?; - Ok(match args.subcommand() { + let conf = Conf::try_new(&cli, &toml)?; + Ok(match cli.subcommand() { Some(("log", sub)) => Self::Log(conf, ConfLog::try_new(sub, &toml)?), Some(("stats", sub)) => Self::Stats(conf, ConfStats::try_new(sub, &toml)?), Some(("predict", sub)) => Self::Predict(conf, ConfPred::try_new(sub, &toml)?), @@ -101,17 +101,17 @@ impl Configs { // TODO nicer way to specify src fn sel(cli: Option<&String>, toml: Option<&T>, - argsrc: &'static str, + clisrc: &'static str, tomlsrc: &'static str, - parg: A, + arg: A, def: R) -> Result where R: ArgParse + ArgParse { if let Some(a) = cli { - R::parse(a, parg, argsrc) + R::parse(a, arg, clisrc) } else if let Some(a) = toml { - R::parse(a, parg, tomlsrc) + R::parse(a, arg, tomlsrc) } else { Ok(def) } @@ -138,26 +138,26 @@ macro_rules! sel { impl Conf { - pub fn try_new(args: &ArgMatches, toml: &Toml) -> Result { + pub fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { let isterm = std::io::stdout().is_terminal(); - let color = match args.get_one("color") { + let color = match cli.get_one("color") { Some(ColorStyle::Always) => true, Some(ColorStyle::Never) => false, None => isterm, }; - let out = match args.get_one("output") { + let out = match cli.get_one("output") { Some(o) => *o, None if isterm => OutStyle::Columns, None => OutStyle::Tab, }; - let header = args.get_flag("header"); - let dur_t = *args.get_one("duration").unwrap(); - let offset = get_offset(args.get_flag("utc")); - Ok(Self { logfile: sel!(args, toml, logfile, (), String::from("/var/log/emerge.log"))?, - from: args.get_one("from") - .map(|d| i64::parse(d, offset, "--from")) - .transpose()?, - to: args.get_one("to").map(|d| i64::parse(d, offset, "--to")).transpose()?, + let header = cli.get_flag("header"); + let dur_t = *cli.get_one("duration").unwrap(); + let offset = get_offset(cli.get_flag("utc")); + Ok(Self { logfile: sel!(cli, toml, logfile, (), String::from("/var/log/emerge.log"))?, + from: cli.get_one("from") + .map(|d| i64::parse(d, offset, "--from")) + .transpose()?, + to: cli.get_one("to").map(|d| i64::parse(d, offset, "--to")).transpose()?, pkg: AnsiStr::from(if color { "\x1B[1;32m" } else { "" }), merge: AnsiStr::from(if color { "\x1B[1;32m" } else { ">>> " }), unmerge: AnsiStr::from(if color { "\x1B[1;31m" } else { "<<< " }), @@ -168,65 +168,65 @@ impl Conf { header, dur_t, date_offset: offset, - date_fmt: sel!(args, toml, date, (), DateStyle::default())?, + date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, out }) } #[cfg(test)] pub fn from_str(s: impl AsRef) -> Self { - let args = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); - Self::try_new(&args, &Toml::default()).unwrap() + let cli = cli::build_cli().get_matches_from(s.as_ref().split_whitespace()); + Self::try_new(&cli, &Toml::default()).unwrap() } } impl ConfLog { - fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel!(args, toml, log, show, "musa", Show::m())?, - search: args.get_many("search").unwrap_or_default().cloned().collect(), - exact: args.get_flag("exact"), - starttime: sel!(args, toml, log, starttime, (), false)?, - first: *args.get_one("first").unwrap_or(&usize::MAX), - last: *args.get_one("last").unwrap_or(&usize::MAX) }) + fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { show: sel!(cli, toml, log, show, "musa", Show::m())?, + search: cli.get_many("search").unwrap_or_default().cloned().collect(), + exact: cli.get_flag("exact"), + starttime: sel!(cli, toml, log, starttime, (), false)?, + first: *cli.get_one("first").unwrap_or(&usize::MAX), + last: *cli.get_one("last").unwrap_or(&usize::MAX) }) } } impl ConfPred { - fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - let tmpdirs = if let Some(a) = args.get_many::("tmpdir") { + fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { + let tmpdirs = if let Some(a) = cli.get_many::("tmpdir") { a.cloned().collect() } else if let Some(a) = toml.predict.as_ref().and_then(|t| t.tmpdir.as_ref()) { a.to_vec() } else { vec![PathBuf::from("/var/tmp")] }; - Ok(Self { show: sel!(args, toml, predict, show, "emta", Show::emt())?, - avg: sel!(args, toml, predict, avg, (), Average::Median)?, - lim: sel!(args, toml, predict, limit, 1..65000, 10)? as u16, - unknown: sel!(args, toml, predict, unknown, 0..3600, 10)?, - resume: *args.get_one("resume").unwrap_or(&ResumeKind::Current), + Ok(Self { show: sel!(cli, toml, predict, show, "emta", Show::emt())?, + avg: sel!(cli, toml, predict, avg, (), Average::Median)?, + lim: sel!(cli, toml, predict, limit, 1..65000, 10)? as u16, + unknown: sel!(cli, toml, predict, unknown, 0..3600, 10)?, + resume: *cli.get_one("resume").unwrap_or(&ResumeKind::Current), tmpdirs, - first: *args.get_one("first").unwrap_or(&usize::MAX), - last: *args.get_one("last").unwrap_or(&usize::MAX) }) + first: *cli.get_one("first").unwrap_or(&usize::MAX), + last: *cli.get_one("last").unwrap_or(&usize::MAX) }) } } impl ConfStats { - fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel!(args, toml, stats, show, "ptsa", Show::p())?, - search: args.get_many("search").unwrap_or_default().cloned().collect(), - exact: args.get_flag("exact"), - lim: sel!(args, toml, stats, limit, 1..65000, 10)? as u16, - avg: sel!(args, toml, stats, avg, (), Average::Median)?, - group: sel!(args, toml, stats, group, (), Timespan::None)? }) + fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { show: sel!(cli, toml, stats, show, "ptsa", Show::p())?, + search: cli.get_many("search").unwrap_or_default().cloned().collect(), + exact: cli.get_flag("exact"), + lim: sel!(cli, toml, stats, limit, 1..65000, 10)? as u16, + avg: sel!(cli, toml, stats, avg, (), Average::Median)?, + group: sel!(cli, toml, stats, group, (), Timespan::None)? }) } } impl ConfAccuracy { - fn try_new(args: &ArgMatches, toml: &Toml) -> Result { - Ok(Self { show: sel!(args, toml, accuracy, show, "mta", Show::mt())?, - search: args.get_many("search").unwrap_or_default().cloned().collect(), - exact: args.get_flag("exact"), - avg: sel!(args, toml, accuracy, avg, (), Average::Median)?, - lim: sel!(args, toml, accuracy, limit, 1..65000, 10)? as u16, - last: *args.get_one("last").unwrap_or(&usize::MAX) }) + fn try_new(cli: &ArgMatches, toml: &Toml) -> Result { + Ok(Self { show: sel!(cli, toml, accuracy, show, "mta", Show::mt())?, + search: cli.get_many("search").unwrap_or_default().cloned().collect(), + exact: cli.get_flag("exact"), + avg: sel!(cli, toml, accuracy, avg, (), Average::Median)?, + lim: sel!(cli, toml, accuracy, limit, 1..65000, 10)? as u16, + last: *cli.get_one("last").unwrap_or(&usize::MAX) }) } } From 1795bad1a59636b3aee2ca0a5fbd6f5aa679aa56 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 13:04:38 +0000 Subject: [PATCH 22/28] conf: move `--shell` to `Conf` That was the last use of `ArgMatches` outside of the config module :) --- src/commands.rs | 6 ++---- src/config.rs | 13 +++++++++++-- src/main.rs | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index f301ac0..733eb43 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,4 @@ use crate::{datetime::*, parse::*, proces::*, table::*, *}; -use clap::ArgMatches; use std::{collections::{BTreeMap, HashMap}, io::stdin}; @@ -471,10 +470,9 @@ pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { Ok(found) } -pub fn cmd_complete(args: &ArgMatches) -> Result { - let shell: clap_complete::Shell = *args.get_one("shell").unwrap(); +pub fn cmd_complete(sc: &ConfComplete) -> Result { let mut cli = cli::build_cli_nocomplete(); - clap_complete::generate(shell, &mut cli, "emlop", &mut std::io::stdout()); + clap_complete::generate(sc.shell, &mut cli, "emlop", &mut std::io::stdout()); Ok(true) } diff --git a/src/config.rs b/src/config.rs index ee7307e..7bd9082 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ pub enum Configs { Stats(Conf, ConfStats), Predict(Conf, ConfPred), Accuracy(Conf, ConfAccuracy), - Complete(ArgMatches), + Complete(ConfComplete), } /// Global config @@ -71,6 +71,9 @@ pub struct ConfAccuracy { pub last: usize, pub lim: u16, } +pub struct ConfComplete { + pub shell: clap_complete::Shell, +} impl Configs { pub fn load() -> Result { @@ -92,7 +95,7 @@ impl Configs { Some(("stats", sub)) => Self::Stats(conf, ConfStats::try_new(sub, &toml)?), Some(("predict", sub)) => Self::Predict(conf, ConfPred::try_new(sub, &toml)?), Some(("accuracy", sub)) => Self::Accuracy(conf, ConfAccuracy::try_new(sub, &toml)?), - Some(("complete", sub)) => Self::Complete(sub.clone()), + Some(("complete", sub)) => Self::Complete(ConfComplete::try_new(sub)?), _ => unreachable!("clap should have exited already"), }) } @@ -230,3 +233,9 @@ impl ConfAccuracy { last: *cli.get_one("last").unwrap_or(&usize::MAX) }) } } + +impl ConfComplete { + fn try_new(cli: &ArgMatches) -> Result { + Ok(Self { shell: *cli.get_one("shell").unwrap() }) + } +} diff --git a/src/main.rs b/src/main.rs index ea72511..8bcbe41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ fn main() { Ok(Configs::Stats(gc, sc)) => cmd_stats(&gc, &sc), Ok(Configs::Predict(gc, sc)) => cmd_predict(&gc, &sc), Ok(Configs::Accuracy(gc, sc)) => cmd_accuracy(&gc, &sc), - Ok(Configs::Complete(args)) => cmd_complete(&args), + Ok(Configs::Complete(sc)) => cmd_complete(&sc), Err(e) => Err(e), }; match res { From 35cad99dfbf179b7bb765108585ba4d301d088fc Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 21:14:27 +0000 Subject: [PATCH 23/28] qa: Move cli.rs to config/ --- src/commands.rs | 2 +- src/config.rs | 3 ++- src/{ => config}/cli.rs | 0 src/main.rs | 3 +-- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/{ => config}/cli.rs (100%) diff --git a/src/commands.rs b/src/commands.rs index 733eb43..c45721f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -471,7 +471,7 @@ pub fn cmd_accuracy(gc: &Conf, sc: &ConfAccuracy) -> Result { } pub fn cmd_complete(sc: &ConfComplete) -> Result { - let mut cli = cli::build_cli_nocomplete(); + let mut cli = build_cli_nocomplete(); clap_complete::generate(sc.shell, &mut cli, "emlop", &mut std::io::stdout()); Ok(true) } diff --git a/src/config.rs b/src/config.rs index 7bd9082..ff772aa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,12 @@ +mod cli; mod toml; mod types; +pub use crate::config::{cli::*, types::*}; use crate::{config::toml::Toml, *}; use anyhow::Error; use clap::ArgMatches; use std::{env::var, path::PathBuf}; -pub use types::*; pub enum Configs { diff --git a/src/cli.rs b/src/config/cli.rs similarity index 100% rename from src/cli.rs rename to src/config/cli.rs diff --git a/src/main.rs b/src/main.rs index 8bcbe41..f909ae5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ #![cfg_attr(feature = "unstable", feature(test))] -mod cli; mod commands; mod config; mod datetime; @@ -27,7 +26,7 @@ fn main() { Ok(false) => std::process::exit(1), Err(e) => { match e.downcast::() { - Ok(ce) => ce.format(&mut cli::build_cli()).print().unwrap_or(()), + Ok(ce) => ce.format(&mut build_cli()).print().unwrap_or(()), Err(e) => match e.downcast::() { Ok(ae) => eprintln!("{ae}"), Err(e) => log_err(e), From e701e9c30dd6f4629c8a55775992555cef24184a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 21:18:59 +0000 Subject: [PATCH 24/28] conf: Make `header`,`utc` and `duration` configurable --- emlop.toml | 3 +++ src/config.rs | 8 +++----- src/config/cli.rs | 18 ++++++++++-------- src/config/toml.rs | 3 +++ src/config/types.rs | 36 ++++++++++++++++++++++++++++++++++++ src/main.rs | 29 ----------------------------- 6 files changed, 55 insertions(+), 42 deletions(-) diff --git a/emlop.toml b/emlop.toml index 90024cd..cbcfdce 100644 --- a/emlop.toml +++ b/emlop.toml @@ -12,6 +12,9 @@ # logfile = "/var/log/emerge.log" # date = "rfc2822" +# duration = "human" +# utc = true +# header = true [log] #show = "mus" #starttime = true diff --git a/src/config.rs b/src/config.rs index ff772aa..9599e02 100644 --- a/src/config.rs +++ b/src/config.rs @@ -154,9 +154,7 @@ impl Conf { None if isterm => OutStyle::Columns, None => OutStyle::Tab, }; - let header = cli.get_flag("header"); - let dur_t = *cli.get_one("duration").unwrap(); - let offset = get_offset(cli.get_flag("utc")); + let offset = get_offset(sel!(cli, toml, utc, (), false)?); Ok(Self { logfile: sel!(cli, toml, logfile, (), String::from("/var/log/emerge.log"))?, from: cli.get_one("from") .map(|d| i64::parse(d, offset, "--from")) @@ -169,8 +167,8 @@ impl Conf { cnt: AnsiStr::from(if color { "\x1B[2;33m" } else { "" }), clr: AnsiStr::from(if color { "\x1B[0m" } else { "" }), lineend: if color { b"\x1B[0m\n" } else { b"\n" }, - header, - dur_t, + header: sel!(cli, toml, header, (), false)?, + dur_t: sel!(cli, toml, duration, (), DurationStyle::HMS)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, out }) diff --git a/src/config/cli.rs b/src/config/cli.rs index 0a51640..5f76093 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -169,9 +169,11 @@ pub fn build_cli_nocomplete() -> Command { //////////////////////////////////////////////////////////// let header = Arg::new("header").short('H') .long("header") - .action(SetTrue) .global(true) .display_order(1) + .num_args(..=1) + .default_missing_value("y") + .value_name("bool") .help_heading("Format") .help("Show table header"); let date = @@ -194,21 +196,21 @@ pub fn build_cli_nocomplete() -> Command { .long("duration") .display_order(3) .global(true) - .value_parser(value_parser!(crate::DurationStyle)) .hide_possible_values(true) - .default_value("hms") .display_order(51) .help_heading("Format") .help("Output durations in different formats") .long_help("Output durations in different formats\n \ - hms: 10:30\n \ - hmsfixed: 0:10:30\n \ - secs|s: 630\n \ - human|h: 10 minutes, 30 seconds"); + hms|(default): 10:30\n \ + hmsfixed: 0:10:30\n \ + secs|s: 630\n \ + human|h: 10 minutes, 30 seconds"); let utc = Arg::new("utc").long("utc") .global(true) - .action(SetTrue) .display_order(4) + .num_args(..=1) + .default_missing_value("y") + .value_name("bool") .help_heading("Format") .help("Parse/display dates in UTC instead of local time"); let starttime = Arg::new("starttime").long("starttime") diff --git a/src/config/toml.rs b/src/config/toml.rs index 4050042..d782775 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -32,6 +32,9 @@ pub struct TomlAccuracy { pub struct Toml { pub logfile: Option, pub date: Option, + pub duration: Option, + pub header: Option, + pub utc: Option, pub log: Option, pub predict: Option, pub stats: Option, diff --git a/src/config/types.rs b/src/config/types.rs index 8e178f6..8070974 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -133,6 +133,42 @@ pub enum ResumeKind { No, } +#[derive(Clone, Copy)] +pub enum DurationStyle { + HMS, + HMSFixed, + Secs, + Human, +} +impl ArgParse for DurationStyle { + fn parse(v: &String, _: (), s: &'static str) -> Result { + match v.as_str() { + "hms" => Ok(Self::HMS), + "hmsfixed" => Ok(Self::HMSFixed), + "s" | "secs" => Ok(Self::Secs), + "h" | "human" => Ok(Self::Human), + _ => Err(ArgError::new(v, s).pos("hms hmsfixed (s)ecs (h)uman")), + } + } +} + +#[cfg_attr(test, derive(PartialEq, Eq, Debug))] +#[derive(Clone, Copy, clap::ValueEnum)] +pub enum ColorStyle { + #[clap(alias("y"))] + Always, + #[clap(alias("n"))] + Never, +} + +#[derive(Clone, Copy, clap::ValueEnum, PartialEq, Eq)] +pub enum OutStyle { + #[clap(alias("c"))] + Columns, + #[clap(alias("t"))] + Tab, +} + #[derive(Clone, Copy)] pub struct Show { pub pkg: bool, diff --git a/src/main.rs b/src/main.rs index f909ae5..44f9c00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,32 +47,3 @@ pub fn log_err(e: Error) { /// Alias to `write!(...).expect("write to buf")` just to save on typing/indent. #[macro_export] macro_rules! wtb { ($b:ident, $($arg:expr),+) => {write!($b, $($arg),+).expect("write to buf")} } - - -#[derive(Clone, Copy, clap::ValueEnum)] -pub enum DurationStyle { - HMS, - #[clap(id("hmsfixed"))] - HMSFixed, - #[clap(alias("s"))] - Secs, - #[clap(alias("h"))] - Human, -} - -#[cfg_attr(test, derive(PartialEq, Eq, Debug))] -#[derive(Clone, Copy, clap::ValueEnum)] -pub enum ColorStyle { - #[clap(alias("y"))] - Always, - #[clap(alias("n"))] - Never, -} - -#[derive(Clone, Copy, clap::ValueEnum, PartialEq, Eq)] -pub enum OutStyle { - #[clap(alias("c"))] - Columns, - #[clap(alias("t"))] - Tab, -} From a1594ab3646e99c8f46f1370a492be187cf7d91e Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 18:48:17 +0000 Subject: [PATCH 25/28] conf: Remove the `--config` cli flag The only way to change the config location now is using the env var. Changing the location should be a very rare need, so let's simplify things. --- src/config.rs | 6 +++--- src/config/cli.rs | 20 +++++--------------- src/config/toml.rs | 6 +++--- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index 9599e02..c8631e5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ pub use crate::config::{cli::*, types::*}; use crate::{config::toml::Toml, *}; use anyhow::Error; use clap::ArgMatches; -use std::{env::var, path::PathBuf}; +use std::path::PathBuf; pub enum Configs { @@ -88,8 +88,8 @@ impl Configs { }; env_logger::Builder::new().filter_level(level).format_timestamp(None).init(); trace!("{:?}", cli); - let toml = Toml::load(cli.get_one::("config"), var("EMLOP_CONFIG").ok())?; - log::trace!("{:?}", toml); + let toml = Toml::load()?; + trace!("{:?}", toml); let conf = Conf::try_new(&cli, &toml)?; Ok(match cli.subcommand() { Some(("log", sub)) => Self::Log(conf, ConfLog::try_new(sub, &toml)?), diff --git a/src/config/cli.rs b/src/config/cli.rs index 5f76093..de05ef6 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -295,17 +295,6 @@ pub fn build_cli_nocomplete() -> Command { -v: show warnings\n \ -vv: show info\n \ -vvv: show debug"); - let h = "Location of emlop config file\n\ - Default is $HOME/.config/emlop.toml (or $EMLOP_CONFIG if set)\n\ - Set to an empty string to disable\n\ - Config in in TOML format, see example file in /usr/share/doc/emlop-x.y.z/"; - let config = Arg::new("config").value_name("file") - .long("config") - .global(true) - .num_args(1) - .display_order(5) - .help(h.split_once('\n').unwrap().0) - .long_help(h); //////////////////////////////////////////////////////////// // Subcommands @@ -364,9 +353,11 @@ pub fn build_cli_nocomplete() -> Command { let about = "A fast, accurate, ergonomic EMerge LOg Parser\n\ https://github.com/vincentdephily/emlop"; let after_help = - "Subcommands and long args can be abbreviated (eg `emlop l -ss --head -f1w`)\n\ - Subcommands have their own -h / --help\n\ - Exit code is 0 if sucessful, 1 if search found nothing, 2 in case of other errors"; + concat!("Commands and long args can be abbreviated (eg `emlop l -ss --head -f1w`)\n\ + Commands have their own -h / --help\n\ + Exit code is 0 if sucessful, 1 if search found nothing, 2 in case of other errors\n\ + Config can be set in $HOME/.config/emlop.toml, see example in /usr/share/doc/emlop-", + crate_version!()); let styles = styling::Styles::styled().header(styling::AnsiColor::Blue.on_default() | styling::Effects::BOLD) @@ -392,7 +383,6 @@ pub fn build_cli_nocomplete() -> Command { .arg(color) .arg(output) .arg(logfile) - .arg(config) .arg(verbose) .subcommand(cmd_log) .subcommand(cmd_pred) diff --git a/src/config/toml.rs b/src/config/toml.rs index d782775..90a9b85 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -41,8 +41,8 @@ pub struct Toml { pub accuracy: Option, } impl Toml { - pub fn load(arg: Option<&String>, env: Option) -> Result { - match arg.or(env.as_ref()) { + pub fn load() -> Result { + match var("EMLOP_CONFIG").ok() { Some(s) if s.is_empty() => Ok(Self::default()), Some(s) => Self::doload(s.as_str()), _ => Self::doload(&format!("{}/.config/emlop.toml", @@ -50,7 +50,7 @@ impl Toml { } } fn doload(name: &str) -> Result { - log::trace!("Loading config {name:?}"); + log::debug!("Loading config {name:?}"); let mut f = File::open(name).with_context(|| format!("Cannot open {name:?}"))?; let mut buf = String::new(); // TODO Streaming read From a5b1d3a732a5ca8ee5d7013429cbf4d723f6110a Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Tue, 6 Feb 2024 21:24:10 +0000 Subject: [PATCH 26/28] doc: Update changelog, example config, inline help Rewordings and cleanups. --- CHANGELOG.md | 5 ++ emlop.toml | 43 +++++++-------- src/config/cli.rs | 135 +++++++++++++++++++++++----------------------- 3 files changed, 90 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8614bf..f9a0e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ * Support searching by multiple terms - eg `emlop s -e gcc clang llvm rust` +* Support configuration file + - Located by default in `~/.config/emlop.toml`, overridable with `$EMLOP_CONFIG` env var + - Example config added to repo, should be installed alongside emlop docs + - Config options, when available, always correspond to a cli arg + - Many flags/args now take an optional `no` value, so the cli can override the conf * Improve predict: - Autodetect `tmpdir` using currently running emerge processes - Support multiple `--tmpdir` arguments diff --git a/emlop.toml b/emlop.toml index cbcfdce..93a7167 100644 --- a/emlop.toml +++ b/emlop.toml @@ -1,14 +1,9 @@ -######################################## -# This is an example emlop config file. +# This is an example `emlop` config file. # -# To use it, copy to $HOME/.config/emlop.toml (or to whatever you set $EMLOP_CONFIG to) and -# uncomment the desired lines. -# -# All config items have a corresponding command-line arg, see `emlop --help` for -# detailed format and behavior. Not all command-line args have a config item, this file lists all -# the supported items. -######################################## - +# It is loaded from `$HOME/.config/emlop.toml` by default. +# Use `$EMLOP_CONFIG` to set a different location, empty string to disable config loading. +# Entries have the same name and format as command-line args, see `emlop --help`. +# Some args are only avaible via the command line. # logfile = "/var/log/emerge.log" # date = "rfc2822" @@ -16,20 +11,20 @@ # utc = true # header = true [log] -#show = "mus" -#starttime = true +# show = "mus" +# starttime = true [predict] -#show = "emt" -#avg = "arith" -#limit = 20 -#unknown = 300 -#tmpdir = ["/foo", "/bar"] +# show = "emt" +# avg = "arith" +# limit = 20 +# unknown = 300 +# tmpdir = ["/foo", "/bar"] [stats] -#show = "pts" -#avg = "arith" -#limit = 20 -#group = "y" +# show = "pts" +# avg = "arith" +# limit = 20 +# group = "y" [accuracy] -#show = "mt" -#avg = "arith" -#limit = 20 +# show = "mt" +# avg = "arith" +# limit = 20 diff --git a/src/config/cli.rs b/src/config/cli.rs index de05ef6..1766ba9 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -74,38 +74,39 @@ pub fn build_cli_nocomplete() -> Command { m: Package merges\n \ t: Totals\n \ a: All of the above"); - - let from = Arg::new("from").value_name("date") - .short('f') + let h = "Only parse log entries after \n \ + 2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \ + 123456789: Absolute unix timestamp\n \ + 1 year, 2 months|10d: Relative date"; + let from = Arg::new("from").short('f') .long("from") - .display_order(4) + .value_name("date") .global(true) .num_args(1) + .display_order(4) .help_heading("Filter") - .help("Only parse log entries after ") - .long_help("Only parse log entries after \n \ - 2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \ - 123456789: Absolute unix timestamp\n \ - 1 year, 2 months|10d: Relative date"); - let to = Arg::new("to").value_name("date") - .short('t') + .help(h.split_once('\n').unwrap().0) + .long_help(h); + let h = "Only parse log entries before \n \ + 2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \ + 123456789: Absolute unix timestamp\n \ + 1 year, 2 months|10d: Relative date"; + let to = Arg::new("to").short('t') .long("to") - .display_order(5) + .value_name("date") .global(true) .num_args(1) + .display_order(5) .help_heading("Filter") - .help("Only parse log entries before ") - .long_help("Only parse log entries before \n \ - 2018-03-04|2018-03-04 12:34:56|2018-03-04T12:34: Absolute ISO date\n \ - 123456789: Absolute unix timestamp\n \ - 1 year, 2 months|10d: Relative date"); + .help(h.split_once('\n').unwrap().0) + .long_help(h); let first = Arg::new("first").short('N') .long("first") .value_name("num") - .display_order(6) .num_args(..=1) .default_missing_value("1") .value_parser(value_parser!(usize)) + .display_order(6) .help_heading("Filter") .help("Show only the first entries") .long_help("Show only the first entries\n \ @@ -114,24 +115,41 @@ pub fn build_cli_nocomplete() -> Command { let last = Arg::new("last").short('n') .long("last") .value_name("num") - .display_order(7) .num_args(..=1) .default_missing_value("1") .value_parser(value_parser!(usize)) + .display_order(7) .help_heading("Filter") .help("Show only the last entries") .long_help("Show only the last entries\n \ (empty)|1: last entry\n \ 5: last 5 entries\n"); + let h = "Use main, backup, any, or no portage resume list\n\ + This is ignored if STDIN is a piped `emerge -p` output\n \ + (default): Use main resume list, if currently emerging\n \ + any|a|(empty): Use main or backup resume list\n \ + main|m: Use main resume list\n \ + backup|b: Use backup resume list\n \ + no|n: Never use resume list"; + let resume = Arg::new("resume").long("resume") + .value_name("source") + .value_parser(value_parser!(crate::config::ResumeKind)) + .hide_possible_values(true) + .num_args(..=1) + .default_missing_value("any") + .display_order(8) + .help_heading("Filter") + .help(h.split_once('\n').unwrap().0) + .long_help(h); //////////////////////////////////////////////////////////// // Stats arguments //////////////////////////////////////////////////////////// let group = Arg::new("group").short('g') .long("groupby") - .display_order(1) .value_name("y,m,w,d,n") .hide_possible_values(true) + .display_order(10) .help_heading("Stats") .help("Group by (y)ear, (m)onth, (w)eek, (d)ay, (n)one") .long_help("Group by (y)ear, (m)onth, (w)eek, (d)ay, or (n)one\n\ @@ -139,16 +157,16 @@ pub fn build_cli_nocomplete() -> Command { Weeks start on monday and are formated as \ 'year-weeknumber'."); let limit = Arg::new("limit").long("limit") - .display_order(2) - .num_args(1) .value_name("num") + .num_args(1) + .display_order(11) .help_heading("Stats") .help("Use the last merge times to predict durations"); let avg = Arg::new("avg").long("avg") .value_name("fn") - .display_order(3) .hide_possible_values(true) + .display_order(12) .help_heading("Stats") .help("Select function used to predict durations") .long_help("Select function used to predict durations\n \ @@ -156,11 +174,10 @@ pub fn build_cli_nocomplete() -> Command { (defaut)|median|m: middle value, mitigates outliers\n \ weighted-arith|wa: 'sum/count' with more weight for recent values\n \ weighted-median|wm: \"middle\" value shifted toward recent values"); - let unknown = Arg::new("unknown").long("unknown") - .display_order(4) .num_args(1) .value_name("secs") + .display_order(13) .help_heading("Stats") .help("Assume unkown packages take seconds to merge"); @@ -169,19 +186,18 @@ pub fn build_cli_nocomplete() -> Command { //////////////////////////////////////////////////////////// let header = Arg::new("header").short('H') .long("header") + .value_name("bool") .global(true) - .display_order(1) .num_args(..=1) .default_missing_value("y") - .value_name("bool") + .display_order(20) .help_heading("Format") .help("Show table header"); let date = - Arg::new("date").value_name("format") - .long("date") - .display_order(2) + Arg::new("date").long("date") + .value_name("format") .global(true) - .display_order(52) + .display_order(21) .help_heading("Format") .help("Output dates in different formats") .long_help("Output dates in different formats\n \ @@ -192,12 +208,11 @@ pub fn build_cli_nocomplete() -> Command { rfc2822|2822: Mon, 31 Jan 2022 08:59:46 +00:00\n \ compact: 20220131085946\n \ unix: 1643619586"); - let duration = Arg::new("duration").value_name("format") - .long("duration") - .display_order(3) + let duration = Arg::new("duration").long("duration") + .value_name("format") .global(true) .hide_possible_values(true) - .display_order(51) + .display_order(22) .help_heading("Format") .help("Output durations in different formats") .long_help("Output durations in different formats\n \ @@ -206,43 +221,41 @@ pub fn build_cli_nocomplete() -> Command { secs|s: 630\n \ human|h: 10 minutes, 30 seconds"); let utc = Arg::new("utc").long("utc") + .value_name("bool") .global(true) - .display_order(4) .num_args(..=1) .default_missing_value("y") - .value_name("bool") + .display_order(23) .help_heading("Format") .help("Parse/display dates in UTC instead of local time"); let starttime = Arg::new("starttime").long("starttime") + .value_name("bool") .num_args(..=1) .default_missing_value("y") - .value_name("bool") - .display_order(5) + .display_order(24) .help_heading("Format") .help("Display start time instead of end time"); let color = Arg::new("color").long("color") - .alias("colour") - .display_order(6) + .value_name("when") .global(true) .value_parser(value_parser!(crate::ColorStyle)) .hide_possible_values(true) .num_args(..=1) .default_missing_value("y") - .value_name("when") - .display_order(55) + .display_order(25) .help_heading("Format") .help("Enable color (always/never/y/n)") .long_help("Enable color (always/never/y/n)\n \ (default): colored if on tty\n \ (empty)|always|y: colored\n \ never|n: not colored"); - let output = Arg::new("output").long("output") - .short('o') + let output = Arg::new("output").short('o') + .long("output") .value_name("format") .global(true) .value_parser(value_parser!(crate::OutStyle)) .hide_possible_values(true) - .display_order(7) + .display_order(26) .help_heading("Format") .help("Ouput format (columns/c/tab/t)") .long_help("Ouput format (columns/c/tab/t)\n \ @@ -253,43 +266,27 @@ pub fn build_cli_nocomplete() -> Command { //////////////////////////////////////////////////////////// // Misc arguments //////////////////////////////////////////////////////////// - let logfile = Arg::new("logfile").value_name("file") + let logfile = Arg::new("logfile").short('F') .long("logfile") - .short('F') + .value_name("file") .global(true) .num_args(1) - .display_order(1) + .display_order(30) .help("Location of emerge log file"); - let tmpdir = Arg::new("tmpdir").value_name("dir") - .long("tmpdir") + let tmpdir = Arg::new("tmpdir").long("tmpdir") + .value_name("dir") .num_args(1) .action(Append) .value_parser(value_parser!(PathBuf)) - .display_order(2) + .display_order(31) .help("Location of portage tmpdir") .long_help("Location of portage tmpdir\n\ Multiple folders can be provided\n\ Emlop also looks for tmpdir using current emerge processes"); - let h = "Use main, backup, any, or no portage resume list\n\ - This is ignored if STDIN is a piped `emerge -p` output\n \ - (default): Use main resume list, if currently emerging\n \ - any|a|(empty): Use main or backup resume list\n \ - main|m: Use main resume list\n \ - backup|b: Use backup resume list\n \ - no|n: Never use resume list"; - let resume = Arg::new("resume").long("resume") - .value_name("source") - .value_parser(value_parser!(crate::config::ResumeKind)) - .hide_possible_values(true) - .num_args(..=1) - .default_missing_value("any") - .display_order(3) - .help(h.split_once('\n').unwrap().0) - .long_help(h); let verbose = Arg::new("verbose").short('v') .global(true) .action(Count) - .display_order(4) + .display_order(33) .help("Increase verbosity (can be given multiple times)") .long_help("Increase verbosity (defaults to errors only)\n \ -v: show warnings\n \ From 6a553871813505c082ffb1e02b4a5a5e28075dbd Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 7 Feb 2024 18:14:47 +0000 Subject: [PATCH 27/28] qa: Clippy fix I don't agree with clippy here but also don't really care. --- src/config.rs | 2 +- src/config/types.rs | 8 ++++---- src/datetime.rs | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index c8631e5..d5690ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -168,7 +168,7 @@ impl Conf { clr: AnsiStr::from(if color { "\x1B[0m" } else { "" }), lineend: if color { b"\x1B[0m\n" } else { b"\n" }, header: sel!(cli, toml, header, (), false)?, - dur_t: sel!(cli, toml, duration, (), DurationStyle::HMS)?, + dur_t: sel!(cli, toml, duration, (), DurationStyle::Hms)?, date_offset: offset, date_fmt: sel!(cli, toml, date, (), DateStyle::default())?, out }) diff --git a/src/config/types.rs b/src/config/types.rs index 8070974..f40a67d 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -135,16 +135,16 @@ pub enum ResumeKind { #[derive(Clone, Copy)] pub enum DurationStyle { - HMS, - HMSFixed, + Hms, + HmsFixed, Secs, Human, } impl ArgParse for DurationStyle { fn parse(v: &String, _: (), s: &'static str) -> Result { match v.as_str() { - "hms" => Ok(Self::HMS), - "hmsfixed" => Ok(Self::HMSFixed), + "hms" => Ok(Self::Hms), + "hmsfixed" => Ok(Self::HmsFixed), "s" | "secs" => Ok(Self::Secs), "h" | "human" => Ok(Self::Human), _ => Err(ArgError::new(v, s).pos("hms hmsfixed (s)ecs (h)uman")), diff --git a/src/datetime.rs b/src/datetime.rs index 121f2d6..9433ff3 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -269,12 +269,12 @@ impl crate::table::Disp for FmtDur { let start = buf.len(); match conf.dur_t { _ if sec < 0 => wtb!(buf, "{dur}?"), - HMS if sec >= 3600 => { + Hms if sec >= 3600 => { wtb!(buf, "{dur}{}:{:02}:{:02}", sec / 3600, sec % 3600 / 60, sec % 60) }, - HMS if sec >= 60 => wtb!(buf, "{dur}{}:{:02}", sec % 3600 / 60, sec % 60), - HMS | Secs => wtb!(buf, "{dur}{sec}"), - HMSFixed => wtb!(buf, "{dur}{}:{:02}:{:02}", sec / 3600, sec % 3600 / 60, sec % 60), + Hms if sec >= 60 => wtb!(buf, "{dur}{}:{:02}", sec % 3600 / 60, sec % 60), + Hms | Secs => wtb!(buf, "{dur}{sec}"), + HmsFixed => wtb!(buf, "{dur}{}:{:02}:{:02}", sec / 3600, sec % 3600 / 60, sec % 60), Human if sec == 0 => wtb!(buf, "{dur}0 second"), Human => { let a = [(sec / 86400, "day"), From 48801423cb696e1e881ceebc37bf0e183e820a72 Mon Sep 17 00:00:00 2001 From: Vincent de Phily Date: Wed, 7 Feb 2024 19:22:38 +0000 Subject: [PATCH 28/28] conf: Only print a warning for unreadable config file Failure to parse is still a hard error, so far. --- src/config/toml.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/config/toml.rs b/src/config/toml.rs index 90a9b85..80805b2 100644 --- a/src/config/toml.rs +++ b/src/config/toml.rs @@ -51,10 +51,16 @@ impl Toml { } fn doload(name: &str) -> Result { log::debug!("Loading config {name:?}"); - let mut f = File::open(name).with_context(|| format!("Cannot open {name:?}"))?; - let mut buf = String::new(); - // TODO Streaming read - f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; - toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}")) + match File::open(name) { + Err(e) => { + log::warn!("Cannot open {name:?}: {e}"); + Ok(Self::default()) + }, + Ok(mut f) => { + let mut buf = String::new(); + f.read_to_string(&mut buf).with_context(|| format!("Cannot read {name:?}"))?; + toml::from_str(&buf).with_context(|| format!("Cannot parse {name:?}")) + }, + } } }