cli_table/
utils.rs

1use std::io::{Result, Write};
2
3use termcolor::{ColorSpec, WriteColor};
4
5use crate::{
6    buffers::Buffers,
7    table::{Dimension as TableDimension, HorizontalLine, TableFormat, VerticalLine},
8};
9
10const ESC: char = '\x1b';
11
12/// NOTE: `display_width()` is ported from https://docs.rs/ansi-width/0.1.0/src/ansi_width/lib.rs.html#9-55
13///
14/// Return the display width of a unicode string.
15/// This functions takes ANSI-escaped color codes into account.
16pub(crate) fn display_width(text: &str) -> usize {
17    let mut width = 0;
18    let mut chars = text.chars();
19
20    // This lint is a false positive, because we use the iterator later, leading to
21    // ownership issues if we follow the lint.
22    #[allow(clippy::while_let_on_iterator)]
23    while let Some(c) = chars.next() {
24        // ESC starts escape sequences, so we need to take characters until the
25        // end of the escape sequence.
26        if c == ESC {
27            let Some(c) = chars.next() else {
28                break;
29            };
30            match c {
31                // String terminator character: ends other sequences
32                // We probably won't encounter this but it's here for completeness.
33                // Or for if we get passed invalid codes.
34                '\\' => {
35                    // ignore
36                }
37                // Control Sequence Introducer: continue until `\x40-\x7C`
38                '[' => while !matches!(chars.next(), Some('\x40'..='\x7C') | None) {},
39                // Operating System Command: continue until ST
40                ']' => {
41                    let mut last = c;
42                    while let Some(new) = chars.next() {
43                        if new == '\x07' || (new == '\\' && last == ESC) {
44                            break;
45                        }
46                        last = new;
47                    }
48                }
49                // We don't know what character it is, best bet is to fall back to unicode width
50                // The ESC is assumed to have 0 width in this case.
51                _ => {
52                    width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
53                }
54            }
55        } else {
56            // If it's a normal character outside an escape sequence, use the
57            // unicode width.
58            width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
59        }
60    }
61    width
62}
63
64pub fn transpose<T>(v: Vec<Vec<T>>) -> Vec<Vec<T>> {
65    if v.is_empty() || v[0].is_empty() {
66        return v;
67    }
68
69    let columns = v.len();
70    let height = v[0].len();
71
72    let mut optional_v: Vec<Vec<Option<T>>> = v
73        .into_iter()
74        .map(|cell_v| cell_v.into_iter().map(Some).collect())
75        .collect();
76
77    let mut transpose = Vec::with_capacity(height);
78
79    for i in 0..height {
80        let mut row_buffer = Vec::with_capacity(columns);
81
82        for inner_v in optional_v.iter_mut().take(columns) {
83            row_buffer.push(std::mem::take(&mut inner_v[i]).unwrap());
84        }
85
86        transpose.push(row_buffer);
87    }
88
89    transpose
90}
91
92pub(crate) fn print_horizontal_line(
93    buffers: &mut Buffers<'_>,
94    line: Option<&HorizontalLine>,
95    table_dimension: &TableDimension,
96    table_format: &TableFormat,
97    color_spec: &ColorSpec,
98) -> Result<()> {
99    if let Some(line) = line {
100        if table_format.border.left.is_some() {
101            print_char(buffers, line.left_end, color_spec)?;
102        }
103
104        let mut widths = table_dimension.widths.iter().peekable();
105
106        while let Some(width) = widths.next() {
107            let s = std::iter::repeat_n(line.filler, width + 2).collect::<String>();
108            print_str(buffers, &s, color_spec)?;
109
110            match widths.peek() {
111                Some(_) => {
112                    if table_format.separator.column.is_some() {
113                        print_char(buffers, line.junction, color_spec)?
114                    }
115                }
116                None => {
117                    if table_format.border.right.is_some() {
118                        print_char(buffers, line.right_end, color_spec)?;
119                    } else {
120                        print_str(buffers, "", color_spec)?;
121                    }
122                }
123            }
124        }
125
126        println(buffers)?;
127    }
128
129    Ok(())
130}
131
132pub(crate) fn print_vertical_line(
133    buffers: &mut Buffers<'_>,
134    line: Option<&VerticalLine>,
135    color_spec: &ColorSpec,
136) -> Result<()> {
137    if let Some(line) = line {
138        print_char(buffers, line.filler, color_spec)?;
139    }
140    Ok(())
141}
142
143pub(crate) fn print_str(buffers: &mut Buffers<'_>, s: &str, color_spec: &ColorSpec) -> Result<()> {
144    buffers.set_color(color_spec)?;
145    write!(buffers, "{}", s)?;
146    buffers.reset()
147}
148
149pub(crate) fn print_char(buffers: &mut Buffers<'_>, c: char, color_spec: &ColorSpec) -> Result<()> {
150    buffers.set_color(color_spec)?;
151    write!(buffers, "{}", c)?;
152    buffers.reset()
153}
154
155pub(crate) fn println(buffers: &mut Buffers<'_>) -> Result<()> {
156    writeln!(buffers)
157}