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_datetime(ts: &str) -> String {
if ts.len() >= 16 && ts.as_bytes().get(10) == Some(&b'T') {
let year = &ts[..4];
let month = &ts[5..7];
let day = &ts[8..10];
let time = &ts[11..16];
return format!("{}/{}/{} {}", day, month, year, time);
}
ts.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_utc_timestamp() {
assert_eq!(format_datetime("2024-02-25T10:30:00Z"), "25/02/2024 10:30");
}
#[test]
fn formats_timestamp_with_positive_offset() {
assert_eq!(
format_datetime("2024-02-25T12:30:00+02:00"),
"25/02/2024 12:30"
);
}
#[test]
fn formats_timestamp_with_negative_offset() {
assert_eq!(
format_datetime("2024-02-25T08:00:00-05:00"),
"25/02/2024 08:00"
);
}
#[test]
fn invalid_string_returns_original() {
assert_eq!(format_datetime("not-a-timestamp"), "not-a-timestamp");
}
#[test]
fn empty_string_returns_empty() {
assert_eq!(format_datetime(""), "");
}
#[test]
fn date_only_returns_original() {
assert_eq!(format_datetime("2024-02-25"), "2024-02-25");
}
#[test]
fn milliseconds_are_dropped_in_output() {
assert_eq!(
format_datetime("2024-02-25T10:30:45.123Z"),
"25/02/2024 10:30"
);
}
}
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 = "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,
}