use chrono::{Local, TimeZone}; use clap::ArgMatches; use config_file::FromConfigFile; use regex::Regex; use serde::Deserialize; use std::io::IsTerminal; use std::path::Path; use std::path::PathBuf; use crate::dir_walker::Operater; use crate::display::get_number_format; pub static DAY_SECONDS: i64 = 24 * 60 * 60; #[derive(Deserialize, Default)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] pub struct Config { pub display_full_paths: Option, pub display_apparent_size: Option, pub reverse: Option, pub no_colors: Option, pub force_colors: Option, pub no_bars: Option, pub skip_total: Option, pub screen_reader: Option, pub ignore_hidden: Option, pub output_format: Option, pub min_size: Option, pub only_dir: Option, pub only_file: Option, pub disable_progress: Option, pub depth: Option, pub bars_on_right: Option, pub stack_size: Option, pub threads: Option, pub output_json: Option, pub print_errors: Option, pub files0_from: Option, } impl Config { pub fn get_files_from(&self, options: &ArgMatches) -> Option { let from_file = options.get_one::("files0_from"); match from_file { None => self.files0_from.as_ref().map(|x| x.to_string()), Some(x) => Some(x.to_string()), } } pub fn get_no_colors(&self, options: &ArgMatches) -> bool { Some(true) == self.no_colors || options.get_flag("no_colors") } pub fn get_force_colors(&self, options: &ArgMatches) -> bool { Some(true) == self.force_colors || options.get_flag("force_colors") } pub fn get_disable_progress(&self, options: &ArgMatches) -> bool { Some(true) == self.disable_progress || options.get_flag("disable_progress") || !std::io::stdout().is_terminal() } pub fn get_apparent_size(&self, options: &ArgMatches) -> bool { Some(true) == self.display_apparent_size || options.get_flag("display_apparent_size") } pub fn get_ignore_hidden(&self, options: &ArgMatches) -> bool { Some(true) == self.ignore_hidden || options.get_flag("ignore_hidden") } pub fn get_full_paths(&self, options: &ArgMatches) -> bool { Some(true) == self.display_full_paths || options.get_flag("display_full_paths") } pub fn get_reverse(&self, options: &ArgMatches) -> bool { Some(true) == self.reverse || options.get_flag("reverse") } pub fn get_no_bars(&self, options: &ArgMatches) -> bool { Some(true) == self.no_bars || options.get_flag("no_bars") } pub fn get_output_format(&self, options: &ArgMatches) -> String { let out_fmt = options.get_one::("output_format"); (match out_fmt { None => match &self.output_format { None => "".to_string(), Some(x) => x.to_string(), }, Some(x) => x.into(), }) .to_lowercase() } pub fn get_skip_total(&self, options: &ArgMatches) -> bool { Some(true) == self.skip_total || options.get_flag("skip_total") } pub fn get_screen_reader(&self, options: &ArgMatches) -> bool { Some(true) == self.screen_reader || options.get_flag("screen_reader") } pub fn get_depth(&self, options: &ArgMatches) -> usize { if let Some(v) = options.get_one::("depth") { return *v; } self.depth.unwrap_or(usize::MAX) } pub fn get_min_size(&self, options: &ArgMatches) -> Option { let size_from_param = options.get_one::("min_size"); self._get_min_size(size_from_param) } fn _get_min_size(&self, min_size: Option<&String>) -> Option { let size_from_param = min_size.and_then(|a| convert_min_size(a)); if size_from_param.is_none() { self.min_size .as_ref() .and_then(|a| convert_min_size(a.as_ref())) } else { size_from_param } } pub fn get_only_dir(&self, options: &ArgMatches) -> bool { Some(true) == self.only_dir || options.get_flag("only_dir") } pub fn get_print_errors(&self, options: &ArgMatches) -> bool { Some(true) == self.print_errors || options.get_flag("print_errors") } pub fn get_only_file(&self, options: &ArgMatches) -> bool { Some(true) == self.only_file || options.get_flag("only_file") } pub fn get_bars_on_right(&self, options: &ArgMatches) -> bool { Some(true) == self.bars_on_right || options.get_flag("bars_on_right") } pub fn get_custom_stack_size(&self, options: &ArgMatches) -> Option { let from_cmd_line = options.get_one::("stack_size"); if from_cmd_line.is_none() { self.stack_size } else { from_cmd_line.copied() } } pub fn get_threads(&self, options: &ArgMatches) -> Option { let from_cmd_line = options.get_one::("threads"); if from_cmd_line.is_none() { self.threads } else { from_cmd_line.copied() } } pub fn get_output_json(&self, options: &ArgMatches) -> bool { Some(true) == self.output_json || options.get_flag("output_json") } pub fn get_modified_time_operator(&self, options: &ArgMatches) -> Option<(Operater, i64)> { get_filter_time_operator( options.get_one::("mtime"), get_current_date_epoch_seconds(), ) } pub fn get_accessed_time_operator(&self, options: &ArgMatches) -> Option<(Operater, i64)> { get_filter_time_operator( options.get_one::("atime"), get_current_date_epoch_seconds(), ) } pub fn get_created_time_operator(&self, options: &ArgMatches) -> Option<(Operater, i64)> { get_filter_time_operator( options.get_one::("ctime"), get_current_date_epoch_seconds(), ) } } fn get_current_date_epoch_seconds() -> i64 { // calcurate current date epoch seconds let now = Local::now(); let current_date = now.date_naive(); let current_date_time = current_date.and_hms_opt(0, 0, 0).unwrap(); Local .from_local_datetime(¤t_date_time) .unwrap() .timestamp() } fn get_filter_time_operator( option_value: Option<&String>, current_date_epoch_seconds: i64, ) -> Option<(Operater, i64)> { match option_value { Some(val) => { let time = current_date_epoch_seconds - val .parse::() .unwrap_or_else(|_| panic!("invalid data format")) .abs() * DAY_SECONDS; match val.chars().next().expect("Value should not be empty") { '+' => Some((Operater::LessThan, time - DAY_SECONDS)), '-' => Some((Operater::GreaterThan, time)), _ => Some((Operater::Equal, time - DAY_SECONDS)), } } None => None, } } fn convert_min_size(input: &str) -> Option { let re = Regex::new(r"([0-9]+)(\w*)").unwrap(); if let Some(cap) = re.captures(input) { let (_, [digits, letters]) = cap.extract(); // Failure to parse should be impossible due to regex match let digits_as_usize: Option = digits.parse().ok(); match digits_as_usize { Some(parsed_digits) => { let number_format = get_number_format(&letters.to_lowercase()); match number_format { Some((multiple, _)) => Some(parsed_digits * (multiple as usize)), None => { if letters.eq("") { Some(parsed_digits) } else { eprintln!("Ignoring invalid min-size: {input}"); None } } } } None => None, } } else { None } } fn get_config_locations(base: &Path) -> Vec { vec![ base.join(".dust.toml"), base.join(".config").join("dust").join("config.toml"), ] } pub fn get_config() -> Config { if let Some(home) = directories::BaseDirs::new() { for path in get_config_locations(home.home_dir()) { if path.exists() { if let Ok(config) = Config::from_config_file(path) { return config; } } } } Config { ..Default::default() } } #[cfg(test)] mod tests { #[allow(unused_imports)] use super::*; use chrono::{Datelike, Timelike}; use clap::{value_parser, Arg, ArgMatches, Command}; #[test] fn test_get_current_date_epoch_seconds() { let epoch_seconds = get_current_date_epoch_seconds(); let dt = Local.timestamp_opt(epoch_seconds, 0).unwrap(); assert_eq!(dt.hour(), 0); assert_eq!(dt.minute(), 0); assert_eq!(dt.second(), 0); assert_eq!(dt.date_naive().day(), Local::now().date_naive().day()); assert_eq!(dt.date_naive().month(), Local::now().date_naive().month()); assert_eq!(dt.date_naive().year(), Local::now().date_naive().year()); } #[test] fn test_conversion() { assert_eq!(convert_min_size("55"), Some(55)); assert_eq!(convert_min_size("12344321"), Some(12344321)); assert_eq!(convert_min_size("95RUBBISH"), None); assert_eq!(convert_min_size("10Ki"), Some(10 * 1024)); assert_eq!(convert_min_size("10MiB"), Some(10 * 1024usize.pow(2))); assert_eq!(convert_min_size("10M"), Some(10 * 1024usize.pow(2))); assert_eq!(convert_min_size("10Mb"), Some(10 * 1000usize.pow(2))); assert_eq!(convert_min_size("2Gi"), Some(2 * 1024usize.pow(3))); } #[test] fn test_min_size_from_config_applied_or_overridden() { let c = Config { min_size: Some("1KiB".to_owned()), ..Default::default() }; assert_eq!(c._get_min_size(None), Some(1024)); assert_eq!(c._get_min_size(Some(&"2KiB".into())), Some(2048)); assert_eq!(c._get_min_size(Some(&"1kb".into())), Some(1000)); assert_eq!(c._get_min_size(Some(&"2KB".into())), Some(2000)); } #[test] fn test_get_depth() { // No config and no flag. let c = Config::default(); let args = get_args(vec![]); assert_eq!(c.get_depth(&args), usize::MAX); // Config is not defined and flag is defined. let c = Config::default(); let args = get_args(vec!["dust", "--depth", "5"]); assert_eq!(c.get_depth(&args), 5); // Config is defined and flag is not defined. let c = Config { depth: Some(3), ..Default::default() }; let args = get_args(vec![]); assert_eq!(c.get_depth(&args), 3); // Both config and flag are defined. let c = Config { depth: Some(3), ..Default::default() }; let args = get_args(vec!["dust", "--depth", "5"]); assert_eq!(c.get_depth(&args), 5); } fn get_args(args: Vec<&str>) -> ArgMatches { Command::new("Dust") .arg( Arg::new("depth") .long("depth") .num_args(1) .value_parser(value_parser!(usize)), ) .get_matches_from(args) } }