upgate_presentation/
terminal.rs1use 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}