1use std::fmt;
13use std::io;
14use std::io::Write;
15use std::time::Duration;
16
17use indicatif::ProgressBar;
18use indicatif::ProgressStyle;
19use mz_ore::option::OptionExt;
20
21use serde::{Deserialize, Serialize};
22use serde_aux::serde_introspection::serde_introspect;
23use tabled::{Style, Table, Tabled};
24use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
25
26use crate::error::Error;
27
28#[derive(Debug, Clone, clap::ValueEnum)]
30pub enum OutputFormat {
31 Text,
33 Json,
35 Csv,
37}
38
39#[derive(Clone)]
41pub struct OutputFormatter {
42 output_format: OutputFormat,
43 no_color: bool,
44}
45
46const TICKS: [&str; 9] = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷", ""];
48
49impl OutputFormatter {
50 pub fn new(output_format: OutputFormat, no_color: bool) -> OutputFormatter {
52 OutputFormatter {
53 output_format,
54 no_color,
55 }
56 }
57
58 pub fn print_with_color(&self, message: &str, color: Color, stderr: bool) -> Result<(), Error> {
60 let mut stdeo = match stderr {
61 true => StandardStream::stderr(ColorChoice::Always),
62 false => StandardStream::stdout(ColorChoice::Always),
63 };
64 stdeo.set_color(ColorSpec::new().set_fg(Some(color)))?;
65 write!(&mut stdeo, "{}", message)?;
66
67 let _ = stdeo.reset();
69
70 Ok(())
71 }
72
73 pub fn output_warning(&self, msg: &str) -> Result<(), Error> {
75 if self.no_color {
76 eprintln!("\n* Warning * {}", msg);
77 } else {
78 let _ = self.print_with_color("\n* Warning *", Color::Yellow, true)?;
79 eprintln!(" {}", msg);
80 }
81
82 Ok(())
83 }
84
85 pub fn output_scalar(&self, scalar: Option<&str>) -> Result<(), Error> {
87 match self.output_format {
88 OutputFormat::Text => println!("{}", scalar.display_or("<unset>")),
89 OutputFormat::Json => serde_json::to_writer(io::stdout(), &scalar)?,
90 OutputFormat::Csv => {
91 let mut w = csv::Writer::from_writer(io::stdout());
92 w.write_record([scalar.unwrap_or("<unset>")])?;
93 w.flush()?;
94 }
95 }
96 Ok(())
97 }
98
99 pub fn output_table<'a, I, R>(&self, rows: I) -> Result<(), Error>
107 where
108 I: IntoIterator<Item = R>,
109 R: Deserialize<'a> + Serialize + Tabled,
110 {
111 match self.output_format {
112 OutputFormat::Text => {
113 let table = Table::new(rows).with(Style::psql()).to_string();
114 println!("{table}");
115 }
116 OutputFormat::Json => {
117 let rows = rows.into_iter().collect::<Vec<_>>();
118 serde_json::to_writer(io::stdout(), &rows)?;
119 }
120 OutputFormat::Csv => {
121 let mut w = csv::WriterBuilder::new()
122 .has_headers(false)
123 .from_writer(io::stdout());
124 w.write_record(serde_introspect::<R>())?;
125 for row in rows {
126 w.serialize(row)?;
127 }
128 w.flush()?;
129 }
130 }
131 Ok(())
132 }
133
134 pub fn loading_spinner(&self, message: &str) -> ProgressBar {
136 let progress_bar = ProgressBar::new_spinner();
137 progress_bar.enable_steady_tick(Duration::from_millis(120));
138
139 let tick_strings: Vec<&str> = match self.no_color {
140 true => TICKS.to_vec(),
141 false => TICKS.to_vec(),
142 };
143
144 progress_bar.set_style(
145 ProgressStyle::default_spinner()
146 .template("{spinner:1.green/green} {msg}")
147 .expect("template known to be valid")
148 .tick_strings(&tick_strings),
151 );
152
153 progress_bar.set_message(message.to_string());
154
155 progress_bar
156 }
157}
158
159#[derive(Serialize, Deserialize)]
161#[serde(transparent)]
162pub struct OptionalStr<'a>(pub Option<&'a str>);
163
164impl fmt::Display for OptionalStr<'_> {
165 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
166 match &self.0 {
167 None => f.write_str("<unset>"),
168 Some(s) => s.fmt(f),
169 }
170 }
171}