Skip to main content

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            Self::Spaces(w) => *w,
60            Self::Text(t) => ansi_width(t),
61            Self::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    #[must_use]
114    pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
115        let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
116        let widest_cell_width = widths.iter().copied().max().unwrap_or(0);
117
118        let mut grid = Self {
119            options,
120            cells,
121            widths,
122            widest_cell_width,
123            dimensions: Dimensions {
124                num_rows: 0,
125                widths: Vec::new(),
126            },
127        };
128
129        if !grid.cells.is_empty() {
130            grid.dimensions = grid.width_dimensions();
131        }
132
133        grid
134    }
135
136    /// The number of terminal columns this display takes up, based on the separator
137    /// width and the number and width of the columns.
138    #[must_use]
139    pub fn width(&self) -> usize {
140        self.dimensions.total_width(self.options.filling.width())
141    }
142
143    /// The number of rows this display takes up.
144    #[must_use]
145    pub const fn row_count(&self) -> usize {
146        self.dimensions.num_rows
147    }
148
149    /// The width of each column
150    #[must_use]
151    pub fn column_widths(&self) -> &[usize] {
152        &self.dimensions.widths
153    }
154
155    /// Returns whether this display takes up as many columns as were allotted
156    /// to it.
157    ///
158    /// It's possible to construct tables that don't actually use up all the
159    /// columns that they could, such as when there are more columns than
160    /// cells! In this case, a column would have a width of zero. This just
161    /// checks for that.
162    #[must_use]
163    pub fn is_complete(&self) -> bool {
164        self.dimensions.widths.iter().all(|&x| x > 0)
165    }
166
167    fn compute_dimensions(&self, num_lines: usize, num_columns: usize) -> Dimensions {
168        let mut column_widths = vec![0; num_columns];
169        for (index, cell_width) in self.widths.iter().copied().enumerate() {
170            let index = match self.options.direction {
171                Direction::LeftToRight => index % num_columns,
172                Direction::TopToBottom => index / num_lines,
173            };
174            if cell_width > column_widths[index] {
175                column_widths[index] = cell_width;
176            }
177        }
178
179        Dimensions {
180            num_rows: num_lines,
181            widths: column_widths,
182        }
183    }
184
185    fn width_dimensions(&self) -> Dimensions {
186        if self.cells.len() == 1 {
187            let cell_widths = self.widths[0];
188            return Dimensions {
189                num_rows: 1,
190                widths: vec![cell_widths],
191            };
192        }
193
194        // Calculate widest column size with separator.
195        let widest_column = self.widest_cell_width + self.options.filling.width();
196        // If it exceeds terminal's width, return, since it is impossible to fit.
197        if widest_column > self.options.width {
198            return Dimensions {
199                num_rows: self.cells.len(),
200                widths: vec![self.widest_cell_width],
201            };
202        }
203
204        // Calculate the number of columns if all columns had the size of the largest
205        // column. This is a lower bound on the number of columns.
206        let min_columns = self
207            .cells
208            .len()
209            .min((self.options.width + self.options.filling.width()) / widest_column);
210
211        // Calculate maximum number of lines and columns.
212        let max_rows = div_ceil(self.cells.len(), min_columns);
213
214        // This is a potential dimension, which can definitely fit all of the cells.
215        let mut potential_dimension = self.compute_dimensions(max_rows, min_columns);
216
217        // If all of the cells can be fit on one line, return immediately.
218        if max_rows == 1 {
219            return potential_dimension;
220        }
221
222        // Try to increase number of columns, to see if new dimension can still fit.
223        for num_columns in min_columns + 1..=self.cells.len() {
224            let Some(adjusted_width) = self
225                .options
226                .width
227                .checked_sub((num_columns - 1) * self.options.filling.width())
228            else {
229                break;
230            };
231            let num_rows = div_ceil(self.cells.len(), num_columns);
232            let new_dimension = self.compute_dimensions(num_rows, num_columns);
233            if new_dimension.widths.iter().sum::<usize>() <= adjusted_width {
234                potential_dimension = new_dimension;
235            }
236        }
237
238        potential_dimension
239    }
240}
241
242impl<T: AsRef<str>> fmt::Display for Grid<T> {
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
244        // If cells are empty then, nothing to print, skip.
245        if self.cells.is_empty() {
246            return Ok(());
247        }
248
249        let (tab_size, separator) = match &self.options.filling {
250            Filling::Spaces(n) => (0, " ".repeat(*n)),
251            Filling::Text(s) => (0, s.clone()),
252            Filling::Tabs { spaces, tab_size } => (*tab_size, " ".repeat(*spaces)),
253        };
254
255        // Initialize a buffer of spaces. The idea here is that any cell
256        // that needs padding gets a slice of this buffer of the needed
257        // size. This avoids the need of creating a string of spaces for
258        // each cell that needs padding.
259        //
260        // We overestimate how many spaces we need, but this is not
261        // part of the loop and it's therefore not super important to
262        // get exactly right.
263        let padding = " ".repeat(self.widest_cell_width + self.options.filling.width());
264
265        for y in 0..self.dimensions.num_rows {
266            // Current position on the line.
267            let mut cursor: usize = 0;
268            for x in 0..self.dimensions.widths.len() {
269                // Calculate position of the current element of the grid
270                // in cells and widths vectors and the offset to the next value.
271                let (current, offset) = match self.options.direction {
272                    Direction::LeftToRight => (y * self.dimensions.widths.len() + x, 1),
273                    Direction::TopToBottom => {
274                        (y + self.dimensions.num_rows * x, self.dimensions.num_rows)
275                    }
276                };
277
278                // Abandon a line mid-way through if that’s where the cells end.
279                if current >= self.cells.len() {
280                    break;
281                }
282
283                // Last in row checks only the predefined grid width.
284                // It does not check if there will be more entries.
285                // For this purpose we define next value as well.
286                // This prevents printing separator after the actual last value in a row.
287                let last_in_row = x == self.dimensions.widths.len() - 1;
288                let contents = &self.cells[current];
289                let width = self.widths[current];
290                let col_width = self.dimensions.widths[x];
291                let padding_size = col_width - width;
292
293                // The final column doesn’t need to have trailing spaces,
294                // as long as it’s left-aligned.
295                //
296                // We use write_str directly instead of a the write! macro to
297                // avoid some of the formatting overhead. For example, if we pad
298                // using `write!("{contents:>width}")`, the unicode width will
299                // have to be independently calculated by the macro, which is slow and
300                // redundant because we already know the width.
301                //
302                // For the padding, we instead slice into a buffer of spaces defined
303                // above, so we don't need to call `" ".repeat(n)` each loop.
304                // We also only call `write_str` when we actually need padding as
305                // another optimization.
306                f.write_str(contents.as_ref())?;
307
308                // In case this entry was the last on the current line,
309                // there is no need to print the separator and padding.
310                if last_in_row || current + offset >= self.cells.len() {
311                    break;
312                }
313
314                // Special case if tab size was not set. Fill with spaces and separator.
315                if tab_size == 0 {
316                    f.write_str(&padding[..padding_size])?;
317                    f.write_str(&separator)?;
318                } else {
319                    // Move cursor to the end of the current contents.
320                    cursor += width;
321                    let total_spaces = padding_size + self.options.filling.width();
322                    // The size of \t can be inconsistent in terminal.
323                    // Tab stops are relative to the cursor position e.g.,
324                    //  * cursor = 0, \t moves to column 8;
325                    //  * cursor = 5, \t moves to column 8 (3 spaces);
326                    //  * cursor = 9, \t moves to column 16 (7 spaces).
327                    // Calculate the nearest \t position in relation to cursor.
328                    let closest_tab = tab_size - (cursor % tab_size);
329
330                    if closest_tab > total_spaces {
331                        f.write_str(&padding[..total_spaces])?;
332                    } else {
333                        let rest_spaces = total_spaces - closest_tab;
334                        let tabs = 1 + (rest_spaces / tab_size);
335                        let spaces = rest_spaces % tab_size;
336                        f.write_str(&"\t".repeat(tabs))?;
337                        f.write_str(&padding[..spaces])?;
338                    }
339
340                    cursor += total_spaces;
341                }
342            }
343            f.write_str("\n")?;
344        }
345
346        Ok(())
347    }
348}
349
350// Adapted from the unstable API:
351// https://doc.rust-lang.org/std/primitive.usize.html#method.div_ceil
352// Can be removed on MSRV 1.73.
353/// Division with upward rounding
354#[must_use]
355pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
356    let d = lhs / rhs;
357    let r = lhs % rhs;
358    if r > 0 && rhs > 0 {
359        d + 1
360    } else {
361        d
362    }
363}