Skip to main content

aegis_tool/
ui.rs

1use std::sync::OnceLock;
2
3use anyhow::{Result, anyhow};
4
5pub use capulus::ui::{
6    Color, ConfiguredRenderTarget, LiveGroup, LiveRow, RenderTarget, StderrRenderTarget,
7    StdoutRenderTarget, Task, TaskKind, TaskOptions, TaskVisibility, TextEffect, format_duration,
8};
9use capulus::ui::{Ui, UiOptions};
10
11use clap::{Args, ValueEnum};
12
13#[derive(Clone, Copy, Debug, Default, Args)]
14pub struct UiArgs {
15    #[arg(
16        long,
17        global = true,
18        value_enum,
19        default_value_t = UiProgressMode::Auto,
20        help = "Progress rendering mode (auto uses an interactive display on a terminal and plain status otherwise)"
21    )]
22    pub progress: UiProgressMode,
23
24    #[arg(
25        long,
26        global = true,
27        value_enum,
28        default_value_t = UiColorMode::Auto,
29        help = "Color rendering mode"
30    )]
31    pub color: UiColorMode,
32}
33
34#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
35pub enum UiProgressMode {
36    #[default]
37    Auto,
38    Interactive,
39    Plain,
40    Off,
41}
42
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
44pub enum UiColorMode {
45    #[default]
46    Auto,
47    Always,
48    Never,
49}
50
51impl UiArgs {
52    pub fn options(self) -> capulus::ui::UiOptions {
53        capulus::ui::UiOptions {
54            progress: match self.progress {
55                UiProgressMode::Auto => capulus::ui::ProgressMode::Auto,
56                UiProgressMode::Interactive => capulus::ui::ProgressMode::Interactive,
57                UiProgressMode::Plain => capulus::ui::ProgressMode::Plain,
58                UiProgressMode::Off => capulus::ui::ProgressMode::Off,
59            },
60            color: match self.color {
61                UiColorMode::Auto => capulus::ui::ColorMode::Auto,
62                UiColorMode::Always => capulus::ui::ColorMode::Always,
63                UiColorMode::Never => capulus::ui::ColorMode::Never,
64            },
65            cancellation: capulus::ui::CancellationMode::Signal,
66            ..capulus::ui::UiOptions::default()
67        }
68    }
69}
70
71mod selector;
72pub(crate) use selector::{
73    Choice, ChoiceStatus, ChoiceUpdate, Screen, SelectOptions, screen, select_live,
74};
75
76static UI: OnceLock<Ui> = OnceLock::new();
77
78pub fn init(options: UiOptions) -> Result<()> {
79    let ui = Ui::from_options(options)?;
80    UI.set(ui)
81        .map_err(|_| anyhow!("Aegis UI was initialized more than once"))
82}
83
84pub fn current() -> &'static Ui {
85    #[cfg(test)]
86    {
87        UI.get_or_init(|| {
88            Ui::from_options(UiOptions {
89                progress: capulus::ui::ProgressMode::Off,
90                color: capulus::ui::ColorMode::Never,
91                ..UiOptions::default()
92            })
93            .expect("test UI options are valid")
94        })
95    }
96    #[cfg(not(test))]
97    {
98        UI.get().expect("Aegis UI must be initialized before use")
99    }
100}
101
102pub(crate) fn cancellation() -> capulus::Cancellation {
103    UI.get()
104        .map(Ui::cancellation)
105        .unwrap_or_else(capulus::Cancellation::passive)
106}
107
108pub fn task(options: TaskOptions) -> Result<Task> {
109    current().task(options)
110}
111
112pub fn live_group(label: impl Into<String>) -> Result<LiveGroup> {
113    current().live_group(label)
114}
115
116pub fn render_target() -> ConfiguredRenderTarget {
117    current().render_target()
118}
119
120pub fn stdout_render_target() -> ConfiguredRenderTarget {
121    current().stdout_render_target()
122}
123
124pub fn check_cancelled() -> Result<()> {
125    current().check_cancelled()?;
126    Ok(())
127}
128
129pub fn sleep(duration: std::time::Duration) -> Result<()> {
130    current().sleep(duration)?;
131    Ok(())
132}
133
134pub fn suspend<T>(operation: impl FnOnce() -> T) -> T {
135    current().suspend(operation)
136}
137
138pub fn require_interactive(message: &str) -> Result<()> {
139    capulus::ui::require_interactive(message)
140}
141
142pub fn maybe_open_browser(url: &str) {
143    current().suspend(|| capulus::ui::maybe_open_browser(url));
144}
145
146pub fn stage(message: &str) {
147    current().info(message);
148}
149
150pub fn detail(message: &str) {
151    current().detail(message);
152}
153
154pub fn success(message: &str) {
155    current().success(message);
156}
157
158pub fn warn(message: &str) {
159    current().warn(message);
160}
161
162pub fn error(message: &str) {
163    current().error(message);
164}
165
166pub fn completed_line(message: &str) {
167    outcome_line("✓", Color::Green, message);
168}
169
170pub fn abandoned_line(message: &str) {
171    outcome_line("!", Color::Yellow, message);
172}
173
174fn outcome_line(glyph: &str, color: Color, message: &str) {
175    let ui = current();
176    ui.suspend(|| {
177        eprintln!("{} {message}", ui.render_target().paint(glyph, color));
178    });
179}