ascii_table/
lib.rs

1//! Print ASCII tables to the terminal.
2//!
3//! # Example
4//!
5//! ```
6//! use ascii_table::AsciiTable;
7//!
8//! let ascii_table = AsciiTable::default();
9//! let data = vec![&[1, 2, 3], &[4, 5, 6], &[7, 8, 9]];
10//! ascii_table.print(data);
11//! // ┌───┬───┬───┐
12//! // │ 1 │ 2 │ 3 │
13//! // │ 4 │ 5 │ 6 │
14//! // │ 7 │ 8 │ 9 │
15//! // └───┴───┴───┘
16//! ```
17//!
18//! # Example
19//!
20//! ```
21//! use std::fmt::Display;
22//! use ascii_table::{AsciiTable, Align};
23//!
24//! let mut ascii_table = AsciiTable::default();
25//! ascii_table.set_max_width(26);
26//! ascii_table.column(0).set_header("H1").set_align(Align::Left);
27//! ascii_table.column(1).set_header("H2").set_align(Align::Center);
28//! ascii_table.column(2).set_header("H3").set_align(Align::Right);
29//!
30//! let data: Vec<Vec<&dyn Display>> = vec![
31//!     vec![&'v', &'v', &'v'],
32//!     vec![&123, &456, &789, &"abcdef"]
33//! ];
34//! ascii_table.print(data);
35//! // ┌─────┬─────┬─────┬──────┐
36//! // │ H1  │ H2  │ H3  │      │
37//! // ├─────┼─────┼─────┼──────┤
38//! // │ v   │  v  │   v │      │
39//! // │ 123 │ 456 │ 789 │ abc+ │
40//! // └─────┴─────┴─────┴──────┘
41//! ```
42//!
43//! ## Features
44//!
45//! - `auto_table_width`: Sets the default max width of the ascii table to the width of the terminal.
46//! - `color_codes`: Correctly calculates the width of a string when terminal color codes are present
47//!   (like those from the `colorful` crate).
48//! - `wide_characters`: Correctly calculates the width of a string when wide characters are present
49//!   (like emoji's).
50
51#[cfg(test)]
52mod tests;
53
54#[cfg(feature = "color_codes")]
55use ::regex::Regex;
56#[cfg(feature = "wide_characters")]
57use ::unicode_width::UnicodeWidthStr;
58
59use ::std::collections::BTreeMap;
60use ::std::fmt::Display;
61#[cfg(feature = "color_codes")]
62use ::std::sync::LazyLock;
63
64const DEFAULT_WIDTH: usize = 100;
65const SE: &str = "┌";
66const NW: &str = "┘";
67const SW: &str = "┐";
68const NS: &str = "│";
69const NE: &str = "└";
70const EWS: &str = "┬";
71const NES: &str = "├";
72const NWS: &str = "┤";
73const NEW: &str = "┴";
74const NEWS: &str = "┼";
75const EW: &str = "─";
76
77#[cfg(feature = "color_codes")]
78static COLOR_CODE_PARSER: LazyLock<Regex> =
79    LazyLock::new(|| Regex::new("\u{1b}\\[([0-9]+;)*[0-9]+m").expect("Regex compilation error"));
80
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
82pub struct AsciiTable {
83    max_width: Option<usize>,
84    columns: BTreeMap<usize, Column>,
85}
86
87impl AsciiTable {
88    /// Sets the maximum width of the table.
89    ///
90    /// When you use the feature `auto_table_width` the maximum width will be calculated when you
91    /// render the table. Note that the value set by this function will take precedence over the value
92    /// generated by `auto_table_width`.
93    pub fn set_max_width(&mut self, max_width: usize) -> &mut Self {
94        self.max_width = Some(max_width);
95        self
96    }
97
98    /// Gets the maximum width used to render tables. This is either the default width, the width calculated
99    /// by the feature `auto_table_width` or the width specified by `set_max_width`.
100    pub fn max_width(&self) -> usize {
101        match self.max_width {
102            Some(width) => width,
103            None => default_table_width(),
104        }
105    }
106
107    pub fn column(&mut self, index: usize) -> &mut Column {
108        self.columns.entry(index).or_default()
109    }
110}
111
112#[cfg(feature = "auto_table_width")]
113fn default_table_width() -> usize {
114    ::termsize::get()
115        .map(|size| size.cols.into())
116        .unwrap_or(DEFAULT_WIDTH)
117}
118
119#[cfg(not(feature = "auto_table_width"))]
120fn default_table_width() -> usize {
121    DEFAULT_WIDTH
122}
123
124#[derive(Clone, Debug, Eq, PartialEq)]
125pub struct Column {
126    header: String,
127    align: Align,
128    max_width: usize,
129}
130
131impl Column {
132    /// Sets the value for the header row. When none of the table's columns has a header value then
133    /// the header row wont be rendered.
134    pub fn set_header<T>(&mut self, header: T) -> &mut Self
135    where
136        T: Into<String>,
137    {
138        self.header = header.into();
139        self
140    }
141
142    pub fn header(&self) -> &str {
143        &self.header
144    }
145
146    pub fn set_align(&mut self, align: Align) -> &mut Self {
147        self.align = align;
148        self
149    }
150
151    pub fn align(&self) -> Align {
152        self.align
153    }
154
155    /// Sets the maximum width of the content (rendered text) for this column.
156    ///
157    /// The maximum width of the table takes precedence over the maximum width of its columns. So you
158    /// can't use the columns maximum width to extends the table beyond its maximum width.
159    pub fn set_max_width(&mut self, max_width: usize) -> &mut Self {
160        self.max_width = max_width;
161        self
162    }
163
164    pub fn max_width(&self) -> usize {
165        self.max_width
166    }
167}
168
169impl Default for Column {
170    fn default() -> Self {
171        Column {
172            header: Default::default(),
173            align: Default::default(),
174            max_width: usize::max_value(),
175        }
176    }
177}
178
179/// Alignment of text in a cell.
180#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
181pub enum Align {
182    #[default]
183    Left,
184    Center,
185    Right,
186}
187
188impl AsciiTable {
189    pub fn print<L1, L2, T>(&self, data: L1)
190    where
191        L1: IntoIterator<Item = L2>,
192        L2: IntoIterator<Item = T>,
193        T: Display,
194    {
195        print!("{}", self.format(data))
196    }
197
198    pub fn format<L1, L2, T>(&self, data: L1) -> String
199    where
200        L1: IntoIterator<Item = L2>,
201        L2: IntoIterator<Item = T>,
202        T: Display,
203    {
204        self.format_inner(self.stringify(data))
205    }
206
207    fn format_inner(&self, data: Vec<Vec<SmartString>>) -> String {
208        let num_cols = data.iter().map(|row| row.len()).max().unwrap_or(0);
209        let table_max_width = self.max_width();
210        if !self.valid(&data, num_cols, table_max_width) {
211            return self.format_empty();
212        }
213
214        let header = self.stringify_header(num_cols);
215        let data = self.square_data(data, num_cols);
216        let has_header = header.iter().any(|text| !text.is_empty());
217        let widths = self.column_widths(&header, &data, table_max_width);
218
219        let mut result = String::new();
220        result.push_str(&self.format_first(&widths));
221        if has_header {
222            result.push_str(&self.format_header_row(&header, &widths));
223            result.push_str(&self.format_middle(&widths));
224        }
225        for row in data {
226            result.push_str(&self.format_row(&row, &widths));
227        }
228        result.push_str(&self.format_last(&widths));
229        result
230    }
231
232    fn valid(&self, data: &Vec<Vec<SmartString>>, num_cols: usize, table_max_width: usize) -> bool {
233        if data.len() == 0 {
234            false
235        } else if num_cols == 0 {
236            false
237        } else if table_max_width < Self::smallest_width(num_cols) {
238            false
239        } else {
240            true
241        }
242    }
243
244    fn smallest_width(num_cols: usize) -> usize {
245        ((num_cols - 1) * 3) + 4
246    }
247
248    fn stringify<L1, L2, T>(&self, data: L1) -> Vec<Vec<SmartString>>
249    where
250        L1: IntoIterator<Item = L2>,
251        L2: IntoIterator<Item = T>,
252        T: Display,
253    {
254        data.into_iter()
255            .map(|row| {
256                row.into_iter()
257                    .map(|cell| SmartString::from(cell.to_string()))
258                    .collect()
259            })
260            .collect()
261    }
262
263    fn stringify_header(&self, num_cols: usize) -> Vec<SmartString> {
264        (0..num_cols)
265            .map(|n| {
266                let value = self
267                    .columns
268                    .get(&n)
269                    .map(|column| column.header.as_str())
270                    .unwrap_or("");
271                SmartString::from(value)
272            })
273            .collect()
274    }
275
276    fn square_data(
277        &self,
278        mut data: Vec<Vec<SmartString>>,
279        num_cols: usize,
280    ) -> Vec<Vec<SmartString>> {
281        for row in &mut data {
282            while row.len() < num_cols {
283                row.push(SmartString::new());
284            }
285        }
286        data
287    }
288
289    fn column_widths(
290        &self,
291        header: &[SmartString],
292        data: &[Vec<SmartString>],
293        table_max_width: usize,
294    ) -> Vec<usize> {
295        let default_conf = &Default::default();
296        let result: Vec<_> = (0..header.len())
297            .map(|n| {
298                let conf = self.columns.get(&n).unwrap_or(default_conf);
299                let column_width = data.iter().map(|row| row[n].width()).max().unwrap();
300                let header_width = header[n].width();
301                column_width.max(header_width).min(conf.max_width)
302            })
303            .collect();
304        self.truncate_widths(result, table_max_width)
305    }
306
307    fn truncate_widths(&self, mut widths: Vec<usize>, table_max_width: usize) -> Vec<usize> {
308        let table_padding = Self::smallest_width(widths.len());
309        while widths.iter().sum::<usize>() + table_padding > table_max_width
310            && *widths.iter().max().unwrap() > 0
311        {
312            let max = widths.iter().max().unwrap();
313            let idx = widths.iter().rposition(|x| x == max).unwrap();
314            widths[idx] -= 1;
315        }
316        widths
317    }
318
319    fn format_line(&self, row: &[SmartString], head: &str, delim: &str, tail: &str) -> String {
320        let mut result = String::new();
321        result.push_str(head);
322        for cell in row {
323            result.push_str(&format!("{}{}", cell, delim));
324        }
325        for _ in 0..delim.chars().count() {
326            result.pop();
327        }
328        result.push_str(tail);
329        result.push('\n');
330        result
331    }
332
333    fn format_empty(&self) -> String {
334        self.format_first(&vec![0])
335            + &self.format_line(
336                &[SmartString::new()],
337                &format!("{}{}", NS, ' '),
338                &format!("{}{}{}", ' ', NS, ' '),
339                &format!("{}{}", ' ', NS),
340            )
341            + &self.format_last(&[0])
342    }
343
344    fn format_first(&self, widths: &[usize]) -> String {
345        let row: Vec<_> = widths
346            .iter()
347            .map(|&x| SmartString::from_visible(EW.repeat(x)))
348            .collect();
349        self.format_line(
350            &row,
351            &format!("{}{}", SE, EW),
352            &format!("{}{}{}", EW, EWS, EW),
353            &format!("{}{}", EW, SW),
354        )
355    }
356
357    fn format_middle(&self, widths: &[usize]) -> String {
358        let row: Vec<_> = widths
359            .iter()
360            .map(|&x| SmartString::from_visible(EW.repeat(x)))
361            .collect();
362        self.format_line(
363            &row,
364            &format!("{}{}", NES, EW),
365            &format!("{}{}{}", EW, NEWS, EW),
366            &format!("{}{}", EW, NWS),
367        )
368    }
369
370    fn format_row(&self, row: &[SmartString], widths: &[usize]) -> String {
371        let default_conf = &Default::default();
372        let row: Vec<_> = (0..widths.len())
373            .map(|a| {
374                let cell = &row[a];
375                let width = widths[a];
376                let conf = self.columns.get(&a).unwrap_or(default_conf);
377                self.format_cell(cell, width, ' ', conf.align)
378            })
379            .collect();
380        self.format_line(
381            &row,
382            &format!("{}{}", NS, ' '),
383            &format!("{}{}{}", ' ', NS, ' '),
384            &format!("{}{}", ' ', NS),
385        )
386    }
387
388    fn format_header_row(&self, row: &[SmartString], widths: &[usize]) -> String {
389        let row: Vec<_> = row
390            .iter()
391            .zip(widths.iter())
392            .map(|(cell, &width)| self.format_cell(cell, width, ' ', Align::Left))
393            .collect();
394        self.format_line(
395            &row,
396            &format!("{}{}", NS, ' '),
397            &format!("{}{}{}", ' ', NS, ' '),
398            &format!("{}{}", ' ', NS),
399        )
400    }
401
402    fn format_last(&self, widths: &[usize]) -> String {
403        let row: Vec<_> = widths
404            .iter()
405            .map(|&x| SmartString::from_visible(EW.repeat(x)))
406            .collect();
407        self.format_line(
408            &row,
409            &format!("{}{}", NE, EW),
410            &format!("{}{}{}", EW, NEW, EW),
411            &format!("{}{}", EW, NW),
412        )
413    }
414
415    fn format_cell(&self, text: &SmartString, len: usize, pad: char, align: Align) -> SmartString {
416        if text.width() > len {
417            let mut result = text.clone();
418            while result.width() > len {
419                result.pop();
420            }
421            if result.pop().is_some() {
422                result.push_visible('+')
423            }
424            result
425        } else {
426            let mut result = text.clone();
427            match align {
428                Align::Left => {
429                    while result.width() < len {
430                        result.push_visible(pad)
431                    }
432                }
433                Align::Right => {
434                    while result.width() < len {
435                        result.lpush_visible(pad)
436                    }
437                }
438                Align::Center => {
439                    while result.width() < len {
440                        result.push_visible(pad);
441                        if result.width() < len {
442                            result.lpush_visible(pad)
443                        }
444                    }
445                }
446            }
447            result
448        }
449    }
450}
451
452#[derive(Clone, Debug)]
453struct SmartString {
454    fragments: Vec<SmartStringFragment>,
455}
456
457#[derive(Clone, Debug)]
458struct SmartStringFragment {
459    string: String,
460    visible: bool,
461}
462
463impl SmartString {
464    fn new() -> Self {
465        Self {
466            fragments: Vec::new(),
467        }
468    }
469
470    #[cfg(feature = "color_codes")]
471    fn from<T>(string: T) -> Self
472    where
473        T: AsRef<str>,
474    {
475        let string = string.as_ref();
476        let mut fragments = Vec::new();
477        let mut last = 0;
478        for r#match in COLOR_CODE_PARSER.find_iter(string) {
479            let start = r#match.start();
480            let end = r#match.end();
481
482            if last < start {
483                fragments.push(SmartStringFragment::new(&string[last..start], true));
484            }
485            fragments.push(SmartStringFragment::new(&string[start..end], false));
486
487            last = end;
488        }
489
490        if last < string.len() {
491            fragments.push(SmartStringFragment::new(&string[last..], true));
492        }
493
494        Self { fragments }
495    }
496
497    #[cfg(not(feature = "color_codes"))]
498    fn from<T>(string: T) -> Self
499    where
500        T: Into<String>,
501    {
502        Self {
503            fragments: vec![SmartStringFragment::new(string, true)],
504        }
505    }
506
507    fn from_visible<T>(string: T) -> Self
508    where
509        T: Into<String>,
510    {
511        Self {
512            fragments: vec![SmartStringFragment::new(string, true)],
513        }
514    }
515
516    fn width(&self) -> usize {
517        self.fragments
518            .iter()
519            .filter(|fragment| fragment.visible)
520            .map(|fragment| fragment.width())
521            .sum()
522    }
523
524    fn is_empty(&self) -> bool {
525        self.fragments
526            .iter()
527            .filter(|fragment| fragment.visible)
528            .all(|fragment| fragment.string.is_empty())
529    }
530
531    fn pop(&mut self) -> Option<char> {
532        self.fragments
533            .iter_mut()
534            .filter(|fragment| fragment.visible && !fragment.string.is_empty())
535            .last()
536            .and_then(|fragment| fragment.string.pop())
537    }
538
539    fn push_visible(&mut self, ch: char) {
540        let last_fragment = self
541            .fragments
542            .iter_mut()
543            .filter(|fragment| fragment.visible)
544            .map(|fragment| &mut fragment.string)
545            .last();
546        if let Some(fragment) = last_fragment {
547            fragment.push(ch);
548        } else {
549            self.fragments.push(SmartStringFragment::new(ch, true));
550        }
551    }
552
553    fn lpush_visible(&mut self, ch: char) {
554        let first_fragment = self
555            .fragments
556            .iter_mut()
557            .filter(|fragment| fragment.visible)
558            .map(|fragment| &mut fragment.string)
559            .next();
560        if let Some(fragment) = first_fragment {
561            fragment.insert(0, ch);
562        } else {
563            self.fragments.insert(0, SmartStringFragment::new(ch, true));
564        }
565    }
566}
567
568impl Display for SmartString {
569    fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
570        self.fragments
571            .iter()
572            .try_for_each(|fragment| fragment.string.fmt(fmt))
573    }
574}
575
576impl SmartStringFragment {
577    fn new<T>(string: T, visible: bool) -> Self
578    where
579        T: Into<String>,
580    {
581        Self {
582            string: string.into(),
583            visible,
584        }
585    }
586
587    #[cfg(feature = "wide_characters")]
588    fn width(&self) -> usize {
589        UnicodeWidthStr::width(self.string.as_str())
590    }
591
592    #[cfg(not(feature = "wide_characters"))]
593    fn width(&self) -> usize {
594        self.string.chars().count()
595    }
596}