use std::str::FromStr;
use owo_colors::OwoColorize;
use tabled::settings::{object::Rows, Alignment, Modify, Style};
use tabled::{Table, Tabled};
use crate::exit::CtlError;
use crate::model::{Cell, Formation};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Table,
Wide,
Json,
Name,
}
impl FromStr for OutputFormat {
type Err = CtlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"" | "table" => Ok(Self::Table),
"wide" => Ok(Self::Wide),
"json" => Ok(Self::Json),
"name" => Ok(Self::Name),
other => Err(CtlError::usage(format!(
"unknown --output format: {other} (expected: table|wide|json|name)"
))),
}
}
}
#[derive(Tabled)]
struct FormationRow {
#[tabled(rename = "NAME")]
name: String,
#[tabled(rename = "STATE")]
state: String,
#[tabled(rename = "CELLS")]
cells: String,
#[tabled(rename = "AGE")]
age: String,
}
#[derive(Tabled)]
struct FormationWideRow {
#[tabled(rename = "NAME")]
name: String,
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "STATE")]
state: String,
#[tabled(rename = "CELLS")]
cells: String,
#[tabled(rename = "TENANT")]
tenant: String,
#[tabled(rename = "CREATED")]
created: String,
#[tabled(rename = "UPDATED")]
updated: String,
}
#[derive(Tabled)]
struct CellRow {
#[tabled(rename = "NAME")]
name: String,
#[tabled(rename = "STATE")]
state: String,
#[tabled(rename = "FORMATION")]
formation: String,
#[tabled(rename = "AGE")]
age: String,
}
#[derive(Tabled)]
struct CellWideRow {
#[tabled(rename = "NAME")]
name: String,
#[tabled(rename = "ID")]
id: String,
#[tabled(rename = "STATE")]
state: String,
#[tabled(rename = "FORMATION")]
formation: String,
#[tabled(rename = "IMAGE")]
image: String,
#[tabled(rename = "CRITICAL")]
critical: String,
#[tabled(rename = "STARTED")]
started: String,
#[tabled(rename = "FINISHED")]
finished: String,
}
pub fn render_formations(items: &[Formation], fmt: OutputFormat) {
match fmt {
OutputFormat::Json => print_json(items),
OutputFormat::Name => {
for f in items {
println!("{}", pick_name(&f.name, &f.id));
}
}
OutputFormat::Table => {
let rows: Vec<FormationRow> = items
.iter()
.map(|f| FormationRow {
name: pick_name(&f.name, &f.id),
state: colorize_state(&f.state),
cells: f.cells.len().to_string(),
age: age_of(f.created_at.as_deref()),
})
.collect();
print_table_rows(rows);
}
OutputFormat::Wide => {
let rows: Vec<FormationWideRow> = items
.iter()
.map(|f| FormationWideRow {
name: pick_name(&f.name, &f.id),
id: short_id(&f.id),
state: colorize_state(&f.state),
cells: f.cells.len().to_string(),
tenant: f.tenant.clone().unwrap_or_else(|| "-".into()),
created: f.created_at.clone().unwrap_or_else(|| "-".into()),
updated: f.updated_at.clone().unwrap_or_else(|| "-".into()),
})
.collect();
print_table_rows(rows);
}
}
}
pub fn render_cells(items: &[Cell], fmt: OutputFormat) {
match fmt {
OutputFormat::Json => print_json(items),
OutputFormat::Name => {
for c in items {
println!("{}", pick_name(&c.name, &c.id));
}
}
OutputFormat::Table => {
let rows: Vec<CellRow> = items
.iter()
.map(|c| CellRow {
name: pick_name(&c.name, &c.id),
state: colorize_state(&c.state),
formation: c.formation_id.clone().unwrap_or_else(|| "-".into()),
age: age_of(c.created_at.as_deref()),
})
.collect();
print_table_rows(rows);
}
OutputFormat::Wide => {
let rows: Vec<CellWideRow> = items
.iter()
.map(|c| CellWideRow {
name: pick_name(&c.name, &c.id),
id: short_id(&c.id),
state: colorize_state(&c.state),
formation: c.formation_id.clone().unwrap_or_else(|| "-".into()),
image: c.image.clone().unwrap_or_else(|| "-".into()),
critical: c
.critical
.map(|b| if b { "yes" } else { "no" }.to_string())
.unwrap_or_else(|| "-".into()),
started: c.started_at.clone().unwrap_or_else(|| "-".into()),
finished: c.finished_at.clone().unwrap_or_else(|| "-".into()),
})
.collect();
print_table_rows(rows);
}
}
}
fn print_table_rows<R: Tabled>(rows: Vec<R>) {
if rows.is_empty() {
eprintln!("No resources found.");
return;
}
let mut t = Table::new(rows);
t.with(Style::blank())
.with(Modify::new(Rows::first()).with(Alignment::left()));
println!("{t}");
}
fn print_json<T: serde::Serialize + ?Sized>(v: &T) {
match serde_json::to_string_pretty(v) {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("cellctl: api: encode json: {e}"),
}
}
fn pick_name(name: &str, id: &str) -> String {
if !name.is_empty() {
name.to_string()
} else if !id.is_empty() {
id.to_string()
} else {
"-".into()
}
}
fn short_id(id: &str) -> String {
if id.len() > 12 {
id[..12].to_string()
} else {
id.to_string()
}
}
fn colorize_state(state: &str) -> String {
let up = state.to_ascii_uppercase();
match up.as_str() {
"RUNNING" | "LAUNCHING" => up.green().to_string(),
"COMPLETED" | "SUCCEEDED" => up.cyan().to_string(),
"DEGRADED" => up.yellow().to_string(),
"FAILED" | "KILLED" | "CANCELLED" => up.red().to_string(),
"PENDING" | "ADMITTED" => up.bright_blue().to_string(),
_ if up.is_empty() => "-".to_string(),
_ => up,
}
}
fn age_of(ts: Option<&str>) -> String {
let Some(ts) = ts else { return "-".into() };
match chrono::DateTime::parse_from_rfc3339(ts) {
Ok(dt) => {
let now = chrono::Utc::now();
let delta = now.signed_duration_since(dt.with_timezone(&chrono::Utc));
let secs = delta.num_seconds().max(0);
if secs < 60 {
format!("{secs}s")
} else if secs < 3600 {
format!("{}m", secs / 60)
} else if secs < 86_400 {
format!("{}h", secs / 3600)
} else {
format!("{}d", secs / 86_400)
}
}
Err(_) => ts.to_string(),
}
}