1use 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#[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.into_iter().map(|v| Cell::new(v.to_string())).collect();
220 self.table.add_row(cells);
221 self
222 }
223
224 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 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 pub fn build(self) -> Table {
252 self.table
253 }
254
255 pub fn print(self) {
257 println!("{}", self.table);
258 }
259}
260
261#[derive(Debug, Clone, Copy)]
263pub enum StatusType {
264 Success,
265 Warning,
266 Error,
267 Info,
268 Neutral,
269}
270
271#[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#[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#[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}