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}