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;
#[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<String>,
pub in_container: bool,
pub keymap: Keymap,
pub log_search_case_sensitive: bool,
pub raw_logs: bool,
pub dir_config: Option<PathBuf>,
pub dir_save: Option<PathBuf>,
pub show_logs: bool,
pub show_self: bool,
pub show_std_err: bool,
pub show_timestamp: bool,
pub timestamp_format: String,
pub timezone: Option<TimeZone>,
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,
dir_save: Self::try_get_logs_dir(args.save_dir.as_ref()),
dir_config: args.config_file.as_ref().map(|i| PathBuf::from(&i)),
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<(ConfigFile, Option<PathBuf>)> for Config {
fn from((config_file, dir): (ConfigFile, Option<PathBuf>)) -> 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),
dir_config: dir,
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),
dir_save: 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 {
fn parse_timestamp_format(input: Option<String>) -> 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
}
})
}
fn parse_timezone(input: Option<String>) -> Option<TimeZone> {
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)
}
}
fn check_if_in_container() -> bool {
std::env::var(ENV_KEY).is_ok_and(|i| i == ENV_VALUE)
}
fn try_get_logs_dir(dir: Option<&String>) -> Option<PathBuf> {
dir.as_ref()
.map_or_else(Self::try_get_home_dir, |home_dir| {
Some(std::path::Path::new(&home_dir).to_owned())
})
}
fn try_get_home_dir() -> Option<PathBuf> {
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_owned())
}
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.dir_save {
self.dir_save = 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
}
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(dir_config_file) = &args.config_file
&& let Some(config_file) =
parse_config_file::ConfigFile::try_parse_from_file(dir_config_file)
{
return Self::from((config_file, Some(PathBuf::from(dir_config_file))))
.merge_args(config_from_cli);
}
if let Some((config_file, dir)) = parse_config_file::ConfigFile::try_parse(in_container) {
return Self::from((config_file, Some(dir))).merge_args(config_from_cli);
}
config_from_cli
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use jiff::tz::TimeZone;
#[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]
fn test_config_parse_timezone() {
for i in [None, Some("UTC".to_owned())] {
assert!(super::Config::parse_timezone(i).is_none());
}
let expected = Some(TimeZone::get("Asia/Tokyo").unwrap());
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);
}
}
}