Skip to main content

normalize_output/
lib.rs

1//! Output formatting utilities.
2//!
3//! Provides text formatting via the `OutputFormatter` trait.
4//! JSON/jq/jsonl/schema output is handled by server-less at the CLI macro level.
5
6pub mod diagnostics;
7
8use serde::{Deserialize, Serialize};
9use std::io::IsTerminal;
10use std::str::FromStr;
11
12/// Color output mode.
13#[derive(
14    Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize, schemars::JsonSchema,
15)]
16#[serde(rename_all = "lowercase")]
17pub enum ColorMode {
18    /// Auto-detect based on TTY (default)
19    #[default]
20    Auto,
21    /// Always use colors
22    Always,
23    /// Never use colors
24    Never,
25}
26
27impl FromStr for ColorMode {
28    type Err = String;
29    fn from_str(s: &str) -> Result<Self, Self::Err> {
30        match s {
31            "auto" => Ok(Self::Auto),
32            "always" => Ok(Self::Always),
33            "never" => Ok(Self::Never),
34            _ => Err(format!(
35                "unknown color mode `{s}`; expected auto, always, or never"
36            )),
37        }
38    }
39}
40
41/// Configuration for pretty output mode.
42///
43/// Example config.toml:
44/// ```toml
45/// [pretty]
46/// enabled = true       # auto-enable when TTY (default: auto)
47/// colors = "auto"      # "auto", "always", or "never"
48/// highlight = true     # syntax highlighting on signatures
49/// ```
50#[derive(Debug, Clone, Deserialize, Serialize, Default, schemars::JsonSchema)]
51#[serde(default)]
52pub struct PrettyConfig {
53    /// Enable pretty mode. None = auto (true when stdout is TTY)
54    pub enabled: Option<bool>,
55    /// Color mode: auto (default), always, or never
56    pub colors: Option<ColorMode>,
57    /// Enable syntax highlighting. Default: true
58    pub highlight: Option<bool>,
59}
60
61impl PrettyConfig {
62    /// Should pretty mode be enabled?
63    /// Respects explicit setting, otherwise auto-detects TTY.
64    pub fn enabled(&self) -> bool {
65        self.enabled
66            .unwrap_or_else(|| std::io::stdout().is_terminal())
67    }
68
69    /// Should colors be used?
70    /// Respects colors setting and NO_COLOR env var.
71    pub fn use_colors(&self) -> bool {
72        // Check NO_COLOR env var first (standard)
73        if std::env::var("NO_COLOR").is_ok() {
74            return false;
75        }
76
77        match self.colors.unwrap_or_default() {
78            ColorMode::Always => true,
79            ColorMode::Never => false,
80            ColorMode::Auto => std::io::stdout().is_terminal(),
81        }
82    }
83
84    /// Should syntax highlighting be used?
85    pub fn highlight(&self) -> bool {
86        self.highlight.unwrap_or(true)
87    }
88}
89
90/// Trait for types that can format output in multiple formats.
91///
92/// Types implementing this trait provide text formatting. JSON/jq/jsonl
93/// output is handled automatically by server-less via `Serialize`.
94pub trait OutputFormatter: Serialize + schemars::JsonSchema {
95    /// Format as minimal text (LLM-optimized, default).
96    fn format_text(&self) -> String;
97
98    /// Format as pretty text (human-friendly with colors).
99    /// Default implementation falls back to format_text().
100    fn format_pretty(&self) -> String {
101        self.format_text()
102    }
103}
104
105/// Render a plain (uncolored) progress bar using block characters.
106///
107/// `ratio` is clamped to 0.0–1.0. `width` is the total character count.
108/// Callers can wrap the result in ANSI color as needed.
109pub fn progress_bar(ratio: f64, width: usize) -> String {
110    let ratio = ratio.clamp(0.0, 1.0);
111    let filled = ((ratio * width as f64).round() as usize).min(width);
112    format!("{}{}", "█".repeat(filled), "░".repeat(width - filled))
113}
114
115/// Render a colored progress bar where high ratio = good (green) and low = bad (red).
116pub fn progress_bar_good(ratio: f64, width: usize) -> String {
117    use nu_ansi_term::Color;
118    let color = if ratio >= 0.67 {
119        Color::Green
120    } else if ratio >= 0.34 {
121        Color::Yellow
122    } else {
123        Color::Red
124    };
125    color.paint(progress_bar(ratio, width)).to_string()
126}
127
128/// Render a colored progress bar where high ratio = bad (red) and low = good (green).
129pub fn progress_bar_bad(ratio: f64, width: usize) -> String {
130    progress_bar_good(1.0 - ratio, width)
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[derive(Serialize, schemars::JsonSchema)]
138    #[allow(dead_code)]
139    struct TestOutput {
140        name: String,
141        count: usize,
142    }
143
144    impl OutputFormatter for TestOutput {
145        fn format_text(&self) -> String {
146            format!("{}: {}", self.name, self.count)
147        }
148    }
149
150    #[test]
151    fn test_pretty_config_use_colors() {
152        // Always mode
153        let config = PrettyConfig {
154            colors: Some(ColorMode::Always),
155            ..Default::default()
156        };
157        assert!(config.use_colors());
158
159        // Never mode
160        let config = PrettyConfig {
161            colors: Some(ColorMode::Never),
162            ..Default::default()
163        };
164        assert!(!config.use_colors());
165    }
166
167    #[test]
168    fn test_pretty_config_highlight() {
169        // Default is true
170        let config = PrettyConfig::default();
171        assert!(config.highlight());
172
173        // Explicit false
174        let config = PrettyConfig {
175            highlight: Some(false),
176            ..Default::default()
177        };
178        assert!(!config.highlight());
179    }
180}