use std::io::{self, IsTerminal, Write};
use opaline::adapters::owo_colors::OwoThemeExt;
use owo_colors::OwoColorize;
use tabled::{Table, Tabled, settings::Style};
use crate::cli::args::{ColorMode, OutputFormat};
pub fn should_color(mode: &ColorMode) -> bool {
match mode {
ColorMode::Always => true,
ColorMode::Never => false,
ColorMode::Auto => io::stdout().is_terminal() && std::env::var("NO_COLOR").is_err(),
}
}
pub fn load_theme() -> opaline::Theme {
opaline::load_by_name("silkcircuit-neon").expect("builtin theme must exist")
}
pub fn themed(theme: &opaline::Theme, text: &str, token: &str) -> String {
format!("{}", text.style(theme.owo_fg(token)))
}
pub struct Painter {
theme: opaline::Theme,
enabled: bool,
}
impl Painter {
pub fn new(global: &super::args::GlobalOpts) -> Self {
let enabled = should_color(&global.color)
&& matches!(global.output, super::args::OutputFormat::Table);
Self {
theme: load_theme(),
enabled,
}
}
pub fn plain() -> Self {
Self {
theme: load_theme(),
enabled: false,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
fn paint(&self, text: &str, token: &str) -> String {
if self.enabled {
themed(&self.theme, text, token)
} else {
text.to_string()
}
}
pub fn name(&self, text: &str) -> String {
self.paint(text, "accent.secondary")
}
pub fn ip(&self, text: &str) -> String {
self.paint(text, "code.number")
}
pub fn mac(&self, text: &str) -> String {
self.paint(text, "text.dim")
}
pub fn muted(&self, text: &str) -> String {
self.paint(text, "text.muted")
}
pub fn id(&self, text: &str) -> String {
self.paint(text, "text.dim")
}
pub fn number(&self, text: &str) -> String {
self.paint(text, "code.number")
}
pub fn success(&self, text: &str) -> String {
self.paint(text, "success")
}
pub fn error(&self, text: &str) -> String {
self.paint(text, "error")
}
pub fn warning(&self, text: &str) -> String {
self.paint(text, "warning")
}
pub fn enabled(&self, val: bool) -> String {
if val {
self.success("yes")
} else {
self.error("no")
}
}
pub fn action(&self, text: &str) -> String {
match text.to_lowercase().as_str() {
"allow" => self.success(text),
"block" | "drop" => self.error(text),
"reject" => self.warning(text),
_ => self.muted(text),
}
}
pub fn state(&self, text: &str) -> String {
match text.to_lowercase().as_str() {
"online" | "connected" => self.success(text),
"offline" | "disconnected" => self.error(text),
_ => self.warning(text),
}
}
pub fn health(&self, text: &str) -> String {
match text.to_lowercase().as_str() {
"ok" | "healthy" => self.success(text),
"warning" => self.warning(text),
_ => self.error(text),
}
}
pub fn keyword(&self, text: &str) -> String {
if self.enabled {
format!("{}", text.style(self.theme.owo_style("keyword")))
} else {
text.to_string()
}
}
}
pub fn render_list<T, R>(
format: &OutputFormat,
data: &[T],
to_row: impl Fn(&T) -> R,
id_fn: impl Fn(&T) -> String,
) -> String
where
T: serde::Serialize,
R: Tabled,
{
match format {
OutputFormat::Table => {
let rows: Vec<R> = data.iter().map(to_row).collect();
render_table(&rows)
}
OutputFormat::Json => render_json(data, false),
OutputFormat::JsonCompact => render_json(data, true),
OutputFormat::Yaml => render_yaml(data),
OutputFormat::Plain => data.iter().map(&id_fn).collect::<Vec<_>>().join("\n"),
}
}
pub fn render_single<T>(
format: &OutputFormat,
data: &T,
detail_fn: impl Fn(&T) -> String,
id_fn: impl Fn(&T) -> String,
) -> String
where
T: serde::Serialize,
{
match format {
OutputFormat::Table => detail_fn(data),
OutputFormat::Json => render_json(data, false),
OutputFormat::JsonCompact => render_json(data, true),
OutputFormat::Yaml => render_yaml(data),
OutputFormat::Plain => id_fn(data),
}
}
pub fn print_output(output: &str, quiet: bool) {
if quiet || output.is_empty() {
return;
}
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{output}");
}
fn render_table<R: Tabled>(rows: &[R]) -> String {
Table::new(rows).with(Style::rounded()).to_string()
}
pub(crate) fn render_json_pretty<T: serde::Serialize + ?Sized>(data: &T) -> String {
serde_json::to_string_pretty(data).expect("serialization should not fail")
}
pub(crate) fn render_json_compact<T: serde::Serialize + ?Sized>(data: &T) -> String {
serde_json::to_string(data).expect("serialization should not fail")
}
fn render_json<T: serde::Serialize + ?Sized>(data: &T, compact: bool) -> String {
if compact {
render_json_compact(data)
} else {
render_json_pretty(data)
}
}
pub(crate) fn render_yaml<T: serde::Serialize + ?Sized>(data: &T) -> String {
serde_yml::to_string(data).expect("serialization should not fail")
}