term_grid/
lib.rs

1// For the full copyright and license information, please view the LICENSE
2// file that was distributed with this source code.
3
4#![warn(future_incompatible)]
5#![warn(missing_copy_implementations)]
6#![warn(missing_docs)]
7#![warn(nonstandard_style)]
8#![warn(trivial_casts, trivial_numeric_casts)]
9#![warn(unused)]
10#![deny(unsafe_code)]
11#![doc = include_str!("../README.md")]
12
13use ansi_width::ansi_width;
14use std::fmt;
15
16/// Number of spaces in one \t.
17pub const SPACES_IN_TAB: usize = 8;
18
19/// Default size for separator in spaces.
20pub const DEFAULT_SEPARATOR_SIZE: usize = 2;
21
22/// Direction cells should be written in: either across or downwards.
23#[derive(PartialEq, Eq, Debug, Copy, Clone)]
24pub enum Direction {
25    /// Starts at the top left and moves rightwards, going back to the first
26    /// column for a new row, like a typewriter.
27    LeftToRight,
28
29    /// Starts at the top left and moves downwards, going back to the first
30    /// row for a new column, like how `ls` lists files by default.
31    TopToBottom,
32}
33
34/// The text to put in between each pair of columns.
35///
36/// This does not include any spaces used when aligning cells.
37#[derive(PartialEq, Eq, Debug)]
38pub enum Filling {
39    /// A number of spaces
40    Spaces(usize),
41
42    /// An arbitrary string
43    ///
44    /// `"|"` is a common choice.
45    Text(String),
46
47    /// Fill spaces with `\t`
48    Tabs {
49        /// A number of spaces
50        spaces: usize,
51        /// Size of `\t` in spaces
52        tab_size: usize,
53    },
54}
55
56impl Filling {
57    fn width(&self) -> usize {
58        match self {
59            Filling::Spaces(w) => *w,
60            Filling::Text(t) => ansi_width(t),
61            Filling::Tabs { spaces, .. } => *spaces,
62        }
63    }
64}
65
66/// The options for a grid view that should be passed to [`Grid::new`]
67#[derive(Debug)]
68pub struct GridOptions {
69    /// The direction that the cells should be written in
70    pub direction: Direction,
71
72    /// The string to put in between each column of cells
73    pub filling: Filling,
74
75    /// The width to fill with the grid
76    pub width: usize,
77}
78
79#[derive(PartialEq, Eq, Debug)]
80struct Dimensions {
81    /// The number of lines in the grid.
82    num_rows: usize,
83
84    /// The width of each column in the grid. The length of this vector serves
85    /// as the number of columns.
86    widths: Vec<usize>,
87}
88
89impl Dimensions {
90    fn total_width(&self, separator_width: usize) -> usize {
91        if self.widths.is_empty() {
92            0
93        } else {
94            let values = self.widths.iter().sum::<usize>();
95            let separators = separator_width * (self.widths.len() - 1);
96            values + separators
97        }
98    }
99}
100
101/// Everything needed to format the cells with the grid options.
102#[derive(Debug)]
103pub struct Grid<T: AsRef<str>> {
104    options: GridOptions,
105    cells: Vec<T>,
106    widths: Vec<usize>,
107    widest_cell_width: usize,
108    dimensions: Dimensions,
109}
110
111impl<T: AsRef<str>> Grid<T> {
112    /// Creates a new grid view with the given cells and options
113    pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
114        let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
115        let widest_cell_width = widths.iter().copied().max().unwrap_or(0);
116
117        let mut grid = Self {
118            options,
119            cells,
120            widths,
121            widest_cell_width,
122            dimensions: Dimensions {
123                num_rows: 0,
124                widths: Vec::new(),
125            },
126        };
127
128        if !grid.cells.is_empty() {
129            grid.dimensions = grid.width_dimensions();
130        }
131
132        grid
133    }
134
135    /// The number of terminal columns this display takes up, based on the separator
136    /// width and the number and width of the columns.
137    pub fn width(&self) -> usize {
138        self.dimensions.total_width(self.options.filling.width())
139    }
140
141    /// The number of rows this display takes up.
142    pub fn row_count(&self) -> usize {
143        self.dimensions.num_rows
144    }
145
146    /// The width of each column
147    pub fn column_widths(&self) -> &[usize] {
148        &self.dimensions.widths
149    }
150
151    /// Returns whether this display takes up as many columns as were allotted
152    /// to it.
153    ///
154    /// It’s possible to construct tables that don’t actually use up all the
155    /// columns that they could, such as when there are more columns than
156    /// cells! In this case, a column would have a width of zero. This just
157    /// checks for that.
158    pub fn is_complete(&self) -> bool {
159        self.dimensions.widths.iter().all(|&x| x > 0)
160    }
161
162    fn compute_dimensions(&self, num_lines: usize, num_columns: usize) -> Dimensions {
163        let mut column_widths = vec![0; num_columns];
164        for (index, cell_width) in self.widths.iter().copied().enumerate() {
165            let index = match self.options.direction {
166                Direction::LeftToRight => index % num_columns,
167                Direction::TopToBottom => index / num_lines,
168            };
169            if cell_width > column_widths[index] {
170                column_widths[index] = cell_width;
171            }
172        }
173
174        Dimensions {
175            num_rows: num_lines,
176            widths: column_widths,
177        }
178    }
179
180    fn width_dimensions(&mut self) -> Dimensions {
181        if self.cells.len() == 1 {
182            let cell_widths = self.widths[0];
183            return Dimensions {
184                num_rows: 1,
185                widths: vec![cell_widths],
186            };
187        }
188
189        // Calculate widest column size with separator.
190        let widest_column = self.widest_cell_width + self.options.filling.width();
191        // If it exceeds terminal's width, return, since it is impossible to fit.
192        if widest_column > self.options.width {
193            return Dimensions {
194                num_rows: self.cells.len(),
195                widths: vec![self.widest_cell_width],
196            };
197        }
198
199        // Calculate the number of columns if all columns had the size of the largest
200        // column. This is a lower bound on the number of columns.
201        let min_columns = self
202            .cells
203            .len()
204            .min((self.options.width + self.options.filling.width()) / widest_column);
205
206        // Calculate maximum number of lines and columns.
207        let max_rows = div_ceil(self.cells.len(), min_columns);
208
209        // This is a potential dimension, which can definitely fit all of the cells.
210        let mut potential_dimension = self.compute_dimensions(max_rows, min_columns);
211
212        // If all of the cells can be fit on one line, return immediately.
213        if max_rows == 1 {
214            return potential_dimension;
215        }
216
217        // Try to increase number of columns, to see if new dimension can still fit.
218        for num_columns in min_columns + 1..self.cells.len() {
219            let Some(adjusted_width) = self
220                .options
221                .width
222                .checked_sub((num_columns - 1) * self.options.filling.width())
223            else {
224                break;
225            };
226            let num_rows = div_ceil(self.cells.len(), num_columns);
227            let new_dimension = self.compute_dimensions(num_rows, num_columns);
228            if new_dimension.widths.iter().sum::<usize>() <= adjusted_width {
229                potential_dimension = new_dimension;
230            }
231        }
232
233        potential_dimension
234    }
235}
236
237impl<T: AsRef<str>> fmt::Display for Grid<T> {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
239        // If cells are empty then, nothing to print, skip.
240        if self.cells.is_empty() {
241            return Ok(());
242        }
243
244        let (tab_size, separator) = match &self.options.filling {
245            Filling::Spaces(n) => (0, " ".repeat(*n)),
246            Filling::Text(s) => (0, s.clone()),
247            Filling::Tabs { spaces, tab_size } => (*tab_size, " ".repeat(*spaces)),
248        };
249
250        // Initialize a buffer of spaces. The idea here is that any cell
251        // that needs padding gets a slice of this buffer of the needed
252        // size. This avoids the need of creating a string of spaces for
253        // each cell that needs padding.
254        //
255        // We overestimate how many spaces we need, but this is not
256        // part of the loop and it's therefore not super important to
257        // get exactly right.
258        let padding = " ".repeat(self.widest_cell_width + self.options.filling.width());
259
260        for y in 0..self.dimensions.num_rows {
261            // Current position on the line.
262            let mut cursor: usize = 0;
263            for x in 0..self.dimensions.widths.len() {
264                // Calculate position of the current element of the grid
265                // in cells and widths vectors and the offset to the next value.
266                let (current, offset) = match self.options.direction {
267                    Direction::LeftToRight => (y * self.dimensions.widths.len() + x, 1),
268                    Direction::TopToBottom => {
269                        (y + self.dimensions.num_rows * x, self.dimensions.num_rows)
270                    }
271                };
272
273                // Abandon a line mid-way through if that’s where the cells end.
274                if current >= self.cells.len() {
275                    break;
276                }
277
278                // Last in row checks only the predefined grid width.
279                // It does not check if there will be more entries.
280                // For this purpose we define next value as well.
281                // This prevents printing separator after the actual last value in a row.
282                let last_in_row = x == self.dimensions.widths.len() - 1;
283                let contents = &self.cells[current];
284                let width = self.widths[current];
285                let col_width = self.dimensions.widths[x];
286                let padding_size = col_width - width;
287
288                // The final column doesn’t need to have trailing spaces,
289                // as long as it’s left-aligned.
290                //
291                // We use write_str directly instead of a the write! macro to
292                // avoid some of the formatting overhead. For example, if we pad
293                // using `write!("{contents:>width}")`, the unicode width will
294                // have to be independently calculated by the macro, which is slow and
295                // redundant because we already know the width.
296                //
297                // For the padding, we instead slice into a buffer of spaces defined
298                // above, so we don't need to call `" ".repeat(n)` each loop.
299                // We also only call `write_str` when we actually need padding as
300                // another optimization.
301                f.write_str(contents.as_ref())?;
302
303                // In case this entry was the last on the current line,
304                // there is no need to print the separator and padding.
305                if last_in_row || current + offset >= self.cells.len() {
306                    break;
307                }
308
309                // Special case if tab size was not set. Fill with spaces and separator.
310                if tab_size == 0 {
311                    f.write_str(&padding[..padding_size])?;
312                    f.write_str(&separator)?;
313                } else {
314                    // Move cursor to the end of the current contents.
315                    cursor += width;
316                    let total_spaces = padding_size + self.options.filling.width();
317                    // The size of \t can be inconsistent in terminal.
318                    // Tab stops are relative to the cursor position e.g.,
319                    //  * cursor = 0, \t moves to column 8;
320                    //  * cursor = 5, \t moves to column 8 (3 spaces);
321                    //  * cursor = 9, \t moves to column 16 (7 spaces).
322                    // Calculate the nearest \t position in relation to cursor.
323                    let closest_tab = tab_size - (cursor % tab_size);
324
325                    if closest_tab > total_spaces {
326                        f.write_str(&padding[..total_spaces])?;
327                    } else {
328                        let rest_spaces = total_spaces - closest_tab;
329                        let tabs = 1 + (rest_spaces / tab_size);
330                        let spaces = rest_spaces % tab_size;
331                        f.write_str(&"\t".repeat(tabs))?;
332                        f.write_str(&padding[..spaces])?;
333                    }
334
335                    cursor += total_spaces;
336                }
337            }
338            f.write_str("\n")?;
339        }
340
341        Ok(())
342    }
343}
344
345// Adapted from the unstable API:
346// https://doc.rust-lang.org/std/primitive.usize.html#method.div_ceil
347// Can be removed on MSRV 1.73.
348/// Division with upward rounding
349pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
350    let d = lhs / rhs;
351    let r = lhs % rhs;
352    if r > 0 && rhs > 0 {
353        d + 1
354    } else {
355        d
356    }
357}