gdsuite 0.1.2

Workflow CLI for Godot projects: installs and coordinates gdtoolkit, gdtest, gdhealth and gdtypecheck
use std::{
    env,
    io::{self, IsTerminal, Write},
};

use anstream::{AutoStream, ColorChoice};
use anstyle::{AnsiColor, Style};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
    Pretty,
    Plain,
    Json,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandScope {
    Init,
    Setup,
    Doctor,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Level {
    Step,
    Info,
    Ok,
    Warn,
    Error,
}

pub struct OutputContext {
    mode: OutputMode,
    scope: CommandScope,
    color_enabled: bool,
}

impl OutputContext {
    pub fn new(mode: OutputMode, scope: CommandScope) -> Self {
        let color_enabled = mode == OutputMode::Pretty
            && io::stdout().is_terminal()
            && env::var_os("NO_COLOR").is_none();
        Self {
            mode,
            scope,
            color_enabled,
        }
    }

    pub fn mode(&self) -> OutputMode {
        self.mode
    }

    pub fn step(&self, message: impl AsRef<str>) {
        self.level(Level::Step, message.as_ref());
    }

    pub fn info(&self, message: impl AsRef<str>) {
        self.level(Level::Info, message.as_ref());
    }

    pub fn ok(&self, message: impl AsRef<str>) {
        self.level(Level::Ok, message.as_ref());
    }

    pub fn warn(&self, message: impl AsRef<str>) {
        self.level(Level::Warn, message.as_ref());
    }

    pub fn error(&self, message: impl AsRef<str>) {
        self.level(Level::Error, message.as_ref());
    }

    pub fn status(&self, status: &str) {
        match self.mode {
            OutputMode::Pretty => {
                self.write_stdout_line(&format!("{} status {}", self.prefix(), status))
            }
            OutputMode::Plain => {
                self.write_stdout_line(&format!("{} status={status}", self.prefix()))
            }
            OutputMode::Json => {}
        }
    }

    pub fn summary(&self, message: &str) {
        match self.mode {
            OutputMode::Pretty => self.ok(message),
            OutputMode::Plain => {
                self.write_stdout_line(&format!("{} summary={message}", self.prefix()))
            }
            OutputMode::Json => {}
        }
    }

    pub fn kv(&self, key: &str, value: &str) {
        match self.mode {
            OutputMode::Pretty => self.info(format!("{key:<13} {value}")),
            OutputMode::Plain => {
                self.write_stdout_line(&format!("{} {key}={value}", self.prefix()))
            }
            OutputMode::Json => {}
        }
    }

    pub fn blank(&self) {
        if self.mode == OutputMode::Pretty {
            self.write_stdout_line("");
        }
    }

    fn level(&self, level: Level, message: &str) {
        match self.mode {
            OutputMode::Pretty => self.write_stdout_line(&format!(
                "{} {} {message}",
                self.prefix(),
                self.render_level(level)
            )),
            OutputMode::Plain => self.write_stdout_line(&format!(
                "{} {}={message}",
                self.prefix(),
                level.plain_key()
            )),
            OutputMode::Json => {}
        }
    }

    fn prefix(&self) -> String {
        format!("[gdsuite:{}]", self.scope.as_str())
    }

    fn render_level(&self, level: Level) -> String {
        let label = format!("{:<5}", level.pretty_label());
        if !self.color_enabled {
            return label;
        }

        let style = level.style();
        format!("{}{}{}", style.render(), label, style.render_reset())
    }

    fn write_stdout_line(&self, line: &str) {
        let choice = if self.color_enabled {
            ColorChoice::Auto
        } else {
            ColorChoice::Never
        };
        let mut stream = AutoStream::new(io::stdout(), choice);
        let _ = writeln!(stream, "{line}");
    }
}

impl CommandScope {
    fn as_str(self) -> &'static str {
        match self {
            CommandScope::Init => "init",
            CommandScope::Setup => "setup",
            CommandScope::Doctor => "doctor",
        }
    }
}

impl Level {
    fn pretty_label(self) -> &'static str {
        match self {
            Level::Step => "step",
            Level::Info => "info",
            Level::Ok => "ok",
            Level::Warn => "warn",
            Level::Error => "error",
        }
    }

    fn plain_key(self) -> &'static str {
        match self {
            Level::Step => "step",
            Level::Info => "info",
            Level::Ok => "ok",
            Level::Warn => "warning",
            Level::Error => "error",
        }
    }

    fn style(self) -> Style {
        match self {
            Level::Step => Style::new().fg_color(Some(AnsiColor::Blue.into())).bold(),
            Level::Info => Style::new().fg_color(Some(AnsiColor::Cyan.into())),
            Level::Ok => Style::new().fg_color(Some(AnsiColor::Green.into())).bold(),
            Level::Warn => Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold(),
            Level::Error => Style::new().fg_color(Some(AnsiColor::Red.into())).bold(),
        }
    }
}