diff options
Diffstat (limited to 'crates/mozart/src/console.rs')
| -rw-r--r-- | crates/mozart/src/console.rs | 259 |
1 files changed, 251 insertions, 8 deletions
diff --git a/crates/mozart/src/console.rs b/crates/mozart/src/console.rs index 863e9ba..5f108c0 100644 --- a/crates/mozart/src/console.rs +++ b/crates/mozart/src/console.rs @@ -1,5 +1,10 @@ use colored::{ColoredString, Colorize}; use dialoguer::{Confirm, Input}; +use std::io::IsTerminal; + +// --------------------------------------------------------------------------- +// Tag-style color helpers (module-level free functions, unchanged API) +// --------------------------------------------------------------------------- /// `<info>` — green foreground pub fn info(message: &str) -> ColoredString { @@ -31,29 +36,169 @@ pub fn warning(message: &str) -> ColoredString { message.black().on_yellow() } +// --------------------------------------------------------------------------- +// Verbosity +// --------------------------------------------------------------------------- + +/// Output verbosity level, ordered from least to most verbose. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Verbosity { + /// `-q` / `--quiet`: suppress all non-error output. + Quiet, + /// Default: normal informational messages. + Normal, + /// `-v`: additional detail (URLs, cache hits, skips). + Verbose, + /// `-vv`: HTTP details, file operations, resolver iterations. + VeryVerbose, + /// `-vvv`: full debug output (headers, raw payloads, timing). + Debug, +} + +impl Verbosity { + /// Construct a `Verbosity` from CLI flag counts. + /// + /// - `quiet == true` → `Quiet` (takes priority over `-v` flags) + /// - `verbose_count == 0` → `Normal` + /// - `verbose_count == 1` → `Verbose` + /// - `verbose_count == 2` → `VeryVerbose` + /// - `verbose_count >= 3` → `Debug` + pub fn from_flags(verbose_count: u8, quiet: bool) -> Self { + if quiet { + return Verbosity::Quiet; + } + match verbose_count { + 0 => Verbosity::Normal, + 1 => Verbosity::Verbose, + 2 => Verbosity::VeryVerbose, + _ => Verbosity::Debug, + } + } +} + +// --------------------------------------------------------------------------- +// Console +// --------------------------------------------------------------------------- + +/// Central IO hub for Mozart commands. +/// +/// Constructed once in `commands::execute()` and passed as `&Console` to every +/// command and library function that needs to produce output. pub struct Console { + /// Whether the user can answer interactive prompts. pub interactive: bool, - pub quiet: bool, + /// Current verbosity level. + pub verbosity: Verbosity, + /// Whether ANSI color codes should be emitted. + pub decorated: bool, } impl Console { - pub fn new(no_interaction: bool, quiet: bool) -> Self { + /// Build a `Console` from the parsed CLI. + /// + /// This is the primary constructor used in production. It reads + /// `cli.verbose`, `cli.quiet`, `cli.ansi`, `cli.no_ansi`, and + /// `cli.no_interaction` to configure all fields. + pub fn from_cli(cli: &crate::commands::Cli) -> Self { + let verbosity = Verbosity::from_flags(cli.verbose, cli.quiet); + let decorated = Self::resolve_decorated(cli.ansi, cli.no_ansi); + colored::control::set_override(decorated); Self { - interactive: !no_interaction, - quiet, + interactive: !cli.no_interaction, + verbosity, + decorated, } } - pub fn info(&self, msg: &str) { - if !self.quiet { + /// Determine whether ANSI color output should be enabled. + /// + /// - `no_ansi == true` → always disable + /// - `ansi == true` → always enable + /// - Otherwise → auto-detect: enabled only when stderr is a TTY + pub fn resolve_decorated(ansi: bool, no_ansi: bool) -> bool { + if no_ansi { + return false; + } + if ansi { + return true; + } + std::io::stderr().is_terminal() + } + + // ----------------------------------------------------------------------- + // Output methods + // ----------------------------------------------------------------------- + + /// Write `msg` to stderr if `self.verbosity >= required`. + pub fn write(&self, msg: &str, required: Verbosity) { + if self.verbosity >= required { eprintln!("{msg}"); } } - pub fn error(&self, msg: &str) { + /// Write `msg` to stdout if `self.verbosity >= required`. + pub fn write_stdout(&self, msg: &str, required: Verbosity) { + if self.verbosity >= required { + println!("{msg}"); + } + } + + /// Write an error to stderr. Always shown, even in quiet mode. + pub fn write_error(&self, msg: &str) { eprintln!("{}", error(msg)); } + // Convenience verbosity-level shortcuts: + + /// Normal-level message (suppressed by `--quiet`). + pub fn info(&self, msg: &str) { + self.write(msg, Verbosity::Normal); + } + + /// Verbose-level message (shown with `-v` or higher). + pub fn verbose(&self, msg: &str) { + self.write(msg, Verbosity::Verbose); + } + + /// Very-verbose-level message (shown with `-vv` or higher). + pub fn very_verbose(&self, msg: &str) { + self.write(msg, Verbosity::VeryVerbose); + } + + /// Debug-level message (shown with `-vvv`). + pub fn debug(&self, msg: &str) { + self.write(msg, Verbosity::Debug); + } + + /// Error message — always shown. + pub fn error(&self, msg: &str) { + self.write_error(msg); + } + + // ----------------------------------------------------------------------- + // Query methods + // ----------------------------------------------------------------------- + + pub fn is_verbose(&self) -> bool { + self.verbosity >= Verbosity::Verbose + } + + pub fn is_very_verbose(&self) -> bool { + self.verbosity >= Verbosity::VeryVerbose + } + + pub fn is_debug(&self) -> bool { + self.verbosity >= Verbosity::Debug + } + + pub fn is_quiet(&self) -> bool { + self.verbosity == Verbosity::Quiet + } + + // ----------------------------------------------------------------------- + // Interactive prompt methods (unchanged from prior implementation) + // ----------------------------------------------------------------------- + pub fn ask(&self, prompt: &str, default: &str) -> String { if !self.interactive { return default.to_string(); @@ -92,7 +237,7 @@ impl Console { match validator(&input) { Ok(()) => return Ok(input), Err(e) => { - self.error(&e); + self.write_error(&e); } } } @@ -110,3 +255,101 @@ impl Console { .unwrap_or(true) } } + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // ── Verbosity::from_flags ─────────────────────────────────────────────── + + #[test] + fn test_verbosity_quiet_takes_priority() { + assert_eq!(Verbosity::from_flags(3, true), Verbosity::Quiet); + assert_eq!(Verbosity::from_flags(0, true), Verbosity::Quiet); + } + + #[test] + fn test_verbosity_normal() { + assert_eq!(Verbosity::from_flags(0, false), Verbosity::Normal); + } + + #[test] + fn test_verbosity_verbose() { + assert_eq!(Verbosity::from_flags(1, false), Verbosity::Verbose); + } + + #[test] + fn test_verbosity_very_verbose() { + assert_eq!(Verbosity::from_flags(2, false), Verbosity::VeryVerbose); + } + + #[test] + fn test_verbosity_debug() { + assert_eq!(Verbosity::from_flags(3, false), Verbosity::Debug); + assert_eq!(Verbosity::from_flags(10, false), Verbosity::Debug); + } + + // ── Verbosity ordering ────────────────────────────────────────────────── + + #[test] + fn test_verbosity_ordering() { + assert!(Verbosity::Quiet < Verbosity::Normal); + assert!(Verbosity::Normal < Verbosity::Verbose); + assert!(Verbosity::Verbose < Verbosity::VeryVerbose); + assert!(Verbosity::VeryVerbose < Verbosity::Debug); + } + + // ── Console::resolve_decorated ────────────────────────────────────────── + + #[test] + fn test_resolve_decorated_no_ansi_wins() { + assert!(!Console::resolve_decorated(true, true)); + assert!(!Console::resolve_decorated(false, true)); + } + + #[test] + fn test_resolve_decorated_ansi_forces_on() { + assert!(Console::resolve_decorated(true, false)); + } + + // ── Console query methods ─────────────────────────────────────────────── + + fn make_console(verbosity: Verbosity) -> Console { + Console { + interactive: false, + verbosity, + decorated: false, + } + } + + #[test] + fn test_is_quiet() { + assert!(make_console(Verbosity::Quiet).is_quiet()); + assert!(!make_console(Verbosity::Normal).is_quiet()); + } + + #[test] + fn test_is_verbose() { + assert!(!make_console(Verbosity::Normal).is_verbose()); + assert!(make_console(Verbosity::Verbose).is_verbose()); + assert!(make_console(Verbosity::VeryVerbose).is_verbose()); + assert!(make_console(Verbosity::Debug).is_verbose()); + } + + #[test] + fn test_is_very_verbose() { + assert!(!make_console(Verbosity::Verbose).is_very_verbose()); + assert!(make_console(Verbosity::VeryVerbose).is_very_verbose()); + assert!(make_console(Verbosity::Debug).is_very_verbose()); + } + + #[test] + fn test_is_debug() { + assert!(!make_console(Verbosity::VeryVerbose).is_debug()); + assert!(make_console(Verbosity::Debug).is_debug()); + } +} |
