Skip to main content

upgate_presentation/
terminal.rs

1use std::fmt::{self, Display};
2use std::io::IsTerminal;
3use std::time::Duration;
4
5use indicatif::{ProgressBar, ProgressStyle};
6
7use crate::OutputTheme;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum BatchTerminalAction {
11    Scan,
12    Plan,
13    Apply,
14}
15
16impl BatchTerminalAction {
17    const fn spinner_label(self) -> &'static str {
18        match self {
19            Self::Scan => "Scanning",
20            Self::Plan => "Planning",
21            Self::Apply => "Applying",
22        }
23    }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MutationNotice {
28    Skip,
29    Real,
30}
31
32impl Display for MutationNotice {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        formatter.write_str(match self {
35            Self::Skip => {
36                "note: apply runs in safe mode: mutating commands are skipped (safe mode)"
37            }
38            Self::Real => "warning: apply runs with real mutating commands are ENABLED",
39        })
40    }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct BatchTerminal {
45    theme: OutputTheme,
46    stderr_is_tty: bool,
47    spinner_suppressed: bool,
48}
49
50impl BatchTerminal {
51    pub fn from_environment(theme: OutputTheme) -> Self {
52        Self {
53            theme,
54            stderr_is_tty: std::io::stderr().is_terminal(),
55            spinner_suppressed: false,
56        }
57    }
58    pub const fn suppress_spinner(self) -> Self {
59        Self {
60            spinner_suppressed: true,
61            ..self
62        }
63    }
64    pub const fn spinner_enabled(self) -> bool {
65        !self.spinner_suppressed && !self.theme.is_plain() && self.stderr_is_tty
66    }
67    pub const fn notice_enabled(self) -> bool {
68        self.stderr_is_tty
69    }
70    pub fn start_action_spinner(self, action: BatchTerminalAction) -> ManagerSpinner {
71        if !self.spinner_enabled() {
72            return ManagerSpinner(None);
73        }
74
75        let progress = ProgressBar::new_spinner();
76        progress.set_style(spinner_style(self.theme.color()));
77        progress.set_message(format!("{}...", action.spinner_label()));
78        progress.enable_steady_tick(Duration::from_millis(90));
79
80        ManagerSpinner(Some(progress))
81    }
82}
83
84#[derive(Debug)]
85pub struct ManagerSpinner(Option<ProgressBar>);
86
87impl Drop for ManagerSpinner {
88    fn drop(&mut self) {
89        if let Some(progress) = self.0.take() {
90            progress.finish_and_clear();
91        }
92    }
93}
94
95fn spinner_style(color: bool) -> ProgressStyle {
96    let (template, ticks): (&str, &[&str]) = if color {
97        (
98            "{spinner:.cyan} {msg}",
99            &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
100        )
101    } else {
102        ("{spinner} {msg}", &["-", "\\", "|", "/"])
103    };
104
105    ProgressStyle::with_template(template)
106        .unwrap_or_else(|_| ProgressStyle::default_spinner())
107        .tick_strings(ticks)
108}