use owo_colors::OwoColorize;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub struct Spinner {
running: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Spinner {
fn new() -> Self {
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let handle = std::thread::spawn(move || {
let frames = ["Loading", "Loading. ", "Loading.. ", "Loading..."];
let mut i = 0;
while r.load(Ordering::Relaxed) {
eprint!("\r{}", frames[i % frames.len()]);
let _ = std::io::stderr().flush();
std::thread::sleep(std::time::Duration::from_millis(150));
i += 1;
}
eprint!("\r \r");
let _ = std::io::stderr().flush();
});
Spinner {
running,
handle: Some(handle),
}
}
pub fn finish_and_clear(&self) {
self.running.store(false, Ordering::Relaxed);
}
}
impl Drop for Spinner {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub fn new_spinner() -> Spinner {
Spinner::new()
}
pub fn sparkline(values: &[f64]) -> String {
const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
if values.is_empty() {
return String::new();
}
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let range = (max - min).max(1e-10);
values
.iter()
.map(|v| {
let idx = (((v - min) / range) * 7.0).round() as usize;
BLOCKS[idx.min(7)]
})
.collect()
}
pub fn colored_job_status(status: &str) -> String {
match status {
"succeeded" => status.green().to_string(),
"failed" | "timed_out" => status.red().to_string(),
"in_progress" => status.yellow().to_string(),
"canceled" => status.dimmed().to_string(),
"open" => status.cyan().to_string(),
_ => status.to_string(),
}
}
pub fn format_time(ts: &str) -> &str {
if ts.len() >= 19 && ts.as_bytes().get(10) == Some(&b'T') {
&ts[11..19]
} else {
ts
}
}
pub fn print_success(msg: &str) {
println!("{} {}", "✓".green().bold(), msg);
}
#[allow(dead_code)]
pub fn print_info(msg: &str) {
println!("{} {}", "→".cyan(), msg);
}
#[allow(dead_code)]
pub fn print_warning(msg: &str) {
eprintln!("{} {}", "warn:".yellow().bold(), msg);
}
pub fn print_error(err: &dyn std::error::Error) {
eprintln!("{} {}", "error:".red().bold(), err);
let mut source = err.source();
while let Some(cause) = source {
eprintln!(" {} {}", "→".dimmed(), cause.to_string().dimmed());
source = cause.source();
}
}
use tabled::Tabled;
#[derive(Tabled)]
pub struct WorkspaceRow {
#[tabled(rename = "Name")]
pub name: String,
#[tabled(rename = "Email")]
pub email: String,
#[tabled(rename = "ID")]
pub id: String,
}
#[derive(Tabled)]
pub struct ProjectRow {
#[tabled(rename = "Name")]
pub name: String,
#[tabled(rename = "Environment")]
pub environment: String,
#[tabled(rename = "ID")]
pub id: String,
}
#[derive(Tabled)]
pub struct JobRow {
#[tabled(rename = "Type")]
pub job_type: String,
#[tabled(rename = "Ref")]
pub deploy_ref: String,
#[tabled(rename = "Status")]
pub status: String,
#[tabled(rename = "Created")]
pub created_at: String,
}
#[derive(Tabled)]
pub struct ValidationRow {
#[tabled(rename = "Field")]
pub field: String,
#[tabled(rename = "Status")]
pub status: String,
#[tabled(rename = "Message")]
pub message: String,
}