evobench_tools/output_table/
terminal.rs1use std::{
12 io::{BufWriter, IsTerminal, Write},
13 os::unix::ffi::OsStrExt,
14};
15
16use crate::{
17 output_table::{BarKind, CellValue, OutputStyle, OutputTable, OutputTableTitle, Row},
18 utillib::get_terminal_width::get_terminal_width,
19};
20use anyhow::{Result, anyhow, bail};
21use lazy_static::lazy_static;
22use strum_macros::EnumString;
23use yansi::{Color, Paint, Style};
24
25impl From<OutputStyle> for Style {
26 fn from(value: OutputStyle) -> Self {
27 let OutputStyle {
28 faded,
29 bold,
30 italic,
31 color,
32 font_size: _,
33 } = value;
34 let mut style = Style::new();
35 if faded {
36 style = style.bright_black()
40 }
41 if bold {
42 style = style.bold()
43 }
44 if italic {
45 style = style.italic()
46 }
47 if let Some(col) = color {
48 style = style.fg(Color::Fixed(col))
54 }
55
56 style
57 }
58}
59
60lazy_static! {
61 static ref UNICODE_IS_FINE: bool = (|| -> Option<bool> {
62 let term = std::env::var_os("TERM")?;
63 let lang = std::env::var_os("LANG")?;
64 let lang = lang.to_str()?;
65 Some(term.as_bytes().starts_with(b"xterm") && lang.contains("UTF-8"))
66 })()
67 .unwrap_or(false);
68}
69
70#[derive(Debug, EnumString, PartialEq, Clone, Copy)]
71#[strum(serialize_all = "kebab_case")]
72pub enum ColorOpt {
73 Auto,
74 Always,
75 Never,
76}
77
78impl ColorOpt {
79 pub fn want_color(self, detected_terminal: bool) -> bool {
80 match self {
81 ColorOpt::Auto => detected_terminal,
82 ColorOpt::Always => true,
83 ColorOpt::Never => false,
84 }
85 }
86}
87
88#[derive(Debug, clap::Args, Clone)]
89pub struct TerminalTableOpts {
90 #[clap(long)]
93 tsv: bool,
94
95 #[clap(long, default_value = "auto")]
98 color: ColorOpt,
99}
100
101impl TerminalTableOpts {
102 pub fn want_color(&self, detected_terminal: bool) -> bool {
103 let Self { tsv, color } = self;
104 if *tsv {
105 false
106 } else {
107 color.want_color(detected_terminal)
108 }
109 }
110}
111
112struct TerminalTableSettings {
113 widths: Vec<usize>,
114 padding: String,
115 is_terminal: bool,
116}
117
118pub struct TerminalTable<O: Write + IsTerminal> {
124 pub opts: TerminalTableOpts,
125 settings: TerminalTableSettings,
126 thin_bar: String,
127 thick_bar: String,
128 out: BufWriter<O>,
129}
130
131impl<O: Write + IsTerminal> TerminalTable<O> {
132 const MINIMAL_PADDING_LEN: usize = 1;
135
136 pub fn new(widths: &[usize], opts: TerminalTableOpts, out: O) -> Self {
145 let max_width = widths.iter().max().copied().unwrap_or(0);
146 let padding = " ".repeat(max_width);
147 let is_terminal = out.is_terminal();
148
149 let width = get_terminal_width(1);
150 let bar_of = |c: &str| c.repeat(width) + "\n";
151 let (thin_bar, thick_bar) = if *UNICODE_IS_FINE {
152 (bar_of("─"), bar_of("═"))
153 } else {
154 (bar_of("-"), bar_of("="))
155 };
156
157 Self {
158 settings: TerminalTableSettings {
159 widths: widths.to_owned(),
160 padding,
161 is_terminal,
162 },
163 opts,
164 out: BufWriter::new(out),
165 thin_bar,
166 thick_bar,
167 }
168 }
169}
170
171impl<O: Write + IsTerminal> OutputTable for TerminalTable<O> {
172 type Output = O;
173
174 fn num_columns(&self) -> usize {
175 self.settings.widths.len() + 1
176 }
177
178 fn write_row<'url, V: CellValue<'url>>(
181 &mut self,
182 row: Row<V>,
183 line_style: Option<OutputStyle>,
184 ) -> Result<()> {
185 let (expected_num_columns, row_num_columns) = (self.num_columns(), row.logical_len());
186 if expected_num_columns != row_num_columns {
187 bail!(
188 "the row contains {row_num_columns} instead of the expected \
189 {expected_num_columns} columns"
190 )
191 }
192
193 let mut is_first = true;
194 for (text, width_opt) in row.string_and_widths(&self.settings.widths) {
195 if self.opts.tsv && !is_first {
196 self.out.write_all("\t".as_bytes())?;
197 }
198 let mut text = text.to_string();
199 let minimal_pading_len;
200 if let Some(style) = line_style {
201 text.push_str(" ");
203 minimal_pading_len = Self::MINIMAL_PADDING_LEN.saturating_sub(1);
204 let s = text.as_str().paint(style);
205 let s = s.to_string();
206 self.out.write_all(s.as_bytes())?;
207 } else {
208 minimal_pading_len = Self::MINIMAL_PADDING_LEN;
209 self.out.write_all(text.as_bytes())?;
210 }
211 let text_len = text.len();
212
213 if let Some(width) = width_opt {
214 if !self.opts.tsv {
215 let missing_padding_len = width.saturating_sub(text_len);
216 let wanted_padding_len = missing_padding_len.max(minimal_pading_len);
217 let padding = &self.settings.padding[0..wanted_padding_len];
218 self.out.write_all(padding.as_bytes())?;
219 }
220 }
221
222 is_first = false;
223 }
224 self.out.write_all(b"\n")?;
225 Ok(())
226 }
227
228 fn write_title_row(
229 &mut self,
230 titles: &[OutputTableTitle],
231 line_style: Option<OutputStyle>,
232 ) -> Result<()> {
233 let style = line_style.unwrap_or(OutputStyle {
234 bold: true,
235 italic: true,
236 ..Default::default()
237 });
238
239 self.write_row(
240 Row::<&str>::WithSpans(titles),
241 if self.opts.want_color(self.settings.is_terminal) {
242 Some(style)
243 } else {
244 None
245 },
246 )
247 }
248
249 fn write_bar(&mut self, bar_kind: BarKind, _anchor_name: Option<&str>) -> anyhow::Result<()> {
250 let bar = match bar_kind {
251 BarKind::Thin => &self.thin_bar,
252 BarKind::Thick => &self.thick_bar,
253 };
254 Ok(self.out.write_all(bar.as_bytes())?)
255 }
256
257 fn print<'url, V: CellValue<'url>>(&mut self, value: V) -> anyhow::Result<()> {
258 self.out.write_all(value.as_ref().as_bytes())?;
259 Ok(())
260 }
261
262 fn finish(self) -> Result<O> {
263 self.out
264 .into_inner()
265 .map_err(|e| anyhow!("flushing the buffer: {}", e.error()))
266 }
267}