use comfy_table::{presets, Cell, Color, ContentArrangement, Table};
use console::style;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use serde::Serialize;
use std::fmt::Display;
use std::io;
use std::time::Duration;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OutputFormat {
#[default]
Table,
Json,
Yaml,
Compact,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"table" => Some(Self::Table),
"json" => Some(Self::Json),
"yaml" => Some(Self::Yaml),
"compact" => Some(Self::Compact),
_ => None,
}
}
}
pub struct OutputWriter {
format: OutputFormat,
colors: bool,
multi_progress: MultiProgress,
}
impl OutputWriter {
pub fn new(format: OutputFormat, colors: bool) -> Self {
Self {
format,
colors,
multi_progress: MultiProgress::new(),
}
}
pub fn format(&self) -> OutputFormat {
self.format
}
pub fn colors_enabled(&self) -> bool {
self.colors
}
pub fn success(&self, msg: impl Display) {
if self.colors {
println!("{} {}", style("✓").green().bold(), msg);
} else {
println!("[OK] {}", msg);
}
}
pub fn error(&self, msg: impl Display) {
if self.colors {
eprintln!("{} {}", style("✗").red().bold(), msg);
} else {
eprintln!("[ERROR] {}", msg);
}
}
pub fn warning(&self, msg: impl Display) {
if self.colors {
println!("{} {}", style("⚠").yellow().bold(), msg);
} else {
println!("[WARN] {}", msg);
}
}
pub fn info(&self, msg: impl Display) {
if self.colors {
println!("{} {}", style("ℹ").blue().bold(), msg);
} else {
println!("[INFO] {}", msg);
}
}
pub fn header(&self, msg: impl Display) {
if self.colors {
println!("\n{}", style(msg.to_string()).cyan().bold());
println!("{}", style("─".repeat(40)).dim());
} else {
println!("\n=== {} ===", msg);
}
}
pub fn kv(&self, key: impl Display, value: impl Display) {
if self.colors {
println!(" {}: {}", style(key.to_string()).dim(), value);
} else {
println!(" {}: {}", key, value);
}
}
pub fn write<T: Serialize>(&self, data: &T) -> io::Result<()> {
match self.format {
OutputFormat::Json => {
let output = serde_json::to_string_pretty(data)?;
println!("{}", output);
}
OutputFormat::Yaml => {
let output = serde_yaml::to_string(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
println!("{}", output);
}
OutputFormat::Compact => {
let output = serde_json::to_string(data)?;
println!("{}", output);
}
OutputFormat::Table => {
let output = serde_json::to_string_pretty(data)?;
println!("{}", output);
}
}
Ok(())
}
pub fn progress(&self, total: u64, msg: impl Into<String>) -> ProgressBar {
let pb = self.multi_progress.add(ProgressBar::new(total));
pb.set_style(
ProgressStyle::with_template(if self.colors {
"{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"
} else {
"[{elapsed_precise}] [{bar:40}] {pos}/{len} {msg}"
})
.unwrap()
.progress_chars("█▓░"),
);
pb.set_message(msg.into());
pb
}
pub fn spinner(&self, msg: impl Into<String>) -> ProgressBar {
let pb = self.multi_progress.add(ProgressBar::new_spinner());
pb.set_style(
ProgressStyle::with_template(if self.colors {
"{spinner:.green} {msg}"
} else {
"[*] {msg}"
})
.unwrap(),
);
pb.set_message(msg.into());
pb.enable_steady_tick(Duration::from_millis(100));
pb
}
pub fn multi_progress(&self) -> &MultiProgress {
&self.multi_progress
}
}
pub struct TableBuilder {
table: Table,
colors: bool,
}
impl TableBuilder {
pub fn new(colors: bool) -> Self {
let mut table = Table::new();
table.load_preset(presets::UTF8_FULL_CONDENSED);
table.set_content_arrangement(ContentArrangement::Dynamic);
Self { table, colors }
}
pub fn header(mut self, columns: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let cells: Vec<Cell> = columns
.into_iter()
.map(|c| {
if self.colors {
Cell::new(c.as_ref()).fg(Color::Cyan)
} else {
Cell::new(c.as_ref())
}
})
.collect();
self.table.set_header(cells);
self
}
pub fn row(mut self, values: impl IntoIterator<Item = impl Display>) -> Self {
let cells: Vec<Cell> = values
.into_iter()
.map(|v| Cell::new(v.to_string()))
.collect();
self.table.add_row(cells);
self
}
pub fn status_row(
mut self,
values: impl IntoIterator<Item = impl Display>,
status: StatusType,
) -> Self {
let values: Vec<String> = values.into_iter().map(|v| v.to_string()).collect();
let mut cells: Vec<Cell> = values.iter().map(|v| Cell::new(v)).collect();
if self.colors && !cells.is_empty() {
let color = match status {
StatusType::Success => Color::Green,
StatusType::Warning => Color::Yellow,
StatusType::Error => Color::Red,
StatusType::Info => Color::Blue,
StatusType::Neutral => Color::White,
};
if let Some(last) = cells.last_mut() {
*last = Cell::new(&values[values.len() - 1]).fg(color);
}
}
self.table.add_row(cells);
self
}
pub fn summary_row(mut self, message: &str, col_count: usize) -> Self {
let mut cells = vec![Cell::new(message)];
for _ in 1..col_count {
cells.push(Cell::new(""));
}
if self.colors {
cells[0] = Cell::new(message).fg(Color::DarkGrey);
}
self.table.add_row(cells);
self
}
pub fn build(self) -> Table {
self.table
}
pub fn print(self) {
println!("{}", self.table);
}
}
pub struct PaginatedTable {
max_visible: usize,
head_count: usize,
tail_count: usize,
}
impl Default for PaginatedTable {
fn default() -> Self {
Self {
max_visible: 20,
head_count: 10,
tail_count: 5,
}
}
}
impl PaginatedTable {
pub fn new(max_visible: usize, head_count: usize, tail_count: usize) -> Self {
Self {
max_visible,
head_count,
tail_count,
}
}
pub fn render<F>(
self,
mut builder: TableBuilder,
total: usize,
col_count: usize,
row_fn: F,
) -> TableBuilder
where
F: Fn(usize) -> (Vec<String>, StatusType),
{
if total <= self.max_visible {
for i in 0..total {
let (cells, status) = row_fn(i);
builder = builder.status_row(cells, status);
}
} else {
for i in 0..self.head_count {
let (cells, status) = row_fn(i);
builder = builder.status_row(cells, status);
}
let omitted = total - self.head_count - self.tail_count;
builder = builder.summary_row(&format!("... {} more devices ...", omitted), col_count);
for i in (total - self.tail_count)..total {
let (cells, status) = row_fn(i);
builder = builder.status_row(cells, status);
}
}
builder
}
}
#[derive(Debug, Clone, Copy)]
pub enum StatusType {
Success,
Warning,
Error,
Info,
Neutral,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProtocolStatus {
pub protocol: String,
pub devices: usize,
pub points: usize,
pub status: String,
pub uptime: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DeviceSummary {
pub id: String,
pub name: String,
pub protocol: String,
pub status: String,
pub points: usize,
pub last_update: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationWarning {
pub path: String,
pub message: String,
}