use std::path::PathBuf; use clap::Parser; use jiff::tz::TimeZone; use parse_args::Args; use parse_config_file::ConfigFile; mod color_parser; mod keymap_parser; use crate::{ENV_KEY, ENV_VALUE}; pub use {color_parser::AppColors, keymap_parser::Keymap}; mod parse_args; mod parse_config_file; // TODO use a global pub static oncelock for the config // static CELL: OnceLock = OnceLock::new(); #[derive(Debug, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct Config { pub app_colors: AppColors, pub color_logs: bool, pub docker_interval_ms: u32, pub gui: bool, pub host: Option, pub in_container: bool, pub keymap: Keymap, pub log_search_case_sensitive: bool, pub raw_logs: bool, pub save_dir: Option, pub show_logs: bool, pub show_self: bool, pub show_std_err: bool, pub show_timestamp: bool, pub timestamp_format: String, pub timezone: Option, pub use_cli: bool, } impl From<&Args> for Config { fn from(args: &Args) -> Self { Self { app_colors: AppColors::new(), color_logs: args.color, docker_interval_ms: args.docker_interval, gui: !args.gui, host: args.host.clone(), in_container: Self::check_if_in_container(), keymap: Keymap::new(), log_search_case_sensitive: true, raw_logs: args.raw, save_dir: Self::try_get_logs_dir(args.save_dir.as_ref()), show_logs: true, show_self: !args.show_self, show_std_err: !args.no_std_err, show_timestamp: !args.timestamp, timestamp_format: Self::parse_timestamp_format(None), timezone: Self::parse_timezone(args.timezone.clone()), use_cli: args.use_cli, } } } impl From for Config { fn from(config_file: ConfigFile) -> Self { Self { app_colors: AppColors::from(config_file.colors), color_logs: config_file.color_logs.unwrap_or(false), docker_interval_ms: config_file.docker_interval.unwrap_or(1000), gui: config_file.gui.unwrap_or(true), host: config_file.host, in_container: Self::check_if_in_container(), keymap: Keymap::from(config_file.keymap), log_search_case_sensitive: config_file.log_search_case_sensitive.unwrap_or(true), raw_logs: config_file.raw_logs.unwrap_or(false), save_dir: Self::try_get_logs_dir(config_file.save_dir.as_ref()), show_logs: config_file.show_logs.unwrap_or(true), show_self: config_file.show_self.unwrap_or(false), show_std_err: config_file.show_std_err.unwrap_or(true), show_timestamp: config_file.show_timestamp.unwrap_or(true), timestamp_format: Self::parse_timestamp_format(config_file.timestamp_format), timezone: Self::parse_timezone(config_file.timezone), use_cli: config_file.use_cli.unwrap_or(false), } } } impl Config { /// A basic timestampt format parser, will only take 32 chars, and checks if the parsed timestamp isn't identical to the given formatter fn parse_timestamp_format(input: Option) -> String { let default = || "%Y-%m-%dT%H:%M:%S.%8f".to_owned(); input.map_or_else(default, |input| { if input.chars().count() >= 32 || jiff::Timestamp::now().strftime(&input).to_string() == input { default() } else { input } }) } /// Attempt to parse a timezone into a jiff::tz::TimeZone /// Also return a format to display the timesampt in fn parse_timezone(input: Option) -> Option { let timezone_str = input?; let Ok(tz) = jiff::tz::TimeZone::get(&timezone_str) else { return None; }; let current_ts = jiff::Timestamp::now(); let offset = tz.to_offset(current_ts); if jiff::tz::TimeZone::UTC.to_offset(current_ts) == offset { None } else { Some(tz) } } /// Check if oxker is running inside of a container fn check_if_in_container() -> bool { std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE) } /// If a cli_arg is provided, create a pathbuf from it, else try to get home_dir automatically fn try_get_logs_dir(dir: Option<&String>) -> Option { dir.as_ref() .map_or_else(Self::try_get_home_dir, |home_dir| { Some(std::path::Path::new(&home_dir).to_owned()) }) } /// Try to get the home dir of the current user fn try_get_home_dir() -> Option { directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned()) } /// Combine config from CLI into config file, the cli take priority /// and also make sure color_logs and raw_logs can't clash fn merge_args(mut self, config_from_cli: Self) -> Self { let default_args = Args::default(); if config_from_cli.color_logs != default_args.color { self.color_logs = config_from_cli.color_logs; self.raw_logs = !self.color_logs; } if config_from_cli.raw_logs != default_args.raw { self.raw_logs = config_from_cli.raw_logs; self.color_logs = !self.raw_logs; } if config_from_cli.gui != default_args.gui { self.gui = config_from_cli.gui; } if config_from_cli.docker_interval_ms != default_args.docker_interval { self.docker_interval_ms = config_from_cli.docker_interval_ms; } if config_from_cli.docker_interval_ms < 1000 { self.docker_interval_ms = default_args.docker_interval; } if config_from_cli.raw_logs != default_args.raw { self.raw_logs = config_from_cli.raw_logs; } if config_from_cli.show_self != default_args.show_self { self.show_self = config_from_cli.show_self; } if config_from_cli.show_std_err != default_args.no_std_err { self.show_std_err = config_from_cli.show_std_err; } if config_from_cli.show_timestamp != default_args.timestamp { self.show_timestamp = config_from_cli.show_timestamp; } if config_from_cli.use_cli != default_args.use_cli { self.use_cli = config_from_cli.use_cli; } if let Some(host) = config_from_cli.host { self.host = Some(host); } if let Some(x) = config_from_cli.save_dir { self.save_dir = Some(x); } if let Some(tz) = config_from_cli.timezone { self.timezone = Some(tz); } if self.color_logs && self.raw_logs { self.raw_logs = false; } self } /// Generate a new config file /// First check cli args, /// then if a config file location is given check then /// Else check the default location /// else just return the default config + the cli args /// cli args will take precedence over config settings pub fn new() -> Self { let in_container = Self::check_if_in_container(); let args = Args::parse(); let config_from_cli = Self::from(&args); if let Some(config_file) = &args.config_file && let Some(config_file) = parse_config_file::ConfigFile::try_parse_from_file(config_file) { return Self::from(config_file).merge_args(config_from_cli); } if let Some(config_file) = parse_config_file::ConfigFile::try_parse(in_container) { return Self::from(config_file).merge_args(config_from_cli); } config_from_cli } } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use jiff::tz::TimeZone; /// Test the basic timestamp_format parsing/checker function #[test] fn test_config_parse_timestamp_format() { let default = "%Y-%m-%dT%H:%M:%S.%8f"; let result = super::Config::parse_timestamp_format(None); assert_eq!(result, default); let result = super::Config::parse_timestamp_format(Some(String::new())); assert_eq!(result, default); let result = super::Config::parse_timestamp_format(Some(" ".to_owned())); assert_eq!(result, default); let result = super::Config::parse_timestamp_format(Some(" ".to_owned())); assert_eq!(result, default); let result = super::Config::parse_timestamp_format(Some("not a valid formatter".to_owned())); assert_eq!(result, default); let result = super::Config::parse_timestamp_format(Some( "%A, %B %d, %Y %I:%M %p %A, %B %d, %Y %I:%M %p".to_owned(), )); assert_eq!(result, default); let input = "%Y-%m-%d %H:%M:%S"; let result = super::Config::parse_timestamp_format(Some(input.to_owned())); assert_eq!(result, input); let input = "%Y-%j"; let result = super::Config::parse_timestamp_format(Some(input.to_owned())); assert_eq!(result, input); } #[test] /// Test various timezones get parsed correctly fn test_config_parse_timezone() { // Timezone with no offset just return None for i in [None, Some("UTC".to_owned())] { assert!(super::Config::parse_timezone(i).is_none()); } let expected = Some(TimeZone::get("Asia/Tokyo").unwrap()); // string case ignored for i in ["ASIA/TOKYO", "asia/tokyo", "aSiA/tOkYo"] { let result = super::Config::parse_timezone(Some(i.to_owned())); assert!(result.is_some()); assert_eq!(result, expected); } } }