fundaia 0.25.1

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use std::sync::atomic::{AtomicBool, Ordering};

use owo_colors::{OwoColorize, Style};

/// Whether anything in this module paints.
///
/// `owo_colors::set_override` was doing this job and never could: it governs
/// `if_supports_color`, and every screen in this tool applies a `Style`
/// directly — so `--no-color` and `--json` both left the escapes in, and an
/// agent parsing the output got them. Read on every style rather than resolved
/// once, because it is set after `main` has already begun.
///
/// A process-wide flag because that is what the question is: one invocation
/// writes to one terminal. Threading a palette through every renderer would put
/// an argument on forty functions to carry a value none of them decide.
static PAINTS: AtomicBool = AtomicBool::new(true);

/// Turns every style below into a no-op, for `--no-color` and for `--json`.
pub fn silence() {
    PAINTS.store(false, Ordering::Relaxed);
}

/// The style, or nothing at all when the output must stay plain.
fn painted(style: Style) -> Style {
    chosen(style, PAINTS.load(Ordering::Relaxed))
}

/// The decision, without the global, so a test can make it without deciding it
/// for every other test in the process.
fn chosen(style: Style, paints: bool) -> Style {
    if paints {
        style
    } else {
        Style::new()
    }
}

/// The palette, matched to the web interface rather than invented.
///
/// A terminal has sixteen colours it can promise and a truecolour escape it
/// usually honours; these use the named ones, so the output stays legible in a
/// light terminal, over SSH, and inside a CI log that strips colour entirely.
///
/// The names are roles, not colours, for the same reason the CSS tokens are:
/// `state_style("active")` should keep working when green stops being the
/// answer.
pub fn accent() -> Style {
    painted(Style::new().magenta())
}

pub fn muted() -> Style {
    painted(Style::new().bright_black())
}

pub fn strong() -> Style {
    painted(Style::new().bold())
}

/// The other half of what a hand-written paragraph uses, next to `strong`.
pub fn emphasis() -> Style {
    painted(Style::new().italic())
}

pub fn success() -> Style {
    painted(Style::new().green())
}

pub fn warning() -> Style {
    painted(Style::new().yellow())
}

pub fn danger() -> Style {
    painted(Style::new().red())
}

/// How a service state is coloured, and what it is called in Spanish.
pub fn state_style(state: &str) -> Style {
    match state {
        "active" => success(),
        "deploying" => accent(),
        "sleeping" => Style::new().cyan(),
        "degraded" => warning(),
        "crashed" | "failed" => danger(),
        _ => muted(),
    }
}

/// Borrowed from the argument, so an unknown state can be passed through
/// rather than replaced by a placeholder that hides what the server said.
pub fn state_word(state: &str) -> &str {
    match state {
        "draft" | "idle" => "sin desplegar",
        "deploying" => "desplegando",
        "active" => "en marcha",
        "sleeping" => "dormido",
        "degraded" => "con problemas",
        "crashed" => "ha caído",
        "failed" => "ha fallado",
        "removing" => "eliminando",
        other => other,
    }
}

pub fn deployment_status_style(status: &str) -> Style {
    match status {
        "active" => success(),
        "queued" | "building" | "deploying" => accent(),
        "failed" | "crashed" => danger(),
        // Not red: stopped on purpose is not something that went wrong.
        _ => muted(),
    }
}

/// A dot in the state's colour: the same vocabulary as the canvas.
pub fn state_dot(state: &str) -> String {
    format!("{}", "●".style(state_style(state)))
}

/// A single ASCII fallback is not worth it — every terminal in use draws these.
pub const CHECK: &str = "✔";
pub const CROSS: &str = "✖";
pub const SKIP: &str = "–";
pub const PENDING: &str = "○";
pub const ARROW: &str = "→";

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_translate_a_running_service() {
        assert_eq!(state_word("active"), "en marcha");
    }

    #[test]
    fn it_should_pass_an_unknown_state_through_unchanged() {
        assert_eq!(state_word("something-new"), "something-new");
    }

    fn painted_with(paints: bool) -> String {
        format!("{}", "x".style(chosen(Style::new().red(), paints)))
    }

    #[test]
    fn it_should_paint_when_colour_is_on() {
        assert_ne!(painted_with(true), "x");
    }

    /// What `--no-color` and `--json` both need, and what `set_override` alone
    /// never gave them.
    #[test]
    fn it_should_leave_the_text_bare_when_colour_is_off() {
        assert_eq!(painted_with(false), "x");
    }
}