ngdp_client/
output.rs

1//! Output formatting utilities for the CLI
2//!
3//! This module provides utilities for formatting output in various styles
4//! including tables, colored text, and structured displays.
5
6use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table, presets};
7use owo_colors::OwoColorize;
8
9/// Style configuration for output formatting
10pub struct OutputStyle {
11    /// Whether to use colors in output
12    pub use_color: bool,
13    /// Whether to use Unicode characters for borders
14    pub use_unicode: bool,
15}
16
17impl Default for OutputStyle {
18    fn default() -> Self {
19        Self {
20            // Check if NO_COLOR env var is set
21            use_color: std::env::var("NO_COLOR").is_err(),
22            use_unicode: true,
23        }
24    }
25}
26
27impl OutputStyle {
28    /// Create a new output style
29    #[must_use]
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Disable colors in output
35    #[must_use]
36    pub fn no_color(mut self) -> Self {
37        self.use_color = false;
38        self
39    }
40
41    /// Use ASCII characters instead of Unicode
42    #[must_use]
43    pub fn ascii(mut self) -> Self {
44        self.use_unicode = false;
45        self
46    }
47}
48
49/// Format a header with appropriate styling
50pub fn format_header(text: &str, style: &OutputStyle) -> String {
51    if style.use_color {
52        text.bold().bright_blue().to_string()
53    } else {
54        text.to_string()
55    }
56}
57
58/// Format a success message
59pub fn format_success(text: &str, style: &OutputStyle) -> String {
60    if style.use_color {
61        text.green().to_string()
62    } else {
63        text.to_string()
64    }
65}
66
67/// Format a warning message
68pub fn format_warning(text: &str, style: &OutputStyle) -> String {
69    if style.use_color {
70        text.yellow().to_string()
71    } else {
72        text.to_string()
73    }
74}
75
76/// Format an error message
77pub fn format_error(text: &str, style: &OutputStyle) -> String {
78    if style.use_color {
79        text.red().to_string()
80    } else {
81        text.to_string()
82    }
83}
84
85/// Format a key-value pair
86pub fn format_key_value(key: &str, value: &str, style: &OutputStyle) -> String {
87    if style.use_color {
88        format!("{}: {}", key.cyan(), value)
89    } else {
90        format!("{key}: {value}")
91    }
92}
93
94/// Create a styled table
95pub fn create_table(style: &OutputStyle) -> Table {
96    let mut table = Table::new();
97
98    // Set table style based on preferences
99    if style.use_unicode {
100        table
101            .load_preset(presets::UTF8_FULL)
102            .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS);
103    } else {
104        table.load_preset(presets::ASCII_FULL);
105    }
106
107    // Configure table layout
108    table
109        .set_content_arrangement(ContentArrangement::Dynamic)
110        .set_width(140);
111
112    table
113}
114
115/// Create a simple list table (no borders)
116pub fn create_list_table(style: &OutputStyle) -> Table {
117    let mut table = Table::new();
118
119    if style.use_unicode {
120        table.load_preset(presets::UTF8_HORIZONTAL_ONLY);
121    } else {
122        table.load_preset(presets::ASCII_HORIZONTAL_ONLY);
123    }
124
125    table
126        .set_content_arrangement(ContentArrangement::Dynamic)
127        .set_width(100);
128
129    table
130}
131
132/// Style a table header cell
133pub fn header_cell(text: &str, style: &OutputStyle) -> Cell {
134    let cell = Cell::new(text);
135    if style.use_color {
136        cell.fg(Color::Cyan)
137            .add_attribute(Attribute::Bold)
138            .set_alignment(CellAlignment::Left)
139    } else {
140        cell.add_attribute(Attribute::Bold)
141            .set_alignment(CellAlignment::Left)
142    }
143}
144
145/// Style a regular cell
146pub fn regular_cell(text: &str) -> Cell {
147    Cell::new(text).set_alignment(CellAlignment::Left)
148}
149
150/// Style a numeric cell (right-aligned)
151pub fn numeric_cell(text: &str) -> Cell {
152    Cell::new(text).set_alignment(CellAlignment::Right)
153}
154
155/// Style a status cell based on content
156pub fn status_cell(text: &str, style: &OutputStyle) -> Cell {
157    let cell = Cell::new(text);
158    if style.use_color {
159        match text {
160            "✓" | "OK" | "Success" | "Complete" => cell.fg(Color::Green),
161            "✗" | "Failed" | "Error" => cell.fg(Color::Red),
162            "⚠" | "Warning" => cell.fg(Color::Yellow),
163            "…" | "In Progress" => cell.fg(Color::Blue),
164            _ => cell,
165        }
166    } else {
167        cell
168    }
169}
170
171/// Style a hash cell (dimmed)
172pub fn hash_cell(text: &str, style: &OutputStyle) -> Cell {
173    let cell = Cell::new(text);
174    if style.use_color {
175        cell.fg(Color::Grey)
176    } else {
177        cell
178    }
179}
180
181/// Print a section header
182pub fn print_section_header(title: &str, style: &OutputStyle) {
183    if style.use_color {
184        println!("\n{}", title.bold().bright_blue());
185        println!("{}", "═".repeat(title.len()).bright_blue());
186    } else {
187        println!("\n{title}");
188        println!("{}", "=".repeat(title.len()));
189    }
190}
191
192/// Print a subsection header
193pub fn print_subsection_header(title: &str, style: &OutputStyle) {
194    if style.use_color {
195        println!("\n{}", title.cyan());
196        println!("{}", "─".repeat(title.len()).cyan());
197    } else {
198        println!("\n{title}");
199        println!("{}", "-".repeat(title.len()));
200    }
201}
202
203/// Format a count badge (e.g., "(42 items)")
204pub fn format_count_badge(count: usize, item_name: &str, style: &OutputStyle) -> String {
205    let text = if count == 1 {
206        format!("({count} {item_name})")
207    } else {
208        format!("({count} {item_name}s)")
209    };
210
211    if style.use_color {
212        text.dimmed().to_string()
213    } else {
214        text
215    }
216}
217
218/// Format a timestamp
219pub fn format_timestamp(timestamp: &str, style: &OutputStyle) -> String {
220    if style.use_color {
221        timestamp.dimmed().to_string()
222    } else {
223        timestamp.to_string()
224    }
225}
226
227/// Format a file path
228pub fn format_path(path: &str, style: &OutputStyle) -> String {
229    if style.use_color {
230        path.bright_magenta().to_string()
231    } else {
232        path.to_string()
233    }
234}
235
236/// Format a URL
237pub fn format_url(url: &str, style: &OutputStyle) -> String {
238    if style.use_color {
239        url.bright_blue().underline().to_string()
240    } else {
241        url.to_string()
242    }
243}
244
245/// Format a hash or ID
246pub fn format_hash(hash: &str, style: &OutputStyle) -> String {
247    if style.use_color {
248        hash.dimmed().italic().to_string()
249    } else {
250        hash.to_string()
251    }
252}