1use comfy_table::{presets, Cell, Color, ContentArrangement, Table};
6use console::style;
7use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
8use serde::Serialize;
9use std::fmt::Display;
10use std::io;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum OutputFormat {
16 #[default]
18 Table,
19 Json,
21 Yaml,
23 Compact,
25}
26
27impl OutputFormat {
28 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
40pub struct OutputWriter {
42 format: OutputFormat,
43 colors: bool,
44 multi_progress: MultiProgress,
45}
46
47impl OutputWriter {
48 pub fn new(format: OutputFormat, colors: bool) -> Self {
50 Self {
51 format,
52 colors,
53 multi_progress: MultiProgress::new(),
54 }
55 }
56
57 pub fn format(&self) -> OutputFormat {
59 self.format
60 }
61
62 pub fn colors_enabled(&self) -> bool {
64 self.colors
65 }
66
67 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 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 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 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 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 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 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 let output = serde_json::to_string_pretty(data)?;
141 println!("{}", output);
142 }
143 }
144 Ok(())
145 }
146
147 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 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 pub fn multi_progress(&self) -> &MultiProgress {
181 &self.multi_progress
182 }
183}
184
185pub struct TableBuilder {
187 table: Table,
188 colors: bool,
189}
190
191impl TableBuilder {
192 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 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 pub fn row(mut self, values: impl IntoIterator<Item = impl Display>) -> Self {
219 let cells: Vec<Cell> = values
220 .into_iter()
221 .map(|v| Cell::new(v.to_string()))
222 .collect();
223 self.table.add_row(cells);
224 self
225 }
226
227 pub fn status_row(
229 mut self,
230 values: impl IntoIterator<Item = impl Display>,
231 status: StatusType,
232 ) -> Self {
233 let values: Vec<String> = values.into_iter().map(|v| v.to_string()).collect();
234 let mut cells: Vec<Cell> = values.iter().map(|v| Cell::new(v)).collect();
235
236 if self.colors && !cells.is_empty() {
237 let color = match status {
238 StatusType::Success => Color::Green,
239 StatusType::Warning => Color::Yellow,
240 StatusType::Error => Color::Red,
241 StatusType::Info => Color::Blue,
242 StatusType::Neutral => Color::White,
243 };
244 if let Some(last) = cells.last_mut() {
246 *last = Cell::new(&values[values.len() - 1]).fg(color);
247 }
248 }
249 self.table.add_row(cells);
250 self
251 }
252
253 pub fn summary_row(mut self, message: &str, col_count: usize) -> Self {
255 let mut cells = vec![Cell::new(message)];
256 for _ in 1..col_count {
257 cells.push(Cell::new(""));
258 }
259 if self.colors {
260 cells[0] = Cell::new(message).fg(Color::DarkGrey);
261 }
262 self.table.add_row(cells);
263 self
264 }
265
266 pub fn build(self) -> Table {
268 self.table
269 }
270
271 pub fn print(self) {
273 println!("{}", self.table);
274 }
275}
276
277pub struct PaginatedTable {
287 max_visible: usize,
288 head_count: usize,
289 tail_count: usize,
290}
291
292impl Default for PaginatedTable {
293 fn default() -> Self {
294 Self {
295 max_visible: 20,
296 head_count: 10,
297 tail_count: 5,
298 }
299 }
300}
301
302impl PaginatedTable {
303 pub fn new(max_visible: usize, head_count: usize, tail_count: usize) -> Self {
305 Self {
306 max_visible,
307 head_count,
308 tail_count,
309 }
310 }
311
312 pub fn render<F>(
317 self,
318 mut builder: TableBuilder,
319 total: usize,
320 col_count: usize,
321 row_fn: F,
322 ) -> TableBuilder
323 where
324 F: Fn(usize) -> (Vec<String>, StatusType),
325 {
326 if total <= self.max_visible {
327 for i in 0..total {
328 let (cells, status) = row_fn(i);
329 builder = builder.status_row(cells, status);
330 }
331 } else {
332 for i in 0..self.head_count {
333 let (cells, status) = row_fn(i);
334 builder = builder.status_row(cells, status);
335 }
336 let omitted = total - self.head_count - self.tail_count;
337 builder = builder.summary_row(&format!("... {} more devices ...", omitted), col_count);
338 for i in (total - self.tail_count)..total {
339 let (cells, status) = row_fn(i);
340 builder = builder.status_row(cells, status);
341 }
342 }
343 builder
344 }
345}
346
347#[derive(Debug, Clone, Copy)]
349pub enum StatusType {
350 Success,
351 Warning,
352 Error,
353 Info,
354 Neutral,
355}
356
357#[derive(Debug, Clone, Serialize)]
359pub struct ProtocolStatus {
360 pub protocol: String,
361 pub devices: usize,
362 pub points: usize,
363 pub status: String,
364 pub uptime: String,
365}
366
367#[derive(Debug, Clone, Serialize)]
369pub struct DeviceSummary {
370 pub id: String,
371 pub name: String,
372 pub protocol: String,
373 pub status: String,
374 pub points: usize,
375 pub last_update: String,
376}
377
378#[derive(Debug, Clone, Serialize)]
380pub struct ValidationResult {
381 pub valid: bool,
382 pub errors: Vec<ValidationError>,
383 pub warnings: Vec<ValidationWarning>,
384}
385
386#[derive(Debug, Clone, Serialize)]
387pub struct ValidationError {
388 pub path: String,
389 pub message: String,
390}
391
392#[derive(Debug, Clone, Serialize)]
393pub struct ValidationWarning {
394 pub path: String,
395 pub message: String,
396}