evobench_tools/output_table/
mod.rs

1//! Generate tables for human consumption, for the terminal, as HTML or CSV
2
3use itertools::{EitherOrBoth, Itertools};
4use kstring::KString;
5use std::borrow::Cow;
6
7pub mod html;
8pub mod terminal;
9
10#[derive(Debug)]
11pub struct OutputTableTitle<'s> {
12    pub text: Cow<'s, str>,
13    /// How many columns this should span across; should normally be
14    /// `1`
15    pub span: usize,
16    /// Optional anchor for HTML
17    pub anchor_name: Option<KString>,
18}
19
20pub trait CellValue<'url>: AsRef<str> {
21    /// If appropriate for the type and instance, return a URL value
22    fn perhaps_url(&self) -> Option<Cow<'url, str>>;
23    /// If appropriate, generate an anchor
24    fn perhaps_anchor_name(&self) -> Option<&KString>;
25}
26
27impl<'url> CellValue<'url> for str {
28    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
29        None
30    }
31    fn perhaps_anchor_name(&self) -> Option<&KString> {
32        None
33    }
34}
35impl<'url> CellValue<'url> for &str {
36    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
37        None
38    }
39    fn perhaps_anchor_name(&self) -> Option<&KString> {
40        None
41    }
42}
43impl<'url> CellValue<'url> for String {
44    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
45        None
46    }
47    fn perhaps_anchor_name(&self) -> Option<&KString> {
48        None
49    }
50}
51impl<'t, 'url> CellValue<'url> for Cow<'t, str> {
52    fn perhaps_url(&self) -> Option<Cow<'static, str>> {
53        None
54    }
55    fn perhaps_anchor_name(&self) -> Option<&KString> {
56        None
57    }
58}
59// Hmm huh.
60impl<'t, 'url> CellValue<'url> for &dyn CellValue<'url> {
61    fn perhaps_url(&self) -> Option<Cow<'url, str>> {
62        (*self).perhaps_url()
63    }
64    fn perhaps_anchor_name(&self) -> Option<&KString> {
65        None
66    }
67}
68
69/// Either something that can have spans; or something that can have
70/// URLs. Assumes that never want to have both.
71pub enum Row<'r, 's, V> {
72    WithSpans(&'r [OutputTableTitle<'s>]),
73    PlainStrings(&'r [V]),
74}
75
76impl<'r, 's, 'url, V: CellValue<'url>> Row<'r, 's, V> {
77    /// How many columns this Row covers (if it has entries that span
78    /// multiple columns, all of those are added)
79    fn logical_len(&self) -> usize {
80        match self {
81            Row::WithSpans(terminal_table_titles) => {
82                let mut cols = 0;
83                for OutputTableTitle {
84                    text: _,
85                    span,
86                    anchor_name: _,
87                } in *terminal_table_titles
88                {
89                    cols += span;
90                }
91                cols
92            }
93            Row::PlainStrings(items) => items.len(),
94        }
95    }
96
97    /// Adds widths together for spanned columns. The width for the
98    /// last column is None. -- This is only interesting for
99    /// TerminalTable.
100    fn string_and_widths(&self, widths: &[usize]) -> Vec<(Cow<'_, str>, Option<usize>)> {
101        match self {
102            Row::WithSpans(terminal_table_titles) => {
103                let mut v: Vec<(Cow<str>, Option<usize>)> = Vec::new();
104                let mut widths = widths.into_iter();
105                for OutputTableTitle {
106                    text,
107                    span,
108                    anchor_name: _,
109                } in *terminal_table_titles
110                {
111                    match *span {
112                        0 => (),
113                        n => {
114                            let width = (|| {
115                                let mut tot_width = 0;
116                                for _ in 0..n {
117                                    if let Some(width) = widths.next() {
118                                        tot_width += width;
119                                    } else {
120                                        return None;
121                                    }
122                                }
123                                Some(tot_width)
124                            })();
125                            v.push((text.as_ref().into(), width));
126                        }
127                    }
128                }
129                v
130            }
131            Row::PlainStrings(items) => {
132                let mut v: Vec<(Cow<str>, Option<usize>)> = Vec::new();
133                for either_or_both in items.iter().zip_longest(widths) {
134                    match either_or_both {
135                        EitherOrBoth::Both(val, width) => {
136                            v.push((val.as_ref().into(), Some(*width)))
137                        }
138                        EitherOrBoth::Left(val) => v.push((val.as_ref().into(), None)),
139                        EitherOrBoth::Right(_) => {
140                            unreachable!("given row len has been checked against widths len")
141                        }
142                    }
143                }
144                v
145            }
146        }
147    }
148}
149
150#[derive(Debug, Clone, Copy)]
151pub enum FontSize {
152    XxSmall,
153    XSmall,
154    Small,
155    Medium,
156    Large,
157    XLarge,
158    XxLarge,
159}
160
161impl AsRef<str> for FontSize {
162    fn as_ref(&self) -> &str {
163        match self {
164            FontSize::XxSmall => "xx-small",
165            FontSize::XSmall => "x-small",
166            FontSize::Small => "small",
167            FontSize::Medium => "medium",
168            FontSize::Large => "large",
169            FontSize::XLarge => "x-large",
170            FontSize::XxLarge => "xx-large",
171        }
172    }
173}
174
175/// Abstract styling that works for both terminal and HTML
176/// output. `color`, if given, is a ANSI 256-color terminal color.
177#[derive(Debug, Clone, Copy, Default)]
178pub struct OutputStyle {
179    pub faded: bool,
180    pub bold: bool,
181    pub italic: bool,
182    /// Only for HTML, ignored by the terminal backend.
183    pub font_size: Option<FontSize>,
184    pub color: Option<u8>,
185}
186
187#[derive(Debug, Clone, Copy)]
188pub enum BarKind {
189    Thin,
190    Thick,
191}
192
193pub trait OutputTable {
194    type Output;
195
196    /// How many columns this table has (each row has the same number
197    /// of columns, although cells can span multiple columns)
198    fn num_columns(&self) -> usize;
199
200    /// Normally, use `write_title_row` or `write_data_row` instead!
201    fn write_row<'url, V: CellValue<'url>>(
202        &mut self,
203        row: Row<V>,
204        line_style: Option<OutputStyle>,
205    ) -> anyhow::Result<()>;
206
207    fn write_title_row(
208        &mut self,
209        titles: &[OutputTableTitle],
210        line_style: Option<OutputStyle>,
211    ) -> anyhow::Result<()>;
212
213    fn write_data_row<'url, V: CellValue<'url>>(
214        &mut self,
215        data: &[V],
216        line_style: Option<OutputStyle>,
217    ) -> anyhow::Result<()> {
218        self.write_row(Row::PlainStrings(data), line_style)
219    }
220
221    fn write_bar(&mut self, bar_kind: BarKind, anchor_name: Option<&str>) -> anyhow::Result<()>;
222
223    fn print<'url, V: CellValue<'url>>(&mut self, value: V) -> anyhow::Result<()>;
224
225    fn finish(self) -> anyhow::Result<Self::Output>;
226}
227
228/// A text with optional link which is generated only when needed
229/// (i.e. for HTML output)
230#[derive(Clone)]
231pub struct WithUrlOnDemand<'s, 'url> {
232    pub text: &'s str,
233    // dyn because different columns might want different links
234    pub gen_url: Option<&'s dyn Fn() -> Option<Cow<'url, str>>>,
235    pub anchor_name: Option<KString>,
236}
237
238impl<'s, 'url> From<&'s str> for WithUrlOnDemand<'s, 'url> {
239    fn from(text: &'s str) -> Self {
240        WithUrlOnDemand {
241            text,
242            gen_url: None,
243            anchor_name: None,
244        }
245    }
246}
247
248impl<'s, 'url> AsRef<str> for WithUrlOnDemand<'s, 'url> {
249    fn as_ref(&self) -> &str {
250        self.text
251    }
252}
253
254impl<'s, 'url> CellValue<'url> for WithUrlOnDemand<'s, 'url> {
255    fn perhaps_url(&self) -> Option<Cow<'url, str>> {
256        if let Some(gen_url) = self.gen_url {
257            gen_url()
258        } else {
259            None
260        }
261    }
262    fn perhaps_anchor_name(&self) -> Option<&KString> {
263        self.anchor_name.as_ref()
264    }
265}