diff --git a/Cargo.toml b/Cargo.toml index 0688588..bf04a50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ paxy = { path = "paxy" } tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = "0.3" +log = "0.4" # Configuration figment = "0.10" @@ -57,7 +58,7 @@ serde-aux = "4.5" serde_yaml = "0.9" tracing-serde = "0.1" speedy = "0.8" -log = "0.4" +itertools = "0.12" # Internationalization fluent = "0.16" diff --git a/paxy-cli/Cargo.toml b/paxy-cli/Cargo.toml index e14712f..546c6ba 100644 --- a/paxy-cli/Cargo.toml +++ b/paxy-cli/Cargo.toml @@ -20,6 +20,7 @@ paxy = { workspace = true } # Logging tracing = { workspace = true } +log = { workspace = true, features = ["serde"] } # Error handling snafu = {workspace = true} diff --git a/paxy-cli/src/lib.rs b/paxy-cli/src/lib.rs index a0883de..0006965 100644 --- a/paxy-cli/src/lib.rs +++ b/paxy-cli/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_cli`] function and the commandline interface template +//! [`cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the CLI mode. pub fn run_cli() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -19,8 +26,9 @@ pub enum Error { } // region: IMPORTS + use owo_colors::OwoColorize; -use paxy::ui; +use paxy::app::ui; use snafu::Snafu; // endregion: IMPORTS @@ -51,7 +59,7 @@ mod cli_template { )] pub struct CliTemplate { #[command(flatten)] - pub global_arguments: ui::cli_template::GlobalArgs, + pub global_args: ui::cli_template::GlobalArgs, #[command(subcommand)] pub entity: Option, @@ -60,43 +68,39 @@ mod cli_template { /// Implement a trait that can extract standard global arguments from our /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_arguments - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { - self.global_arguments - .json_flag + self.global_args + .is_json() } fn is_plain(&self) -> bool { - self.global_arguments - .plain_flag + self.global_args + .is_plain() } fn is_debug(&self) -> bool { - self.global_arguments - .debug_flag + self.global_args + .is_debug() } - fn is_no_color(&self) -> bool { - self.global_arguments - .no_color_flag + fn is_test(&self) -> bool { + self.global_args + .is_test() } - fn is_test(&self) -> bool { - self.global_arguments - .test_flag + fn is_no_color(&self) -> bool { + self.global_args + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_arguments - .verbose + fn verbosity_filter(&self) -> log::LevelFilter { + self.global_args + .verbosity_filter() } } @@ -399,7 +403,7 @@ mod cli_template { use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; - use paxy::ui; + use paxy::app::ui; // endregion: IMPORTS } @@ -408,6 +412,6 @@ mod cli_template { // region: RE-EXPORTS -pub use cli_template::*; +pub use cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-cli/src/main.rs b/paxy-cli/src/main.rs index 7a7b4c3..73509b7 100644 --- a/paxy-cli/src/main.rs +++ b/paxy-cli/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_cli::run_cli`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_cli::run_cli(); match return_value { diff --git a/paxy-gui/Cargo.toml b/paxy-gui/Cargo.toml index c9c1f77..5d7ba36 100644 --- a/paxy-gui/Cargo.toml +++ b/paxy-gui/Cargo.toml @@ -20,6 +20,7 @@ paxy = { workspace = true } # Logging tracing = { workspace = true } +log = { workspace = true, features = ["serde"] } # Error handling snafu = {workspace = true} diff --git a/paxy-gui/src/lib.rs b/paxy-gui/src/lib.rs index 7fb4393..4694307 100644 --- a/paxy-gui/src/lib.rs +++ b/paxy-gui/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_gui`] function and the commandline interface template +//! [`gui_cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the GUI mode. pub fn run_gui() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -15,20 +22,24 @@ pub fn run_gui() -> Result<(), paxy::Error> { pub enum Error { #[non_exhaustive] #[snafu(display(""), visibility(pub))] - GuiDummy {}, + GuiDummy {}, // No errors implemented yet } // region: IMPORTS use owo_colors::OwoColorize; -use paxy::ui; +use paxy::app::ui; use snafu::Snafu; // endregion: IMPORTS // region: MODULES +/// The commandline interface for the GUI program. Allows one to specify flags +/// that control output on a console. mod gui_cli_template { + + /// The base commandline template consists of global arguments #[derive(Parser, Debug)] #[command(version, author, about, args_conflicts_with_subcommands = true)] pub struct CliTemplate { @@ -36,44 +47,42 @@ mod gui_cli_template { pub global_args: ui::cli_template::GlobalArgs, } + /// Implement a trait that can extract standard global arguments from our + /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_args - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { self.global_args - .json_flag + .is_json() } fn is_plain(&self) -> bool { self.global_args - .plain_flag + .is_plain() } fn is_debug(&self) -> bool { self.global_args - .debug_flag + .is_debug() } - fn is_no_color(&self) -> bool { + fn is_test(&self) -> bool { self.global_args - .no_color_flag + .is_test() } - fn is_test(&self) -> bool { + fn is_no_color(&self) -> bool { self.global_args - .test_flag + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_args - .verbose + fn verbosity_filter(&self) -> log::LevelFilter { + self.global_args + .verbosity_filter() } } @@ -82,7 +91,7 @@ mod gui_cli_template { use std::path::PathBuf; use clap::Parser; - use paxy::ui; + use paxy::app::ui; // endregion: IMPORTS } @@ -91,6 +100,6 @@ mod gui_cli_template { // region: RE-EXPORTS -pub use gui_cli_template::*; +pub use gui_cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-gui/src/main.rs b/paxy-gui/src/main.rs index edf4ea4..f96597b 100644 --- a/paxy-gui/src/main.rs +++ b/paxy-gui/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_gui::run_gui`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_gui::run_gui(); match return_value { diff --git a/paxy/Cargo.toml b/paxy/Cargo.toml index 1af4864..479ba7b 100644 --- a/paxy/Cargo.toml +++ b/paxy/Cargo.toml @@ -20,6 +20,7 @@ edition.workspace = true tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } +log = { workspace = true, features = ["serde"] } # Configuration figment = { workspace = true, features = ["toml", "json", "yaml", "env"] } @@ -37,7 +38,7 @@ serde-aux = { workspace = true } serde_yaml = { workspace = true } tracing-serde = { workspace = true } speedy = { workspace = true } -log = { workspace = true, features = ["serde"] } +itertools = { workspace = true } # Internationalization fluent = { workspace = true } @@ -53,7 +54,7 @@ console = { workspace = true } home = "0.5.9" toml = "0.8.10" pollster = "0.3" -reqwest = "0.11" +reqwest = "0.12" url = { version = "2.3", features = ["serde"] } extism = "1.2.0" bson = "2.9.0" diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 0a59e0c..7f45a7c 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -1,19 +1,63 @@ +lazy_static! { + pub static ref CONFIG_FILE_EXTENSIONS: &'static [&'static str] = + &["toml", "json", "yaml", "yml"]; +} + /// Initializes a layered configuration deriving values from the app-wide /// defaults, overridden by values from global paths, overridden by values from /// local paths. overridden by environment variables starting with `PAXY_`, /// overridden by the configuration file specified by the commandline. /// Values from only files with supported file extensions would be merged. -pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec), Error> { - let mut candidate_config_filepath_stubs: Vec = Vec::new(); +pub fn init_config( + console_global_arguments: G, +) -> Result { + // Initialize configuration with app-wide defaults + let mut config = Config::new(); + + // Update config filepaths in the config data structure. This is one way - + // The config filepaths are written to the data structure but not retreived + // from user-provided config files, except for a config file path that is + // received from the console arguments. + let config_filepaths = candidate_config_filepaths()?; + config.figment = config + .figment + .admerge(("config_filepaths", &config_filepaths)); + + // Merge configuration values from global and local filepaths + config = config.with_overriding_files(&config_filepaths); + + // Merge configuration values from environment variables + config = config.with_overriding_env(&format!("{}_", *app::APP_NAME)); + + // Merge configuration values from the CLI + config = config.with_overriding_args(&console_global_arguments); + + // Update log dirpaths in the config data structure. This is one way - The + // log dirpaths are written to the data structure but not retreived from + // user-provided config files. + let preferred_log_dirpath: Option = config + .figment + .extract_inner("log_dirpath") + .ok(); + let log_dirpath = candidate_log_dirpath(preferred_log_dirpath)?; + config.figment = config + .figment + .admerge(("log_dirpath", &log_dirpath)); + + Ok(config.object()?) +} + +fn candidate_config_filepaths() -> Result, Error> { + let mut config_filepaths: Vec = Vec::new(); // Global directories #[cfg(target_family = "unix")] - candidate_config_filepath_stubs.extend(["/etc/xdg".into(), "/etc".into()]); + config_filepaths.extend(["/etc/xdg".into(), "/etc".into()]); #[cfg(target_os = "windows")] candidate_config_filepath_stubs.extend([""]); // Local directories - candidate_config_filepath_stubs.push( + config_filepaths.push( directories::BaseDirs::new() .context(RetreiveConfigUserAppBaseDirectoriesSnafu {})? .config_dir() @@ -21,137 +65,337 @@ pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec) -> Result { + if let Some(preferred_log_dirpath) = preferred_log_dirpath { + if !fs::metadata(&preferred_log_dirpath) + .map(|m| m.permissions()) + .map(|p| p.readonly()) + .unwrap_or(true) + { + Ok(preferred_log_dirpath) + } else { + Ok(fallback_log_dirpath()?) } + } else { + Ok(fallback_log_dirpath()?) } - - Ok(( - figment - .extract() - .context(ExtractConfigSnafu {})?, - candidate_config_filepath_stubs - .iter() - .map(PathBuf::from) - .collect(), - )) } -fn admerge_from_stub(candidate_config_filepath_stub: &PathBuf, mut figment: Figment) -> Figment { - figment = figment.admerge(Toml::file( - candidate_config_filepath_stub.with_extension("toml"), - )); - figment = figment.admerge(Json::file( - candidate_config_filepath_stub.with_extension("json"), - )); - figment = figment.admerge(Yaml::file( - candidate_config_filepath_stub.with_extension("yml"), - )); - figment = figment.admerge(Yaml::file( - candidate_config_filepath_stub.with_extension("yaml"), - )); - figment +fn fallback_log_dirpath() -> Result { + let xdg_app_dirs = + directories::BaseDirs::new().context(RetreiveLoggingUserAppBaseDirectoriesSnafu {})?; + fs::create_dir_all(xdg_app_dirs.data_dir()).context(CreateLogDirectorySnafu { + path: { + let mut state_dirpath = xdg_app_dirs + .data_dir() + .to_owned(); + state_dirpath.push(*app::APP_NAME); + state_dirpath + }, + })?; + Ok(xdg_app_dirs + .data_dir() + .to_owned()) } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Config { - pub log_directory: Option, - - pub log_level_filter: Option, +pub struct ConfigTemplate { + pub config_filepaths: Vec, + pub log_dirpath: PathBuf, + pub console_output_format: ui::ConsoleOutputFormat, +} - pub no_color: Option, +impl Default for ConfigTemplate { + fn default() -> Self { + Self { + config_filepaths: Vec::new(), + log_dirpath: PathBuf::default(), + console_output_format: ui::ConsoleOutputFormat::default(), + } + } } -impl Config { - pub fn new() -> Self { - Self::default() +// Make `ConfigTemplate` a provider itself for composability. +impl figment::Provider for ConfigTemplate { + fn metadata(&self) -> figment::Metadata { + figment::Metadata::named("Config Object") + } + + fn data( + &self, + ) -> Result, figment::Error> { + figment::providers::Serialized::defaults(ConfigTemplate::default()).data() + } + + fn profile(&self) -> Option { + None } } +#[derive(Debug, Clone)] +pub struct Config { + pub figment: Figment, +} + impl Default for Config { fn default() -> Self { - Config { - log_directory: None, - log_level_filter: Some(log::LevelFilter::Info), - no_color: Some(false), + Self { + figment: Figment::from(ConfigTemplate::default()), } } } -impl Provider for Config { - fn metadata(&self) -> Metadata { - Metadata::named("Library Config") +impl Config { + pub fn new() -> Self { + Config::default() } - fn data(&self) -> Result, figment::Error> { - figment::providers::Serialized::defaults(Config::default()).data() + pub fn with_overriding_file>(mut self, filepath: P) -> Self { + let filepath: &Path = filepath.as_ref(); + if let Some(file_extension) = filepath.extension() { + let file_extension = file_extension + .to_string_lossy() + .to_lowercase(); + match file_extension.as_str() { + "toml" => { + self.figment = self + .figment + .admerge(Toml::file(filepath)); + } + "json" => { + self.figment = self + .figment + .admerge(Json::file(filepath)); + } + "yaml" | "yml" => { + self.figment = self + .figment + .admerge(Yaml::file(filepath)); + } + &_ => {} + } + } + + self } - fn profile(&self) -> Option { - None + pub fn with_overriding_files(self, filepaths: I) -> Self + where + P: AsRef, + I: IntoIterator, + { + filepaths + .into_iter() + .fold(self, |config, filepath| { + config.with_overriding_file(filepath) + }) + } + + pub fn with_overriding_filepath_stubs( + mut self, + file_extensions: I1, + filepath_stubs: I2, + ) -> Self + where + I1: IntoIterator, + S: AsRef + Clone, + I2: IntoIterator, + ::IntoIter: Clone, + P: Into + Clone, + { + let filepath_stubs = filepath_stubs.into_iter(); + self = file_extensions + .into_iter() + .cartesian_product(filepath_stubs) + .fold(self, |config, (file_extension, filepath_stub)| { + let mut filepath: PathBuf = filepath_stub.into(); + filepath.set_extension(file_extension.as_ref()); + + config.with_overriding_file(filepath) + }); + + self + } + + pub fn with_overriding_filepath_stub( + self, + file_extensions: I, + filepath_stub: P, + ) -> Self + where + I: IntoIterator, + S: AsRef + Clone, + P: Into + Clone, + { + self.with_overriding_filepath_stubs(file_extensions, iter::once(filepath_stub)) + } + + pub fn with_overriding_env>(mut self, prefix: S) -> Self { + let prefix = prefix.as_ref(); + self.figment = self + .figment + .admerge(Env::prefixed(prefix)); + + self + } + + /// Merge configuration values from the CLI + /// These are not set to be optional, so only action-required states are + /// merged with the configuration + pub fn with_overriding_args(mut self, console_arguments: A) -> Self { + // Incorporate the extra config file specified through arguments + if let Some(path) = console_arguments.config_filepath() { + self.figment = self + .figment + .admerge(("config_filepaths", path)); + } + + // Override console output mode from console arguments only if a + // non-regular output mode is explicitly specified + let console_output_mode = console_arguments.console_output_mode(); + if console_output_mode != ConsoleOutputMode::Regular { + self.figment = self + .figment + .admerge(("console_output_format.mode", console_output_mode)); + } + + // Override max verbosity from console arguments only if a greater + // output verbosity is explicitly specified + let current_max_verbosity = self + .figment + .extract_inner::("console_output_format.max_verbosity"); + let requested_max_verbosity = console_arguments.max_output_verbosity(); + if let Ok(current_max_verbosity) = current_max_verbosity { + if requested_max_verbosity > current_max_verbosity { + self.figment = self + .figment + .admerge(( + "console_output_format.max_verbosity", + requested_max_verbosity, + )) + } + } + + // Override no-color only if no-color is not already enabled and if + // either the environment or the console arguments indicate either + // directly or indirectly that no-color is to be enabled + let requested_no_color = console_arguments.is_no_color() + || console_arguments.is_plain() + || console_arguments.is_json(); + let current_no_color = self + .figment + .extract_inner::("console_output_format.no_color"); + let env_no_color = env::var("NO_COLOR").is_ok() + || env::var(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + .is_ok() + || env::var("TERM").is_ok_and(|env_term_value| env_term_value.to_lowercase() == "dumb"); + if (requested_no_color || env_no_color) && !current_no_color.unwrap_or(false) { + self.figment = self + .figment + .admerge(("console_output_format.no_color", true)); + } + + self + } + + pub fn object(&self) -> Result { + let mut config_object: ConfigTemplate = self + .figment + .extract() + .context(ExtractConfigSnafu {})?; + + config_object + .console_output_format + .internally_consistent(); + + Ok(config_object) } } +// region: IMPORTS + +use std::{ + clone::Clone, + env, + fs, + iter, + path::{Path, PathBuf}, +}; + +use figment::{ + providers::{Env, Format, Json, Toml, Yaml}, + Figment, +}; +use itertools::Itertools; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use snafu::{OptionExt, ResultExt, Snafu}; + +use super::ui::{ConsoleOutputMode, GlobalArguments}; +use crate::app; +use crate::app::ui; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { #[non_exhaustive] #[snafu( - display("could not retreive the XDG base directories for the user"), + display("could not retrieve the XDG base directories for the user"), visibility(pub) )] RetreiveConfigUserAppBaseDirectories {}, #[non_exhaustive] #[snafu( - display("could not retreive configuration information: {source}"), + display("could not retrieve the XDG base directories for the user"), visibility(pub) )] - ExtractConfig { source: figment::Error }, -} - -// region: IMPORTS - -use std::path::{Path, PathBuf}; + RetreiveLoggingUserAppBaseDirectories {}, -use figment::{ - providers::{Env, Format, Json, Toml, Yaml}, - value::{Dict, Map}, - Figment, - Metadata, - Profile, - Provider, -}; -use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; + #[non_exhaustive] + #[snafu( + display("could not create the log directory at {:?}: {source}", path), + visibility(pub) + )] + CreateLogDirectory { + path: PathBuf, + source: std::io::Error, + }, -use crate::app; + #[non_exhaustive] + #[snafu( + display("could not retrieve configuration information: {source}"), + visibility(pub) + )] + ExtractConfig { source: figment::Error }, +} -// endregion: IMPORTS +// endregion: ERRORS diff --git a/paxy/src/app/i18n.rs b/paxy/src/app/i18n.rs index 5a5f554..ef0eadc 100644 --- a/paxy/src/app/i18n.rs +++ b/paxy/src/app/i18n.rs @@ -1,3 +1,13 @@ +// TODO: The module code goes here + +// region: IMPORTS + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -6,8 +16,4 @@ pub enum Error { I18nDummy {}, } -// region: IMPORTS - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS diff --git a/paxy/src/app/logging.rs b/paxy/src/app/logging.rs index c7a6478..62229ec 100644 --- a/paxy/src/app/logging.rs +++ b/paxy/src/app/logging.rs @@ -1,12 +1,21 @@ -pub fn init_log( - preferred_log_dirpath: Option, - preferred_log_level_filter: Option, -) -> Result<(Handle, PathBuf), Error> { +pub fn init_log(config: &config::ConfigTemplate) -> Result { let log_filename = format!("{}.log", *app::APP_NAME); - let log_dirpath = obtain_log_dirpath(preferred_log_dirpath)?; let log_file_appender = - tracing_appender::rolling::daily(log_dirpath.clone(), log_filename.clone()); - let log_level_filter = preferred_log_level_filter.unwrap_or(LevelFilter::INFO); + tracing_appender::rolling::daily(&config.log_dirpath, log_filename.clone()); + let log_level_filter = tracing_level_filter_from_log_level_filter( + config + .console_output_format + .max_verbosity, + ); + + // Turn off colors in the console streams if requested + if config + .console_output_format + .no_color + { + anstream::ColorChoice::Never.write_global(); + owo_colors::set_override(false); + } // Obtain writers to various logging destinations and worker guards (for // keeping the streams alive) @@ -137,55 +146,39 @@ pub fn init_log( tracing::subscriber::set_global_default(subscriber) .context(SetGlobalDefaultSubscriberSnafu {})?; - Ok(( - Handle { - _switch_stdout_inner: Some(Box::new(switch_stdout)), - _switch_stderr_inner: Some(Box::new(switch_stderr)), - worker_guards: vec![ - _file_writer_guard, - _stdout_writer_guard, - _stderr_writer_guard, - ], - }, - { - let mut log_filepath = log_dirpath; - log_filepath.push(log_filename + "*"); - log_filepath - }, - )) -} + let mut logging_handle = Handle { + _switch_stdout_inner: Some(Box::new(switch_stdout)), + _switch_stderr_inner: Some(Box::new(switch_stderr)), + worker_guards: vec![ + _file_writer_guard, + _stdout_writer_guard, + _stderr_writer_guard, + ], + }; -fn obtain_log_dirpath(preferred_log_dirpath: Option) -> Result { - let obtain_fallback_log_dirpath = || { - let xdg_app_dirs = - directories::BaseDirs::new().context(RetreiveLoggingUserAppBaseDirectoriesSnafu {})?; - fs::create_dir_all(xdg_app_dirs.data_dir()).context(CreateLogDirectorySnafu { - path: { - let mut state_dirpath = xdg_app_dirs - .data_dir() - .to_owned(); - state_dirpath.push(*app::APP_NAME); - state_dirpath - }, - })?; - Ok(xdg_app_dirs - .data_dir() - .to_owned()) + // Change the output mode if requested + match config + .console_output_format + .mode + { + ui::ConsoleOutputMode::Plain => logging_handle.switch_to_plain()?, + ui::ConsoleOutputMode::Json => logging_handle.switch_to_json()?, + ui::ConsoleOutputMode::Test => logging_handle.switch_to_test()?, + _ => {} }; - Ok(match preferred_log_dirpath { - Some(preferred_log_dirpath) => { - if !fs::metadata(&preferred_log_dirpath) - .map(|m| m.permissions()) - .map(|p| p.readonly()) - .unwrap_or(true) - { - preferred_log_dirpath - } else { - obtain_fallback_log_dirpath()? - } - } - None => obtain_fallback_log_dirpath()?, - }) + + Ok(logging_handle) +} + +fn tracing_level_filter_from_log_level_filter(level_filter: log::LevelFilter) -> LevelFilter { + match level_filter { + log::LevelFilter::Off => LevelFilter::OFF, + log::LevelFilter::Error => LevelFilter::ERROR, + log::LevelFilter::Warn => LevelFilter::WARN, + log::LevelFilter::Info => LevelFilter::INFO, + log::LevelFilter::Debug => LevelFilter::DEBUG, + log::LevelFilter::Trace => LevelFilter::TRACE, + } } type OutputModeSwitchFunction = Box Result<(), Error>>; @@ -235,23 +228,6 @@ pub enum LoggingMode { #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { - #[non_exhaustive] - #[snafu( - display("could not retreive the XDG base directories for the user"), - visibility(pub) - )] - RetreiveLoggingUserAppBaseDirectories {}, - - #[non_exhaustive] - #[snafu( - display("could not create the log directory at {:?}: {source}", path), - visibility(pub) - )] - CreateLogDirectory { - path: PathBuf, - source: std::io::Error, - }, - #[non_exhaustive] #[snafu( display("could not set the global default tracing subscriber: {source}"), @@ -300,10 +276,8 @@ pub enum Error { // region: IMPORTS -use std::{fs, path::PathBuf}; - use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use tracing::{Level, Metadata}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{ @@ -314,6 +288,6 @@ use tracing_subscriber::{ Layer, }; -use crate::app; +use crate::app::{self, config, ui}; // endregion: IMPORTS diff --git a/paxy/src/app/mod.rs b/paxy/src/app/mod.rs index ddba724..ab1280b 100644 --- a/paxy/src/app/mod.rs +++ b/paxy/src/app/mod.rs @@ -2,6 +2,15 @@ lazy_static! { pub static ref APP_NAME: &'static str = "paxy"; } +// region: IMPORTS + +use lazy_static::lazy_static; +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -19,6 +28,13 @@ pub enum Error { source: config::Error, }, + #[non_exhaustive] + #[snafu(display("in the UI: {source}"), visibility(pub))] + Ui { + #[snafu(backtrace)] + source: ui::Error, + }, + #[non_exhaustive] #[snafu(display("in internationalization: {source}"), visibility(pub))] Internationalization { @@ -27,27 +43,13 @@ pub enum Error { }, } -// region: IMPORTS -use lazy_static::lazy_static; -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod config; pub mod i18n; pub mod logging; +pub mod ui; -// endregion: MODULES - -// region: RE-EXPORTS - -#[allow(unused_imports)] -pub use config::*; -#[allow(unused_imports)] -pub use i18n::*; -#[allow(unused_imports)] -pub use logging::*; - -// endregion: RE-EXPORTS +// endregion: EXTERNAL-SUBMODULES diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs new file mode 100644 index 0000000..6a6cc11 --- /dev/null +++ b/paxy/src/app/ui.rs @@ -0,0 +1,438 @@ +#[tracing::instrument(level = "trace")] +pub fn run_common() -> Result<(C, Vec), crate::Error> +where + C: clap::Parser + GlobalArguments + fmt::Debug, +{ + // Obtain CLI arguments + let console_input = C::parse(); + + // Obtain user configuration + let config = config::init_config(&console_input) + .context(app::ConfigSnafu {}) + .context(crate::AppSnafu)?; + + // Begin logging and outputting + let logging_handle = logging::init_log(&config) + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?; + + emit_welcome_messages(); + + emit_diagnostic_messages(&config); + + emit_test_messages(&config, &console_input); + + Ok((console_input, logging_handle.worker_guards)) +} + +fn emit_welcome_messages() { + // Welcome messages + tracing::debug!( + "{} - {}", + "Paxy".bold(), + "A package manager that gets out of your way".magenta() + ); + tracing::debug!( + "{} {} {}", + console::Emoji("โœ‰๏ธ", ""), + "shivanandvp".italic(), + "".italic() + ); +} + +fn emit_diagnostic_messages(config: &ConfigTemplate) { + tracing::trace!( + "{} {} messages {}...", + console::Emoji("๐Ÿ”", ""), + "Diagnostic" + .cyan() + .dimmed(), + "begin" + .green() + .dimmed(), + ); + + tracing::debug!( + "{} The {} is {}... {}", + console::Emoji("โš™๏ธ", ""), + "configuration".cyan(), + "loaded".green(), + console::Emoji("โœ…", ""), + ); + tracing::debug!( + "{} The {} has {}... {}", + console::Emoji("๐Ÿ“", ""), + "logging".cyan(), + "begun".green(), + console::Emoji("โœ…", ""), + ); + + tracing::debug!( + "{} {} {:?}", + console::Emoji("๐Ÿ“‚", ""), + "Config Filepath(s):".magenta(), + config.config_filepaths, + ); + + tracing::debug!( + "{} {} {:?}", + console::Emoji("๐Ÿ“‚", ""), + "Log Directory Path:".magenta(), + config.log_dirpath + ); + + tracing::trace!( + "{} {} messages {}...", + console::Emoji("๐Ÿ”", ""), + "Diagnostic" + .cyan() + .dimmed(), + "end" + .green() + .dimmed(), + ); +} + +fn emit_test_messages(config: &ConfigTemplate, console_input: &C) +where + C: clap::Parser + fmt::Debug, +{ + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "โ–ˆโ–ˆโ–ˆ".black(), + "โ–ˆโ–ˆโ–ˆ".red(), + "โ–ˆโ–ˆโ–ˆ".green(), + "โ–ˆโ–ˆโ–ˆ".yellow(), + "โ–ˆโ–ˆโ–ˆ".blue(), + "โ–ˆโ–ˆโ–ˆ".purple(), + "โ–ˆโ–ˆโ–ˆ".cyan(), + "โ–ˆโ–ˆโ–ˆ".white() + ); + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "โ–ˆโ–ˆโ–ˆ".bright_black(), + "โ–ˆโ–ˆโ–ˆ".bright_red(), + "โ–ˆโ–ˆโ–ˆ".bright_green(), + "โ–ˆโ–ˆโ–ˆ".bright_yellow(), + "โ–ˆโ–ˆโ–ˆ".bright_blue(), + "โ–ˆโ–ˆโ–ˆ".bright_purple(), + "โ–ˆโ–ˆโ–ˆ".bright_cyan(), + "โ–ˆโ–ˆโ–ˆ".bright_white() + ); + + tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("๐Ÿงช", "")); + tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("๐Ÿงช", "")); + tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("๐Ÿงช", "")); + tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("๐Ÿงช", "")); + tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("๐Ÿงช", "")); + + tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("๐Ÿงช", ""), "{\"JSON\": \"Target\"}"); + tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("๐Ÿงช", "")); + + tracing::info!( + target:"TEST", + "{} {} {:#?}", + console::Emoji("โŒจ๏ธ", ""), + "CLI input arguments:" + .magenta() + .dimmed(), + console_input.dimmed() + ); + + tracing::info!( + target:"TEST", + "{} {} {:#?}", + console::Emoji("โŒจ๏ธ", ""), + "Config dump:" + .magenta() + .dimmed(), + config.dimmed() + ); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsoleOutputFormat { + pub mode: ConsoleOutputMode, + pub max_verbosity: log::LevelFilter, + pub no_color: bool, +} + +impl Default for ConsoleOutputFormat { + fn default() -> Self { + Self { + mode: ConsoleOutputMode::default(), + max_verbosity: log::LevelFilter::Info, + no_color: false, + } + } +} + +impl ConsoleOutputFormat { + /// Make the console output format internally consistent. + /// 1. Adjust the no-color setting to be consistent with the console output + /// mode - there should be no color in plain and json modes. + /// 2. Adjust the max verbosity to be consistent with the console output + /// mode - decrease the max verbosity if in plain or json modes. + pub fn internally_consistent(&mut self) -> &Self { + self.internally_consistent_color(); + self.internally_consistent_verbosity(); + + self + } + + /// Adjust the no-color setting to be consistent with the console output + /// mode - there should be no color in plain and json modes. + /// If color is already disabled, do not enable it. Otherwise toggle + /// no-color based on the console output mode. + fn internally_consistent_color(&mut self) -> &Self { + if !self.no_color + && matches!( + self.mode, + ConsoleOutputMode::Plain | ConsoleOutputMode::Json + ) + { + self.no_color = false; + } + + self + } + + /// Adjust the max verbosity to be consistent with the console output + /// mode - decrease the max verbosity if in plain or json modes. + fn internally_consistent_verbosity(&mut self) -> &Self { + if self.max_verbosity > log::LevelFilter::Info + && matches!( + self.mode, + ConsoleOutputMode::Plain | ConsoleOutputMode::Json + ) + { + self.max_verbosity = log::LevelFilter::Info; + } + + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ConsoleOutputMode { + Regular, + Plain, + Json, + Test, +} + +impl Default for ConsoleOutputMode { + fn default() -> Self { + ConsoleOutputMode::Regular + } +} + +pub trait GlobalArguments { + fn config_filepath(&self) -> &Option; + + fn is_json(&self) -> bool; + + fn is_plain(&self) -> bool; + + fn is_debug(&self) -> bool; + + fn is_no_color(&self) -> bool; + + fn is_test(&self) -> bool; + + fn verbosity_filter(&self) -> log::LevelFilter; + + fn console_output_mode(&self) -> ConsoleOutputMode { + if self.is_json() { + ConsoleOutputMode::Json + } else if self.is_plain() { + ConsoleOutputMode::Plain + } else if self.is_test() { + ConsoleOutputMode::Test + } else { + ConsoleOutputMode::Regular + } + } + + fn max_output_verbosity(&self) -> log::LevelFilter { + if self.is_plain() || self.is_json() { + log::LevelFilter::Info + } else if self.is_debug() && log::LevelFilter::Debug > self.verbosity_filter() { + log::LevelFilter::Debug + } else { + self.verbosity_filter() + } + } +} + +impl GlobalArguments for &T { + fn config_filepath(&self) -> &Option { + (**self).config_filepath() + } + + fn is_json(&self) -> bool { + (**self).is_json() + } + + fn is_plain(&self) -> bool { + (**self).is_plain() + } + + fn is_debug(&self) -> bool { + (**self).is_debug() + } + + fn is_no_color(&self) -> bool { + (**self).is_no_color() + } + + fn is_test(&self) -> bool { + (**self).is_test() + } + + fn verbosity_filter(&self) -> log::LevelFilter { + (**self).verbosity_filter() + } +} + +#[derive(Debug, Snafu)] +#[non_exhaustive] +pub enum Error { + #[non_exhaustive] + #[snafu(display(""))] + Dummy {}, +} + +// region: IMPORTS + +use core::fmt; +use std::path::PathBuf; + +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; +use tracing_appender::non_blocking::WorkerGuard; + +use crate::app::{self, config, logging}; + +// endregion: IMPORTS + +// region: MODULES + +/// Common commandline interface template for global arguments, intended to be +/// shared between the GUI and CLI programs. +pub mod cli_template { + #[derive(Clone, Debug, Args)] + #[command(next_display_order = usize::MAX - 100)] + pub struct GlobalArgs + where + L: clap_verbosity_flag::LogLevel, + { + #[arg( + long = "config", + short = 'c', + help = "Path to the configuration file to use.", + global = true, + display_order = usize::MAX - 6 + )] + pub config_file: Option, + + #[arg( + long = "json", + help = "Output in the JSON format for machine readability and scripting purposes.", + global = true, + display_order = usize::MAX - 5 + )] + pub json_flag: bool, + + #[arg( + long = "plain", + help = "Output as plain text without extra information, for machine readability and scripting purposes.", + global = true, + display_order = usize::MAX - 4 + )] + pub plain_flag: bool, + + #[arg( + long = "debug", + help = "Output debug messages.", + global = true, + display_order = usize::MAX - 3 + )] + pub debug_flag: bool, + + #[arg( + long = "no-color", + help = "Disable output coloring.", + global = true, + display_order = usize::MAX - 2 + )] + pub no_color_flag: bool, + + #[arg( + long = "test", + help = "Avoid destructive modifications and show all output subject to the commandline filters. Useful for dry-runs and for developers.", + global = true, + display_order = usize::MAX - 1 + )] + pub test_flag: bool, + + #[command(flatten)] + pub verbosity: clap_verbosity_flag::Verbosity, + } + + impl GlobalArguments for GlobalArgs + where + L: clap_verbosity_flag::LogLevel, + { + fn config_filepath(&self) -> &Option { + &self.config_file + } + + fn is_json(&self) -> bool { + self.json_flag + } + + fn is_plain(&self) -> bool { + self.plain_flag + } + + fn is_debug(&self) -> bool { + self.debug_flag + } + + fn is_test(&self) -> bool { + self.test_flag + } + + fn is_no_color(&self) -> bool { + self.no_color_flag + } + + fn verbosity_filter(&self) -> log::LevelFilter { + self.verbosity + .log_level_filter() + } + } + + // region: IMPORTS + + use std::path::PathBuf; + + use clap::Args; + + use super::GlobalArguments; + + // endregion: IMPORTS +} + +// endregion: MODULES + +// region: RE-EXPORTS + +#[allow(unused_imports)] +pub use cli_template::*; + +use super::config::ConfigTemplate; // Flatten the module heirarchy for easier access + +// endregion: RE-EXPORTS diff --git a/paxy/src/lib.rs b/paxy/src/lib.rs index 0f1bfa9..0790bf0 100644 --- a/paxy/src/lib.rs +++ b/paxy/src/lib.rs @@ -2,6 +2,16 @@ pub fn type_of(_: &T) -> &str { any::type_name::() } +// region: IMPORTS + +use std::any; + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -12,13 +22,6 @@ pub enum Error { source: app::Error, }, - #[non_exhaustive] - #[snafu(display("in the UI: {source}"), visibility(pub))] - Ui { - #[snafu(backtrace)] - source: ui::Error, - }, - #[non_exhaustive] #[snafu(display("in an action:{source}"), visibility(pub))] Actions { @@ -27,19 +30,12 @@ pub enum Error { }, } -// region: IMPORTS - -use std::any; - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod actions; pub mod app; pub mod data; -pub mod ui; -// endregion: MODULES +// endregion: EXTERNAL-SUBMODULES diff --git a/paxy/src/ui/mod.rs b/paxy/src/ui/mod.rs deleted file mode 100644 index d1d0756..0000000 --- a/paxy/src/ui/mod.rs +++ /dev/null @@ -1,323 +0,0 @@ -#[tracing::instrument(level = "trace")] -pub fn run_common() -> Result<(C, Vec), crate::Error> -where - C: clap::Parser + CliModifier + fmt::Debug, - ::L: LogLevel, -{ - // Obtain CLI arguments - let cli_input = C::parse(); - - // Obtain user configuration - let (config, config_filepaths) = config::init_config( - cli_input - .config_file() - .as_ref() - .map(|f| PathBuf::as_path(&f)), - ) - .context(app::ConfigSnafu {}) - .context(crate::AppSnafu)?; - - // Turn off colors if needed - let mut is_cli_uncolored = cli_input.is_uncolored(); - if !is_cli_uncolored { - if let Some(no_color) = config.no_color { - is_cli_uncolored = no_color; - } - } - if is_cli_uncolored { - anstream::ColorChoice::Never.write_global(); - owo_colors::set_override(false); - } - - // Begin logging with preferred log directory and preferred verbosity - let config_log_dirpath = config - .log_directory - .as_ref() - .map(PathBuf::from); - let config_verbosity_filter: Option = config - .log_level_filter - .and_then(|lf| { - lf.as_str() - .parse() - .ok() - }); - let verbosity_filter = cli_input - .verbosity_filter() - .or(config_verbosity_filter); - let (mut handle, log_filepath) = logging::init_log(config_log_dirpath, verbosity_filter) - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - - // Modify logging behavior if Plain or Json output is desired - if cli_input.is_json() { - handle - .switch_to_json() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_plain() { - handle - .switch_to_plain() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_test() { - handle - .switch_to_test() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } - - // Welcome message - tracing::debug!( - "{} - {}", - "Paxy".bold(), - "A package manager that gets out of your way".magenta() - ); - tracing::debug!( - "{} {} {}", - console::Emoji("โœ‰๏ธ", ""), - "shivanandvp".italic(), - "".italic() - ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "โ–ˆโ–ˆโ–ˆ".black(), - "โ–ˆโ–ˆโ–ˆ".red(), - "โ–ˆโ–ˆโ–ˆ".green(), - "โ–ˆโ–ˆโ–ˆ".yellow(), - "โ–ˆโ–ˆโ–ˆ".blue(), - "โ–ˆโ–ˆโ–ˆ".purple(), - "โ–ˆโ–ˆโ–ˆ".cyan(), - "โ–ˆโ–ˆโ–ˆ".white() - ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "โ–ˆโ–ˆโ–ˆ".bright_black(), - "โ–ˆโ–ˆโ–ˆ".bright_red(), - "โ–ˆโ–ˆโ–ˆ".bright_green(), - "โ–ˆโ–ˆโ–ˆ".bright_yellow(), - "โ–ˆโ–ˆโ–ˆ".bright_blue(), - "โ–ˆโ–ˆโ–ˆ".bright_purple(), - "โ–ˆโ–ˆโ–ˆ".bright_cyan(), - "โ–ˆโ–ˆโ–ˆ".bright_white() - ); - - if cli_input.is_test() { - // Test messages - tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("๐Ÿงช", "")); - tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("๐Ÿงช", "")); - tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("๐Ÿงช", "")); - tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("๐Ÿงช", "")); - tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("๐Ÿงช", "")); - - tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("๐Ÿงช", ""), "{\"JSON\": \"Target\"}"); - tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("๐Ÿงช", "")); - } - - tracing::debug!( - "{} The {} is {}... {}", - console::Emoji("โš™๏ธ", ""), - "configuration".cyan(), - "loaded".green(), - console::Emoji("โœ…", ""), - ); - tracing::debug!( - "{} The {} has {}... {}", - console::Emoji("๐Ÿ“", ""), - "logging".cyan(), - "begun".green(), - console::Emoji("โœ…", ""), - ); - - tracing::debug!( - "{} {} {:?}", - console::Emoji("๐Ÿ“‚", ""), - "Config Filepath(s) (without file extensions):".magenta(), - config_filepaths, - ); - tracing::debug!( - "{} {} {:?}", - console::Emoji("๐Ÿ“‚", ""), - "Log Filepath:".magenta(), - log_filepath - ); - - tracing::trace!( - "{} {} {:#?}", - console::Emoji("โŒจ๏ธ", ""), - "CLI input arguments:" - .magenta() - .dimmed(), - cli_input.dimmed() - ); - - Ok((cli_input, handle.worker_guards)) -} - -impl CliModifier for T -where - T: GlobalArguments, - ::L: LogLevel, -{ -} - -pub trait CliModifier: GlobalArguments -where - ::L: LogLevel, -{ - fn verbosity_filter(&self) -> Option { - if self.is_plain() || self.is_json() { - return Some(LevelFilter::INFO); - } - - let verbosity_flag_filter = self - .verbosity() - .log_level_filter(); - - if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() { - return Some(LevelFilter::DEBUG); - } - - verbosity_flag_filter - .as_str() - .parse() - .ok() - } - - fn is_uncolored(&self) -> bool { - self.is_plain() - || self.is_json() - || self.is_no_color() - || env::var(format!( - "{}_NO_COLOR", - String::from(*app::APP_NAME).to_uppercase() - )) - .map_or(false, |value| !value.is_empty()) - } - - fn is_colored(&self) -> bool { - !self.is_uncolored() - } -} - -pub trait GlobalArguments { - type L; - - fn config_file(&self) -> &Option; - - fn is_json(&self) -> bool; - - fn is_plain(&self) -> bool; - - fn is_debug(&self) -> bool; - - fn is_no_color(&self) -> bool; - - fn is_test(&self) -> bool; - - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity - where - Self::L: LogLevel; -} - -#[derive(Debug, Snafu)] -#[non_exhaustive] -pub enum Error { - #[non_exhaustive] - #[snafu(display(""))] - Dummy {}, -} - -// region: IMPORTS - -use core::fmt; -use std::{env, path::PathBuf}; - -use clap_verbosity_flag::LogLevel; -use owo_colors::OwoColorize; -use snafu::{ResultExt, Snafu}; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::filter::LevelFilter; - -use crate::app::{self, config, logging}; - -// endregion: IMPORTS - -// region: MODULES - -pub mod cli_template { - #[derive(Clone, Debug, Args)] - #[command(next_display_order = usize::MAX - 100)] - pub struct GlobalArgs - where - L: clap_verbosity_flag::LogLevel, - { - #[arg( - long = "config", - short = 'c', - help = "Path to the configuration file to use.", - global = true, - display_order = usize::MAX - 6 - )] - pub config_file: Option, - - #[arg( - long = "json", - help = "Output in the JSON format for machine readability and scripting purposes.", - global = true, - display_order = usize::MAX - 5 - )] - pub json_flag: bool, - - #[arg( - long = "plain", - help = "Output as plain text without extra information, for machine readability and scripting purposes.", - global = true, - display_order = usize::MAX - 4 - )] - pub plain_flag: bool, - - #[arg( - long = "debug", - help = "Output debug messages.", - global = true, - display_order = usize::MAX - 3 - )] - pub debug_flag: bool, - - #[arg( - long = "no-color", - help = "Disable output coloring.", - global = true, - display_order = usize::MAX - 2 - )] - pub no_color_flag: bool, - - #[arg( - long = "test", - help = "Avoid destructive modifications and show all output subject to the commandline filters. Useful for dry-runs and for developers.", - global = true, - display_order = usize::MAX - 1 - )] - pub test_flag: bool, - - #[command(flatten)] - pub verbose: clap_verbosity_flag::Verbosity, - } - - // region: IMPORTS - - use std::path::PathBuf; - - use clap::Args; - - // endregion: IMPORTS -} - -// endregion: MODULES - -// region: RE-EXPORTS - -#[allow(unused_imports)] -pub use cli_template::*; - -// endregion: RE-EXPORTS diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000