diff options
Diffstat (limited to 'crates/mozart-core/src/console.rs')
| -rw-r--r-- | crates/mozart-core/src/console.rs | 358 |
1 files changed, 358 insertions, 0 deletions
diff --git a/crates/mozart-core/src/console.rs b/crates/mozart-core/src/console.rs new file mode 100644 index 0000000..e37ff23 --- /dev/null +++ b/crates/mozart-core/src/console.rs @@ -0,0 +1,358 @@ +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 { + message.green() +} + +/// `<comment>` — yellow foreground +pub fn comment(message: &str) -> ColoredString { + message.yellow() +} + +/// `<error>` — white on red +pub fn error(message: &str) -> ColoredString { + message.white().on_red() +} + +/// `<question>` — black on cyan +pub fn question(message: &str) -> ColoredString { + message.black().on_cyan() +} + +/// `<highlight>` — red foreground (Composer extension) +pub fn highlight(message: &str) -> ColoredString { + message.red() +} + +/// `<warning>` — black on yellow (Composer extension) +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, + /// Current verbosity level. + pub verbosity: Verbosity, + /// Whether ANSI color codes should be emitted. + pub decorated: bool, +} + +impl Console { + /// Build a `Console` from primitive arguments. + /// + /// This is the primary constructor. Pass the relevant CLI flag values: + /// - `verbose`: the `-v` flag count (0, 1, 2, 3+) + /// - `quiet`: whether `--quiet` was passed + /// - `ansi`: whether `--ansi` was passed + /// - `no_ansi`: whether `--no-ansi` was passed + /// - `no_interaction`: whether `--no-interaction` / `-n` was passed + pub fn new(verbose: u8, quiet: bool, ansi: bool, no_ansi: bool, no_interaction: bool) -> Self { + let verbosity = Verbosity::from_flags(verbose, quiet); + let decorated = Self::resolve_decorated(ansi, no_ansi); + colored::control::set_override(decorated); + Self { + interactive: !no_interaction, + verbosity, + decorated, + } + } + + /// 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}"); + } + } + + /// 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(); + } + + Input::new() + .with_prompt(prompt) + .default(default.to_string()) + .allow_empty(true) + .interact_text() + .unwrap_or_else(|_| default.to_string()) + } + + pub fn ask_validated<F>( + &self, + prompt: &str, + default: &str, + validator: F, + ) -> Result<String, String> + where + F: Fn(&str) -> Result<(), String>, + { + if !self.interactive { + validator(default)?; + return Ok(default.to_string()); + } + + loop { + let input: String = Input::new() + .with_prompt(prompt) + .default(default.to_string()) + .allow_empty(true) + .interact_text() + .unwrap_or_else(|_| default.to_string()); + + match validator(&input) { + Ok(()) => return Ok(input), + Err(e) => { + self.write_error(&e); + } + } + } + } + + pub fn confirm(&self, prompt: &str) -> bool { + if !self.interactive { + return true; + } + + Confirm::new() + .with_prompt(prompt) + .default(true) + .interact() + .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()); + } +} |
