diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-18 20:56:05 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-18 20:56:05 +0800 |
| commit | 68daa10abfe3015beca966825d32cf67c9f5d5d7 (patch) | |
| tree | c28f0470240e7cdc3748cee57ef74353514c47b7 /rola-cli/src | |
| parent | 669898193bebeadc975881bee496fe0239df76a0 (diff) | |
feat(bucket): implement bucket initialization and logging infrastructure
Add bucket init logic with directory structure creation and log macros
for tracing
Diffstat (limited to 'rola-cli/src')
| -rw-r--r-- | rola-cli/src/bin/rola.rs | 14 | ||||
| -rw-r--r-- | rola-cli/src/error/io.rs | 14 | ||||
| -rw-r--r-- | rola-cli/src/lib.rs | 5 | ||||
| -rw-r--r-- | rola-cli/src/output.rs | 6 | ||||
| -rw-r--r-- | rola-cli/src/output/ansi_control.rs | 32 | ||||
| -rw-r--r-- | rola-cli/src/output/ansi_control/colorize_wrapper.rs | 209 | ||||
| -rw-r--r-- | rola-cli/src/output/display.rs | 384 | ||||
| -rw-r--r-- | rola-cli/src/output/env_logger.rs | 54 | ||||
| -rw-r--r-- | rola-cli/src/output/setup.rs | 41 |
9 files changed, 751 insertions, 8 deletions
diff --git a/rola-cli/src/bin/rola.rs b/rola-cli/src/bin/rola.rs index 0cfe675..a201953 100644 --- a/rola-cli/src/bin/rola.rs +++ b/rola-cli/src/bin/rola.rs @@ -5,7 +5,10 @@ use mingling::{ macros::program_setup, setup::{ExitCodeSetup, HelpFlagSetup, QuietFlagSetup}, }; -use rola_cli::{ThisProgram, locale, res::current_dir::ResCurrentDir}; +use rola_cli::{ + ThisProgram, locale, output::ColorOutputSetup, output::EnvLoggerSetup, + res::current_dir::ResCurrentDir, +}; fn main() { let mut program = ThisProgram::new(); @@ -32,11 +35,18 @@ fn main() { program.with_setup(HelpFlagSetup::new(["-h", "--help"])); program.with_setup(StandardOutputSetup); program.with_setup(ExitCodeSetup::default()); + program.with_setup(ColorOutputSetup); - // Execute + // stdout/stderr control let quiet = program.stdout_setting.quiet; let error_output = program.stdout_setting.error_output && !quiet; let render_output = program.stdout_setting.render_output && !quiet; + + if error_output { + program.with_setup(EnvLoggerSetup); + } + + // Execute let result = program.exec_without_render().unwrap(); if !result.is_empty() { if result.exit_code == 0 && render_output { diff --git a/rola-cli/src/error/io.rs b/rola-cli/src/error/io.rs index 7e4de56..d65b765 100644 --- a/rola-cli/src/error/io.rs +++ b/rola-cli/src/error/io.rs @@ -61,7 +61,14 @@ pub enum ErrorIo { pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { let err: std::io::Error = err.into(); let content = format!("{:?}", err); - let (error_info, exit_code) = match err.kind() { + let (error_info, exit_code) = io_error_info(err, content); + + r_println!("{}{}", I18nIoError::io_error_name(), error_info); + ec.exit_code = exit_code; +} + +fn io_error_info(err: std::io::Error, content: String) -> (String, i32) { + match err.kind() { std::io::ErrorKind::NotFound => (I18nIoError::not_found(content), EC_IOERR_NOT_FOUND), std::io::ErrorKind::PermissionDenied => ( I18nIoError::permission_denied(content), @@ -185,10 +192,7 @@ pub fn render_error_io(err: ErrorIo, ec: &mut ResExitCode) { } std::io::ErrorKind::Other => (I18nIoError::other(content), EC_IOERR_OTHER), _ => (I18nIoError::other(content), EC_IOERR_OTHER), - }; - - r_println!("{}: {}", I18nIoError::io_error_name(), error_info); - ec.exit_code = exit_code; + } } impl From<std::io::Error> for ErrorIo { diff --git a/rola-cli/src/lib.rs b/rola-cli/src/lib.rs index 54ff09a..b3587e2 100644 --- a/rola-cli/src/lib.rs +++ b/rola-cli/src/lib.rs @@ -2,6 +2,7 @@ use std::process::exit; use mingling::macros::{gen_program, help}; +pub mod output; pub mod res; pub mod tokio_wrapper; @@ -11,12 +12,14 @@ use bucket_mgr::*; mod error; use error::*; +use crate::output::display::markdown; + #[help] fn handle_error_dispatch_not_found(_err: ErrorDispatcherNotFound) { let help = locale::helps::Basic::help().trim(); // Print directly to stderr and exit with code 0 - eprintln!("{}", help); + eprintln!("{}", markdown(help)); exit(0) } diff --git a/rola-cli/src/output.rs b/rola-cli/src/output.rs new file mode 100644 index 0000000..65cafed --- /dev/null +++ b/rola-cli/src/output.rs @@ -0,0 +1,6 @@ +pub mod ansi_control; +pub mod display; +pub mod env_logger; + +mod setup; +pub use setup::*; diff --git a/rola-cli/src/output/ansi_control.rs b/rola-cli/src/output/ansi_control.rs new file mode 100644 index 0000000..5db8c2a --- /dev/null +++ b/rola-cli/src/output/ansi_control.rs @@ -0,0 +1,32 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +mod colorize_wrapper; +pub use colorize_wrapper::*; + +/// Global flag controlling whether ANSI color output is enabled. +static ANSI_ENABLED: AtomicBool = AtomicBool::new(true); + +/// Enable ANSI color codes in output. +pub fn enable_ansi() { + ANSI_ENABLED.store(true, Ordering::Relaxed); +} + +/// Disable ANSI color codes in output. +pub fn disable_ansi() { + ANSI_ENABLED.store(false, Ordering::Relaxed); +} + +/// Set ANSI color output to the specified state (`true` to enable, `false` to disable). +pub fn set_ansi(enabled: bool) { + ANSI_ENABLED.store(enabled, Ordering::Relaxed); +} + +/// Check whether ANSI color output is currently enabled. +pub fn is_ansi_enabled() -> bool { + ANSI_ENABLED.load(Ordering::Relaxed) +} + +/// Returns `true` if ANSI is enabled, `false` otherwise. +pub fn is_enabled() -> bool { + is_ansi_enabled() +} diff --git a/rola-cli/src/output/ansi_control/colorize_wrapper.rs b/rola-cli/src/output/ansi_control/colorize_wrapper.rs new file mode 100644 index 0000000..4c92f66 --- /dev/null +++ b/rola-cli/src/output/ansi_control/colorize_wrapper.rs @@ -0,0 +1,209 @@ +use colored::ColoredString; +use colored::Colorize as _ColoredColorize; + +use super::is_ansi_enabled; + +macro_rules! if_ansi { + ($self:expr, $method:ident $(, $arg:expr)*) => {{ + if is_ansi_enabled() { + _ColoredColorize::$method($self $(, $arg)*) + } else { + _ColoredColorize::clear($self) + } + }}; +} + +/// A drop-in wrapper for [`colored::Colorize`] that respects the global ANSI +/// output flag controlled by [`enable_ansi()`](super::enable_ansi) / +/// [`disable_ansi()`](super::disable_ansi). +/// +/// When ANSI is **disabled**, all colorization methods return a plain +/// [`ColoredString`] with no foreground/background color or style — +/// effectively a no-op. +/// +/// Import this trait instead of `colored::Colorize` to automatically honour +/// the `--no-color` / `--quiet` flags set by the CLI. +/// +/// # Example +/// +/// ```ignore +/// use rola_cli::res::ansi_control::Colorize; +/// +/// println!("{}", "error".red().bold()); +/// // ^ ANSI on → red bold text +/// // ^ ANSI off → plain "error" +/// ``` +pub trait Colorize: _ColoredColorize { + fn black(self) -> ColoredString { + if_ansi!(self, black) + } + fn red(self) -> ColoredString { + if_ansi!(self, red) + } + fn green(self) -> ColoredString { + if_ansi!(self, green) + } + fn yellow(self) -> ColoredString { + if_ansi!(self, yellow) + } + fn blue(self) -> ColoredString { + if_ansi!(self, blue) + } + fn magenta(self) -> ColoredString { + if_ansi!(self, magenta) + } + fn purple(self) -> ColoredString { + if_ansi!(self, purple) + } + fn cyan(self) -> ColoredString { + if_ansi!(self, cyan) + } + fn white(self) -> ColoredString { + if_ansi!(self, white) + } + + fn bright_black(self) -> ColoredString { + if_ansi!(self, bright_black) + } + fn bright_red(self) -> ColoredString { + if_ansi!(self, bright_red) + } + fn bright_green(self) -> ColoredString { + if_ansi!(self, bright_green) + } + fn bright_yellow(self) -> ColoredString { + if_ansi!(self, bright_yellow) + } + fn bright_blue(self) -> ColoredString { + if_ansi!(self, bright_blue) + } + fn bright_magenta(self) -> ColoredString { + if_ansi!(self, bright_magenta) + } + fn bright_purple(self) -> ColoredString { + if_ansi!(self, bright_purple) + } + fn bright_cyan(self) -> ColoredString { + if_ansi!(self, bright_cyan) + } + fn bright_white(self) -> ColoredString { + if_ansi!(self, bright_white) + } + + fn truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, truecolor, r, g, b) + } + + fn color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn on_black(self) -> ColoredString { + if_ansi!(self, on_black) + } + fn on_red(self) -> ColoredString { + if_ansi!(self, on_red) + } + fn on_green(self) -> ColoredString { + if_ansi!(self, on_green) + } + fn on_yellow(self) -> ColoredString { + if_ansi!(self, on_yellow) + } + fn on_blue(self) -> ColoredString { + if_ansi!(self, on_blue) + } + fn on_magenta(self) -> ColoredString { + if_ansi!(self, on_magenta) + } + fn on_purple(self) -> ColoredString { + if_ansi!(self, on_purple) + } + fn on_cyan(self) -> ColoredString { + if_ansi!(self, on_cyan) + } + fn on_white(self) -> ColoredString { + if_ansi!(self, on_white) + } + + fn on_bright_black(self) -> ColoredString { + if_ansi!(self, on_bright_black) + } + fn on_bright_red(self) -> ColoredString { + if_ansi!(self, on_bright_red) + } + fn on_bright_green(self) -> ColoredString { + if_ansi!(self, on_bright_green) + } + fn on_bright_yellow(self) -> ColoredString { + if_ansi!(self, on_bright_yellow) + } + fn on_bright_blue(self) -> ColoredString { + if_ansi!(self, on_bright_blue) + } + fn on_bright_magenta(self) -> ColoredString { + if_ansi!(self, on_bright_magenta) + } + fn on_bright_purple(self) -> ColoredString { + if_ansi!(self, on_bright_purple) + } + fn on_bright_cyan(self) -> ColoredString { + if_ansi!(self, on_bright_cyan) + } + fn on_bright_white(self) -> ColoredString { + if_ansi!(self, on_bright_white) + } + + fn on_truecolor(self, r: u8, g: u8, b: u8) -> ColoredString { + if_ansi!(self, on_truecolor, r, g, b) + } + + fn on_color<C: Into<colored::Color>>(self, color: C) -> ColoredString { + if is_ansi_enabled() { + _ColoredColorize::on_color(self, color) + } else { + _ColoredColorize::clear(self) + } + } + + fn clear(self) -> ColoredString { + if_ansi!(self, clear) + } + fn normal(self) -> ColoredString { + if_ansi!(self, normal) + } + fn bold(self) -> ColoredString { + if_ansi!(self, bold) + } + fn dimmed(self) -> ColoredString { + if_ansi!(self, dimmed) + } + fn italic(self) -> ColoredString { + if_ansi!(self, italic) + } + fn underline(self) -> ColoredString { + if_ansi!(self, underline) + } + fn blink(self) -> ColoredString { + if_ansi!(self, blink) + } + #[allow(deprecated)] + fn reverse(self) -> ColoredString { + if_ansi!(self, reverse) + } + fn reversed(self) -> ColoredString { + if_ansi!(self, reversed) + } + fn hidden(self) -> ColoredString { + if_ansi!(self, hidden) + } + fn strikethrough(self) -> ColoredString { + if_ansi!(self, strikethrough) + } +} + +impl<T: _ColoredColorize> Colorize for T {} diff --git a/rola-cli/src/output/display.rs b/rola-cli/src/output/display.rs new file mode 100644 index 0000000..0845456 --- /dev/null +++ b/rola-cli/src/output/display.rs @@ -0,0 +1,384 @@ +use crate::output::ansi_control::Colorize; +use std::collections::VecDeque; + +/// Trait for adding markdown formatting to strings +pub trait Markdown { + fn markdown(&self) -> String; +} + +impl Markdown for &str { + fn markdown(&self) -> String { + markdown(self) + } +} + +impl Markdown for String { + fn markdown(&self) -> String { + markdown(self) + } +} + +/// Converts a string to colored/formatted text with ANSI escape codes. +/// +/// Supported syntax: +/// - Bold: `**text**` +/// - Italic: `*text*` +/// - Underline: `_text_` +/// - Angle-bracketed content: `<text>` (displayed as cyan) +/// - Inline code: `` `text` `` (displayed as green) +/// - Color tags: `[[color_name]]` and `[[/]]` to close color +/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters +/// - Headings: `# Heading 1`, `## Heading 2`, up to `###### Heading 6` +/// - Blockquote: `> text` (displays a gray background marker at the beginning of the line) +/// +/// Color tags support the following color names: +/// Color tags support the following color names: +/// +/// | Type | Color Names | +/// |-------------------------|-----------------------------------------------------------------------------| +/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | +/// | Bright colors | `bright_black` | +/// | | `bright_red` | +/// | | `bright_green` | +/// | | `bright_yellow` | +/// | | `bright_blue` | +/// | | `bright_magenta` | +/// | | `bright_cyan` | +/// | | `bright_white` | +/// | Bright color shorthands | `b_black` | +/// | | `b_red` | +/// | | `b_green` | +/// | | `b_yellow` | +/// | | `b_blue` | +/// | | `b_magenta` | +/// | | `b_cyan` | +/// | | `b_white` | +/// | Gray colors | `gray`/`grey` | +/// | | `bright_gray`/`bright_grey` | +/// | | `b_gray`/`b_grey` | +/// +/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. +/// +/// # Arguments +/// * `text` - The text to format, can be any type that implements `AsRef<str>` +/// +/// # Returns +/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. +/// +/// # Examples +/// ``` +/// # use mingling_cli::display::markdown; +/// let formatted = markdown("Hello **world**!"); +/// println!("{}", formatted); +/// +/// let colored = markdown("[[red]]Red text[[/]] and normal text"); +/// println!("{}", colored); +/// +/// let nested = markdown("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); +/// println!("{}", nested); +/// ``` +pub fn markdown(text: impl AsRef<str>) -> String { + let text = text.as_ref(); + let lines: Vec<&str> = text.lines().collect(); + let mut result = String::new(); + let mut content_indent = 0; + + for line in lines { + // Don't trim the line initially, we need to check if it starts with # + let trimmed_line = line.trim(); + let mut line_result = String::new(); + + // Check if line starts with # for heading + // Check if the original line (not trimmed) starts with # + if line.trim_start().starts_with('#') { + let chars: Vec<char> = line.trim_start().chars().collect(); + let mut level = 0; + + // Count # characters at the beginning + while level < chars.len() && level < 7 && chars[level] == '#' { + level += 1; + } + + // Cap level at 6 + let effective_level = if level > 6 { 6 } else { level }; + + // Skip # characters and any whitespace after them + let mut content_start = level; + while content_start < chars.len() && chars[content_start].is_whitespace() { + content_start += 1; + } + + // Extract heading content + let heading_content: String = if content_start < chars.len() { + chars[content_start..].iter().collect() + } else { + String::new() + }; + + // Process the heading content with formatting + let processed_content = process_line(&heading_content); + + // Format heading as white background, black text, bold + // ANSI codes: \x1b[1m for bold, \x1b[47m for white background, \x1b[30m for black text + let formatted_heading = format!("\x1b[1m\x1b[47m\x1b[30m {processed_content} \x1b[0m"); + + // Add indentation to the heading line itself + // Heading indentation = level - 1 + let heading_indent = if effective_level > 0 { + effective_level - 1 + } else { + 0 + }; + let indent = " ".repeat(heading_indent); + line_result.push_str(&indent); + line_result.push_str(&formatted_heading); + + // Update content indent level for subsequent content + // Content after heading should be indented by effective_level + content_indent = effective_level; + } else if !trimmed_line.is_empty() { + // Process regular line with existing formatting + let processed_line = process_line_with_quote(trimmed_line); + + // Add indentation based on content_indent + let indent = " ".repeat(content_indent); + line_result.push_str(&indent); + line_result.push_str(&processed_line); + } else { + line_result.push(' '); + } + + if !line_result.is_empty() { + result.push_str(&line_result); + result.push('\n'); + } + } + + // Remove trailing newline + if result.ends_with('\n') { + result.pop(); + } + + result +} + +// Helper function to process a single line with existing formatting and handle > quotes +fn process_line_with_quote(line: &str) -> String { + let chars: Vec<char> = line.chars().collect(); + + // Check if line starts with '>' and not escaped + if !chars.is_empty() && chars[0] == '>' { + // Check if it's escaped + if chars.len() > 1 && chars[1] == '\\' { + // It's \>, so treat as normal text starting from position 0 + return process_line(line); + } + + // It's a regular > at the beginning, replace with gray background gray text space + let gray_bg_space = "\x1b[48;5;242m\x1b[38;5;242m \x1b[0m"; + let rest_of_line = if chars.len() > 1 { + chars[1..].iter().collect::<String>() + } else { + String::new() + }; + + // Process the rest of the line normally + let processed_rest = process_line(&rest_of_line); + + // Combine the gray background space with the processed rest + format!("{gray_bg_space}{processed_rest}") + } else { + // No > at the beginning, process normally + process_line(line) + } +} + +// Helper function to process a single line with existing formatting +fn process_line(line: &str) -> String { + let mut result = String::new(); + let mut color_stack: VecDeque<String> = VecDeque::new(); + + let chars: Vec<char> = line.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for escape character \ + if chars[i] == '\\' && i + 1 < chars.len() { + let escaped_char = chars[i + 1]; + // Only escape specific characters + if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') { + let mut escaped_text = escaped_char.to_string(); + apply_color_stack(&mut escaped_text, &color_stack); + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() + && chars[i] == '[' + && chars[i + 1] == '[' + && let Some(end) = find_tag_end(&chars, i) + { + let tag_content: String = chars[i + 2..end].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + } else { + // It's a color tag + color_stack.push_back(tag_content.clone()); + } + i = end + 2; + continue; + } + + // Check for bold **text** + if i + 1 < chars.len() + && chars[i] == '*' + && chars[i + 1] == '*' + && let Some(end) = find_matching(&chars, i + 2, "**") + { + let bold_text: String = chars[i + 2..end].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 2; + continue; + } + + // Check for italic *text* + if chars[i] == '*' + && let Some(end) = find_matching(&chars, i + 1, "*") + { + let italic_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for underline _text_ + if chars[i] == '_' + && let Some(end) = find_matching(&chars, i + 1, "_") + { + let underline_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = format!("\x1b[4m{underline_text}\x1b[0m"); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for angle-bracketed content <text> + if chars[i] == '<' + && let Some(end) = find_matching(&chars, i + 1, ">") + { + // Include the angle brackets in the output + let angle_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Check for inline code `text` + if chars[i] == '`' + && let Some(end) = find_matching(&chars, i + 1, "`") + { + // Include the backticks in the output + let code_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + + // Regular character + let mut current_char = chars[i].to_string(); + apply_color_stack(&mut current_char, &color_stack); + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to find matching delimiter +fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option<usize> { + let delim_chars: Vec<char> = delimiter.chars().collect(); + let delim_len = delim_chars.len(); + + let mut j = start; + while j < chars.len() { + if delim_len == 1 { + if chars[j] == delim_chars[0] { + return Some(j); + } + } else if j + 1 < chars.len() + && chars[j] == delim_chars[0] + && chars[j + 1] == delim_chars[1] + { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to find color tag end +fn find_tag_end(chars: &[char], start: usize) -> Option<usize> { + let mut j = start + 2; + while j + 1 < chars.len() { + if chars[j] == ']' && chars[j + 1] == ']' { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to apply color stack to text +fn apply_color_stack(text: &mut String, color_stack: &VecDeque<String>) { + let mut result = text.clone(); + for color in color_stack.iter().rev() { + result = apply_color(&result, color); + } + *text = result; +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef<str>, color_name: impl AsRef<str>) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" | "b_white" | "bright_gray" | "bright_grey" | "b_gray" | "b_grey" => { + text.white().to_string() + } + + // Bright colors and their b_ short aliases + "bright_black" | "b_black" | "gray" | "grey" => text.bright_black().to_string(), + "bright_red" | "b_red" => text.bright_red().to_string(), + "bright_green" | "b_green" => text.bright_green().to_string(), + "bright_yellow" | "b_yellow" => text.bright_yellow().to_string(), + "bright_blue" | "b_blue" => text.bright_blue().to_string(), + "bright_magenta" | "b_magenta" => text.bright_magenta().to_string(), + "bright_cyan" | "b_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} diff --git a/rola-cli/src/output/env_logger.rs b/rola-cli/src/output/env_logger.rs new file mode 100644 index 0000000..289de77 --- /dev/null +++ b/rola-cli/src/output/env_logger.rs @@ -0,0 +1,54 @@ +use chrono::Local; +use std::io::Write; + +use crate::output::ansi_control::Colorize; + +/// Simple env logger that prints formatted messages. +/// Usage: `env_logger::init_with(EnvLogger { show_time: true, show_level: true });` +pub struct EnvLogger { + pub show_time: bool, + pub show_level: bool, + pub level: log::Level, +} + +impl log::Log for EnvLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut buf = Vec::new(); + + if self.show_time { + let now = Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + write!(buf, "[{}] ", now).ok(); + } + + if self.show_level { + let level_str = match record.level() { + log::Level::Trace => "TRACE".bright_black(), + log::Level::Debug => "DEBUG".cyan(), + log::Level::Info => "INFO".green(), + log::Level::Warn => "WARN".yellow(), + log::Level::Error => "ERROR".red(), + }; + write!(buf, "{}: ", level_str.bold()).ok(); + } + + write!(buf, "{}", record.args()).ok(); + eprintln!("{}", String::from_utf8_lossy(&buf)); + } + + fn flush(&self) {} +} + +/// Initialize the env logger with the given configuration. +pub fn init_envlogger(config: EnvLogger) { + log::set_boxed_logger(Box::new(config)) + .map(|()| log::set_max_level(log::LevelFilter::Info)) + .ok(); +} diff --git a/rola-cli/src/output/setup.rs b/rola-cli/src/output/setup.rs new file mode 100644 index 0000000..824348b --- /dev/null +++ b/rola-cli/src/output/setup.rs @@ -0,0 +1,41 @@ +use mingling::{Program, macros::program_setup}; +use shared_functions::info; + +use crate::{ + ThisProgram, + output::{ + ansi_control::disable_ansi, + env_logger::{EnvLogger, init_envlogger}, + }, +}; + +#[program_setup] +pub fn color_output_setup(program: &mut Program<ThisProgram>) { + program.global_flag("--no-color", |_| { + disable_ansi(); + }); +} + +#[program_setup] +pub fn env_logger_setup(program: &mut Program<ThisProgram>) { + program.stdout_setting.verbose = program.pick_global_flag(["-V", "--verbose"]); + let log_show_time = program.pick_global_flag("--log-time"); + let log_level = program + .pick_global_argument("--log-level") + .unwrap_or("info".to_string()); + if program.stdout_setting.verbose { + init_envlogger(EnvLogger { + show_time: log_show_time, + show_level: log_level.as_str() != "disable", + level: match log_level.as_str() { + "trace" => log::Level::Trace, + "debug" => log::Level::Debug, + "warn" => log::Level::Warn, + "error" => log::Level::Error, + _ => log::Level::Info, + }, + }); + } + + info!("Verbose mode enabled!"); +} |
