aegis-tool 0.3.3

Aegis SSH client and managed host agent.
Documentation
use std::sync::OnceLock;

use anyhow::{Result, anyhow};

pub use capulus::ui::{
    Color, ConfiguredRenderTarget, LiveGroup, LiveRow, RenderTarget, StderrRenderTarget,
    StdoutRenderTarget, Task, TaskKind, TaskOptions, TaskVisibility, TextEffect, format_duration,
};
use capulus::ui::{Ui, UiOptions};

use clap::{Args, ValueEnum};

#[derive(Clone, Copy, Debug, Default, Args)]
pub struct UiArgs {
    #[arg(
        long,
        global = true,
        value_enum,
        default_value_t = UiProgressMode::Auto,
        help = "Progress rendering mode (auto uses an interactive display on a terminal and plain status otherwise)"
    )]
    pub progress: UiProgressMode,

    #[arg(
        long,
        global = true,
        value_enum,
        default_value_t = UiColorMode::Auto,
        help = "Color rendering mode"
    )]
    pub color: UiColorMode,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum UiProgressMode {
    #[default]
    Auto,
    Interactive,
    Plain,
    Off,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum UiColorMode {
    #[default]
    Auto,
    Always,
    Never,
}

impl UiArgs {
    pub fn options(self) -> capulus::ui::UiOptions {
        capulus::ui::UiOptions {
            progress: match self.progress {
                UiProgressMode::Auto => capulus::ui::ProgressMode::Auto,
                UiProgressMode::Interactive => capulus::ui::ProgressMode::Interactive,
                UiProgressMode::Plain => capulus::ui::ProgressMode::Plain,
                UiProgressMode::Off => capulus::ui::ProgressMode::Off,
            },
            color: match self.color {
                UiColorMode::Auto => capulus::ui::ColorMode::Auto,
                UiColorMode::Always => capulus::ui::ColorMode::Always,
                UiColorMode::Never => capulus::ui::ColorMode::Never,
            },
            cancellation: capulus::ui::CancellationMode::Signal,
            ..capulus::ui::UiOptions::default()
        }
    }
}

mod selector;
pub(crate) use selector::{
    Choice, ChoiceStatus, ChoiceUpdate, Screen, SelectOptions, screen, select_live,
};

static UI: OnceLock<Ui> = OnceLock::new();

pub fn init(options: UiOptions) -> Result<()> {
    let ui = Ui::from_options(options)?;
    UI.set(ui)
        .map_err(|_| anyhow!("Aegis UI was initialized more than once"))
}

pub fn current() -> &'static Ui {
    #[cfg(test)]
    {
        UI.get_or_init(|| {
            Ui::from_options(UiOptions {
                progress: capulus::ui::ProgressMode::Off,
                color: capulus::ui::ColorMode::Never,
                ..UiOptions::default()
            })
            .expect("test UI options are valid")
        })
    }
    #[cfg(not(test))]
    {
        UI.get().expect("Aegis UI must be initialized before use")
    }
}

pub(crate) fn cancellation() -> capulus::Cancellation {
    UI.get()
        .map(Ui::cancellation)
        .unwrap_or_else(capulus::Cancellation::passive)
}

pub fn task(options: TaskOptions) -> Result<Task> {
    current().task(options)
}

pub fn live_group(label: impl Into<String>) -> Result<LiveGroup> {
    current().live_group(label)
}

pub fn render_target() -> ConfiguredRenderTarget {
    current().render_target()
}

pub fn stdout_render_target() -> ConfiguredRenderTarget {
    current().stdout_render_target()
}

pub fn check_cancelled() -> Result<()> {
    current().check_cancelled()?;
    Ok(())
}

pub fn sleep(duration: std::time::Duration) -> Result<()> {
    current().sleep(duration)?;
    Ok(())
}

pub fn suspend<T>(operation: impl FnOnce() -> T) -> T {
    current().suspend(operation)
}

pub fn require_interactive(message: &str) -> Result<()> {
    capulus::ui::require_interactive(message)
}

pub fn maybe_open_browser(url: &str) {
    current().suspend(|| capulus::ui::maybe_open_browser(url));
}

pub fn stage(message: &str) {
    current().info(message);
}

pub fn detail(message: &str) {
    current().detail(message);
}

pub fn success(message: &str) {
    current().success(message);
}

pub fn warn(message: &str) {
    current().warn(message);
}

pub fn error(message: &str) {
    current().error(message);
}

pub fn completed_line(message: &str) {
    outcome_line("✓", Color::Green, message);
}

pub fn abandoned_line(message: &str) {
    outcome_line("!", Color::Yellow, message);
}

fn outcome_line(glyph: &str, color: Color, message: &str) {
    let ui = current();
    ui.suspend(|| {
        eprintln!("{} {message}", ui.render_target().paint(glyph, color));
    });
}