partiri-cli 0.1.3

partiri CLI — Deploy and manage services on Partiri Cloud
use owo_colors::OwoColorize;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

// ─── Spinner ─────────────────────────────────────────────────────────────────

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()
}

// ─── Sparkline ────────────────────────────────────────────────────────────────

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(),
    }
}

// ─── Timestamp helpers ───────────────────────────────────────────────────────

/// Extract just the time portion (HH:MM:SS) from an ISO 8601 / RFC 3339 timestamp.
/// Falls back to returning the input as-is if parsing fails.
pub fn format_time(ts: &str) -> &str {
    if ts.len() >= 19 && ts.as_bytes().get(10) == Some(&b'T') {
        &ts[11..19]
    } else {
        ts
    }
}

// ─── Print helpers ────────────────────────────────────────────────────────────

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();
    }
}

// ─── Tabled row types ─────────────────────────────────────────────────────────

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,
}