seaplane_cli/
printer.rs

1use std::{
2    borrow::Cow,
3    io,
4    result::Result as StdResult,
5    sync::{Mutex, MutexGuard, PoisonError},
6    time::Duration,
7};
8
9use indicatif::{ProgressBar, ProgressStyle};
10use once_cell::sync::OnceCell;
11use serde::{
12    de::{self, Deserializer},
13    Deserialize, Serialize,
14};
15use strum::{Display, EnumString};
16
17use crate::{error::Result, Ctx};
18
19#[cfg_attr(feature = "api_tests", allow(dead_code))]
20static GLOBAL_PRINTER: OnceCell<Mutex<self::_printer::Printer>> = OnceCell::new();
21#[cfg_attr(feature = "api_tests", allow(dead_code))]
22static GLOBAL_EPRINTER: OnceCell<Mutex<self::_printer::Printer>> = OnceCell::new();
23
24pub use self::_printer::{eprinter, printer, Printer};
25
26/// We wrap the progress bar to be able to hide output when we need to
27#[allow(missing_debug_implementations)]
28pub struct Pb(Option<ProgressBar>);
29
30impl Pb {
31    pub fn new(ctx: &Ctx) -> Self {
32        if !ctx.disable_pb && crate::log::log_level() <= &crate::log::LogLevel::Info {
33            let pb = ProgressBar::new_spinner();
34            pb.enable_steady_tick(Duration::from_millis(120));
35            pb.set_style(
36                ProgressStyle::default_bar()
37                    .template("{spinner:.green} {msg}")
38                    .expect("Invalid progress bar template"),
39            );
40            Pb(Some(pb))
41        } else {
42            Pb(None)
43        }
44    }
45
46    pub fn set_message(&self, msg: impl Into<Cow<'static, str>>) {
47        if let Some(pb) = &self.0 {
48            pb.set_message(msg);
49        }
50    }
51
52    pub fn finish_and_clear(&self) {
53        if let Some(pb) = &self.0 {
54            pb.finish_and_clear()
55        }
56    }
57}
58
59impl Drop for Pb {
60    fn drop(&mut self) { self.finish_and_clear() }
61}
62
63#[derive(EnumString, Display, Deserialize, Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
64#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
65pub enum OutputFormat {
66    Table,
67    Json,
68}
69
70impl Default for OutputFormat {
71    fn default() -> Self { OutputFormat::Table }
72}
73
74#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, clap::ValueEnum, Display, EnumString)]
75#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
76pub enum ColorChoice {
77    Always,
78    Ansi,
79    Auto,
80    Never,
81}
82
83impl Default for ColorChoice {
84    fn default() -> Self { ColorChoice::Auto }
85}
86
87impl<'de> Deserialize<'de> for ColorChoice {
88    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
89        use std::str::FromStr;
90        let s = <&str>::deserialize(deserializer)?;
91        ColorChoice::from_str(s).map_err(de::Error::custom)
92    }
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96#[non_exhaustive]
97#[allow(dead_code)]
98pub enum Color {
99    Black,
100    Blue,
101    Green,
102    Red,
103    Cyan,
104    Magenta,
105    Yellow,
106    White,
107}
108
109pub trait Output {
110    fn print_json(&self, _ctx: &Ctx) -> Result<()> {
111        cli_eprint!("--format=");
112        cli_eprint!(@Yellow, "json");
113        cli_eprintln!(" is not supported by this object");
114
115        Ok(())
116    }
117
118    fn print_table(&self, _ctx: &Ctx) -> Result<()> {
119        cli_eprint!("--format=");
120        cli_eprint!(@Yellow, "table");
121        cli_eprintln!(" is not supported by this object");
122
123        Ok(())
124    }
125}
126
127#[cfg(all(feature = "color", not(feature = "api_tests")))]
128mod _printer {
129    use atty::Stream;
130    use termcolor::{
131        Color as TermColorColor, ColorChoice as TermColorChoice, ColorSpec, StandardStream,
132        WriteColor,
133    };
134
135    use super::*;
136
137    fn detect_tty(stream: Stream) -> TermColorChoice {
138        if atty::is(stream) {
139            TermColorChoice::Auto
140        } else {
141            TermColorChoice::Never
142        }
143    }
144
145    #[allow(missing_debug_implementations)]
146    pub struct Printer(StandardStream);
147
148    pub fn printer() -> MutexGuard<'static, Printer> {
149        GLOBAL_PRINTER
150            .get_or_init(|| Mutex::new(Printer(StandardStream::stdout(TermColorChoice::Auto))))
151            .lock()
152            .unwrap_or_else(PoisonError::into_inner)
153    }
154
155    pub fn eprinter() -> MutexGuard<'static, Printer> {
156        GLOBAL_EPRINTER
157            .get_or_init(|| Mutex::new(Printer(StandardStream::stderr(TermColorChoice::Auto))))
158            .lock()
159            .unwrap_or_else(PoisonError::into_inner)
160    }
161
162    impl Printer {
163        pub fn init(color: ColorChoice) {
164            use TermColorChoice::*;
165            let (choice, echoice) = match color {
166                ColorChoice::Always => (Always, Always),
167                ColorChoice::Auto => (detect_tty(Stream::Stdout), detect_tty(Stream::Stderr)),
168                ColorChoice::Ansi => (AlwaysAnsi, AlwaysAnsi),
169                ColorChoice::Never => (Never, Never),
170            };
171
172            printer().set_stream(StandardStream::stdout(choice));
173            eprinter().set_stream(StandardStream::stderr(echoice));
174        }
175
176        fn set_stream(&mut self, stream: StandardStream) { self.0 = stream; }
177
178        pub fn set_color(&mut self, color: Color) {
179            let _ = self
180                .0
181                .set_color(ColorSpec::new().set_fg(Some(color.into_termcolor())));
182        }
183
184        pub fn reset(&mut self) { let _ = self.0.reset(); }
185    }
186
187    impl io::Write for Printer {
188        fn write(&mut self, buf: &[u8]) -> io::Result<usize> { self.0.write(buf) }
189
190        fn flush(&mut self) -> io::Result<()> { self.0.flush() }
191    }
192
193    impl Color {
194        fn into_termcolor(self) -> TermColorColor {
195            match self {
196                Color::Black => TermColorColor::Black,
197                Color::Blue => TermColorColor::Blue,
198                Color::Green => TermColorColor::Green,
199                Color::Red => TermColorColor::Red,
200                Color::Cyan => TermColorColor::Cyan,
201                Color::Magenta => TermColorColor::Magenta,
202                Color::Yellow => TermColorColor::Yellow,
203                Color::White => TermColorColor::White,
204            }
205        }
206    }
207}
208
209#[cfg(all(not(feature = "color"), not(feature = "api_tests")))]
210mod _printer {
211    use super::*;
212
213    enum StandardStream {
214        Stdout(io::Stdout),
215        Stderr(io::Stderr),
216    }
217
218    #[allow(missing_debug_implementations)]
219    pub struct Printer(StandardStream);
220
221    pub fn printer() -> MutexGuard<'static, Printer> {
222        GLOBAL_PRINTER
223            .get_or_init(|| Mutex::new(Printer(StandardStream::Stdout(io::stdout()))))
224            .lock()
225            .unwrap_or_else(PoisonError::into_inner)
226    }
227
228    pub fn eprinter() -> MutexGuard<'static, Printer> {
229        GLOBAL_EPRINTER
230            .get_or_init(|| Mutex::new(Printer(StandardStream::Stderr(io::stderr()))))
231            .lock()
232            .unwrap_or_else(PoisonError::into_inner)
233    }
234
235    impl Printer {
236        pub fn init(_color: ColorChoice) {
237            let _a = printer();
238            let _a = eprinter();
239        }
240
241        pub fn set_color(&mut self, _color: Color) {}
242
243        pub fn reset(&mut self) {}
244    }
245
246    impl io::Write for Printer {
247        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
248            match self.0 {
249                StandardStream::Stdout(ref mut s) => s.write(buf),
250                StandardStream::Stderr(ref mut s) => s.write(buf),
251            }
252        }
253
254        fn flush(&mut self) -> io::Result<()> {
255            match self.0 {
256                StandardStream::Stdout(ref mut s) => s.flush(),
257                StandardStream::Stderr(ref mut s) => s.flush(),
258            }
259        }
260    }
261}
262
263#[cfg(feature = "api_tests")]
264mod _printer {
265    use std::borrow::Cow;
266
267    use super::*;
268
269    static GLOBAL_TEST_PRINTER: OnceCell<Mutex<self::_printer::Printer>> = OnceCell::new();
270    static GLOBAL_TEST_EPRINTER: OnceCell<Mutex<self::_printer::Printer>> = OnceCell::new();
271
272    #[allow(missing_debug_implementations)]
273    pub struct Printer(pub Vec<u8>);
274
275    pub fn printer() -> MutexGuard<'static, Printer> {
276        GLOBAL_TEST_PRINTER
277            .get_or_init(|| Mutex::new(Printer(Vec::new())))
278            .lock()
279            .unwrap_or_else(PoisonError::into_inner)
280    }
281
282    pub fn eprinter() -> MutexGuard<'static, Printer> {
283        GLOBAL_TEST_EPRINTER
284            .get_or_init(|| Mutex::new(Printer(Vec::new())))
285            .lock()
286            .unwrap_or_else(PoisonError::into_inner)
287    }
288
289    impl Printer {
290        pub fn init(_color: ColorChoice) {
291            let _a = printer();
292            let _a = eprinter();
293        }
294
295        pub fn set_color(&mut self, _color: Color) {}
296
297        pub fn reset(&mut self) {}
298
299        pub fn clear(&mut self) { self.0.clear() }
300
301        pub fn as_string(&self) -> Cow<'_, str> { String::from_utf8_lossy(&self.0) }
302    }
303
304    impl io::Write for Printer {
305        fn write(&mut self, buf: &[u8]) -> io::Result<usize> { self.0.write(buf) }
306
307        fn flush(&mut self) -> io::Result<()> { Ok(()) }
308    }
309}