Skip to main content

mabi_cli/
output.rs

1//! Output formatting and display.
2//!
3//! Provides flexible output formatting for CLI commands.
4
5use comfy_table::{presets, Cell, Color, ContentArrangement, Table};
6use console::{style, Style};
7use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
8use serde::Serialize;
9use std::fmt::Display;
10use std::io::{self, Write};
11use std::time::Duration;
12
13/// Output format options.
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum OutputFormat {
16    /// Human-readable table format.
17    #[default]
18    Table,
19    /// JSON format.
20    Json,
21    /// YAML format.
22    Yaml,
23    /// Compact single-line format.
24    Compact,
25}
26
27impl OutputFormat {
28    /// Parse from string.
29    pub fn from_str(s: &str) -> Option<Self> {
30        match s.to_lowercase().as_str() {
31            "table" => Some(Self::Table),
32            "json" => Some(Self::Json),
33            "yaml" => Some(Self::Yaml),
34            "compact" => Some(Self::Compact),
35            _ => None,
36        }
37    }
38}
39
40/// Output writer with configurable format.
41pub struct OutputWriter {
42    format: OutputFormat,
43    colors: bool,
44    multi_progress: MultiProgress,
45}
46
47impl OutputWriter {
48    /// Create a new output writer.
49    pub fn new(format: OutputFormat, colors: bool) -> Self {
50        Self {
51            format,
52            colors,
53            multi_progress: MultiProgress::new(),
54        }
55    }
56
57    /// Get the output format.
58    pub fn format(&self) -> OutputFormat {
59        self.format
60    }
61
62    /// Check if colors are enabled.
63    pub fn colors_enabled(&self) -> bool {
64        self.colors
65    }
66
67    /// Write a success message.
68    pub fn success(&self, msg: impl Display) {
69        if self.colors {
70            println!("{} {}", style("✓").green().bold(), msg);
71        } else {
72            println!("[OK] {}", msg);
73        }
74    }
75
76    /// Write an error message.
77    pub fn error(&self, msg: impl Display) {
78        if self.colors {
79            eprintln!("{} {}", style("✗").red().bold(), msg);
80        } else {
81            eprintln!("[ERROR] {}", msg);
82        }
83    }
84
85    /// Write a warning message.
86    pub fn warning(&self, msg: impl Display) {
87        if self.colors {
88            println!("{} {}", style("⚠").yellow().bold(), msg);
89        } else {
90            println!("[WARN] {}", msg);
91        }
92    }
93
94    /// Write an info message.
95    pub fn info(&self, msg: impl Display) {
96        if self.colors {
97            println!("{} {}", style("ℹ").blue().bold(), msg);
98        } else {
99            println!("[INFO] {}", msg);
100        }
101    }
102
103    /// Write a header.
104    pub fn header(&self, msg: impl Display) {
105        if self.colors {
106            println!("\n{}", style(msg.to_string()).cyan().bold());
107            println!("{}", style("─".repeat(40)).dim());
108        } else {
109            println!("\n=== {} ===", msg);
110        }
111    }
112
113    /// Write a key-value pair.
114    pub fn kv(&self, key: impl Display, value: impl Display) {
115        if self.colors {
116            println!("  {}: {}", style(key.to_string()).dim(), value);
117        } else {
118            println!("  {}: {}", key, value);
119        }
120    }
121
122    /// Write data in the configured format.
123    pub fn write<T: Serialize>(&self, data: &T) -> io::Result<()> {
124        match self.format {
125            OutputFormat::Json => {
126                let output = serde_json::to_string_pretty(data)?;
127                println!("{}", output);
128            }
129            OutputFormat::Yaml => {
130                let output = serde_yaml::to_string(data)
131                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
132                println!("{}", output);
133            }
134            OutputFormat::Compact => {
135                let output = serde_json::to_string(data)?;
136                println!("{}", output);
137            }
138            OutputFormat::Table => {
139                // For Table format, caller should use write_table
140                let output = serde_json::to_string_pretty(data)?;
141                println!("{}", output);
142            }
143        }
144        Ok(())
145    }
146
147    /// Create a new progress bar.
148    pub fn progress(&self, total: u64, msg: impl Into<String>) -> ProgressBar {
149        let pb = self.multi_progress.add(ProgressBar::new(total));
150        pb.set_style(
151            ProgressStyle::with_template(if self.colors {
152                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"
153            } else {
154                "[{elapsed_precise}] [{bar:40}] {pos}/{len} {msg}"
155            })
156            .unwrap()
157            .progress_chars("█▓░"),
158        );
159        pb.set_message(msg.into());
160        pb
161    }
162
163    /// Create a spinner.
164    pub fn spinner(&self, msg: impl Into<String>) -> ProgressBar {
165        let pb = self.multi_progress.add(ProgressBar::new_spinner());
166        pb.set_style(
167            ProgressStyle::with_template(if self.colors {
168                "{spinner:.green} {msg}"
169            } else {
170                "[*] {msg}"
171            })
172            .unwrap(),
173        );
174        pb.set_message(msg.into());
175        pb.enable_steady_tick(Duration::from_millis(100));
176        pb
177    }
178
179    /// Get the multi-progress instance.
180    pub fn multi_progress(&self) -> &MultiProgress {
181        &self.multi_progress
182    }
183}
184
185/// Table builder for structured output.
186pub struct TableBuilder {
187    table: Table,
188    colors: bool,
189}
190
191impl TableBuilder {
192    /// Create a new table builder.
193    pub fn new(colors: bool) -> Self {
194        let mut table = Table::new();
195        table.load_preset(presets::UTF8_FULL_CONDENSED);
196        table.set_content_arrangement(ContentArrangement::Dynamic);
197
198        Self { table, colors }
199    }
200
201    /// Set the table header.
202    pub fn header(mut self, columns: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
203        let cells: Vec<Cell> = columns
204            .into_iter()
205            .map(|c| {
206                if self.colors {
207                    Cell::new(c.as_ref()).fg(Color::Cyan)
208                } else {
209                    Cell::new(c.as_ref())
210                }
211            })
212            .collect();
213        self.table.set_header(cells);
214        self
215    }
216
217    /// Add a row to the table.
218    pub fn row(mut self, values: impl IntoIterator<Item = impl Display>) -> Self {
219        let cells: Vec<Cell> = values.into_iter().map(|v| Cell::new(v.to_string())).collect();
220        self.table.add_row(cells);
221        self
222    }
223
224    /// Add a row with colored status.
225    pub fn status_row(
226        mut self,
227        values: impl IntoIterator<Item = impl Display>,
228        status: StatusType,
229    ) -> Self {
230        let values: Vec<String> = values.into_iter().map(|v| v.to_string()).collect();
231        let mut cells: Vec<Cell> = values.iter().map(|v| Cell::new(v)).collect();
232
233        if self.colors && !cells.is_empty() {
234            let color = match status {
235                StatusType::Success => Color::Green,
236                StatusType::Warning => Color::Yellow,
237                StatusType::Error => Color::Red,
238                StatusType::Info => Color::Blue,
239                StatusType::Neutral => Color::White,
240            };
241            // Color the last cell (usually the status)
242            if let Some(last) = cells.last_mut() {
243                *last = Cell::new(&values[values.len() - 1]).fg(color);
244            }
245        }
246        self.table.add_row(cells);
247        self
248    }
249
250    /// Build and return the table.
251    pub fn build(self) -> Table {
252        self.table
253    }
254
255    /// Print the table.
256    pub fn print(self) {
257        println!("{}", self.table);
258    }
259}
260
261/// Status type for colored output.
262#[derive(Debug, Clone, Copy)]
263pub enum StatusType {
264    Success,
265    Warning,
266    Error,
267    Info,
268    Neutral,
269}
270
271/// Protocol status display.
272#[derive(Debug, Clone, Serialize)]
273pub struct ProtocolStatus {
274    pub protocol: String,
275    pub devices: usize,
276    pub points: usize,
277    pub status: String,
278    pub uptime: String,
279}
280
281/// Device summary for list output.
282#[derive(Debug, Clone, Serialize)]
283pub struct DeviceSummary {
284    pub id: String,
285    pub name: String,
286    pub protocol: String,
287    pub status: String,
288    pub points: usize,
289    pub last_update: String,
290}
291
292/// Validation result for output.
293#[derive(Debug, Clone, Serialize)]
294pub struct ValidationResult {
295    pub valid: bool,
296    pub errors: Vec<ValidationError>,
297    pub warnings: Vec<ValidationWarning>,
298}
299
300#[derive(Debug, Clone, Serialize)]
301pub struct ValidationError {
302    pub path: String,
303    pub message: String,
304}
305
306#[derive(Debug, Clone, Serialize)]
307pub struct ValidationWarning {
308    pub path: String,
309    pub message: String,
310}