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/// Direction cells should be written in: either across or downwards.
17#[derive(PartialEq, Eq, Debug, Copy, Clone)]
18pub enum Direction {
19    /// Starts at the top left and moves rightwards, going back to the first
20    /// column for a new row, like a typewriter.
21    LeftToRight,
22
23    /// Starts at the top left and moves downwards, going back to the first
24    /// row for a new column, like how `ls` lists files by default.
25    TopToBottom,
26}
27
28/// The text to put in between each pair of columns.
29///
30/// This does not include any spaces used when aligning cells.
31#[derive(PartialEq, Eq, Debug)]
32pub enum Filling {
33    /// A number of spaces
34    Spaces(usize),
35
36    /// An arbitrary string
37    ///
38    /// `"|"` is a common choice.
39    Text(String),
40}
41
42impl Filling {
43    fn width(&self) -> usize {
44        match self {
45            Filling::Spaces(w) => *w,
46            Filling::Text(t) => ansi_width(t),
47        }
48    }
49}
50
51/// The options for a grid view that should be passed to [`Grid::new`]
52#[derive(Debug)]
53pub struct GridOptions {
54    /// The direction that the cells should be written in
55    pub direction: Direction,
56
57    /// The string to put in between each column of cells
58    pub filling: Filling,
59
60    /// The width to fill with the grid
61    pub width: usize,
62}
63
64#[derive(PartialEq, Eq, Debug)]
65struct Dimensions {
66    /// The number of lines in the grid.
67    num_lines: usize,
68
69    /// The width of each column in the grid. The length of this vector serves
70    /// as the number of columns.
71    widths: Vec<usize>,
72}
73
74impl Dimensions {
75    fn total_width(&self, separator_width: usize) -> usize {
76        if self.widths.is_empty() {
77            0
78        } else {
79            let values = self.widths.iter().sum::<usize>();
80            let separators = separator_width * (self.widths.len() - 1);
81            values + separators
82        }
83    }
84}
85
86/// Everything needed to format the cells with the grid options.
87#[derive(Debug)]
88pub struct Grid<T: AsRef<str>> {
89    options: GridOptions,
90    cells: Vec<T>,
91    widths: Vec<usize>,
92    widest_cell_width: usize,
93    dimensions: Dimensions,
94}
95
96impl<T: AsRef<str>> Grid<T> {
97    /// Creates a new grid view with the given cells and options
98    pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
99        let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
100        let widest_cell_width = widths.iter().copied().max().unwrap_or(0);
101        let width = options.width;
102
103        let mut grid = Self {
104            options,
105            cells,
106            widths,
107            widest_cell_width,
108            dimensions: Dimensions {
109                num_lines: 0,
110                widths: Vec::new(),
111            },
112        };
113
114        grid.dimensions = grid.width_dimensions(width).unwrap_or(Dimensions {
115            num_lines: grid.cells.len(),
116            widths: vec![widest_cell_width],
117        });
118
119        grid
120    }
121
122    /// The number of terminal columns this display takes up, based on the separator
123    /// width and the number and width of the columns.
124    pub fn width(&self) -> usize {
125        self.dimensions.total_width(self.options.filling.width())
126    }
127
128    /// The number of rows this display takes up.
129    pub fn row_count(&self) -> usize {
130        self.dimensions.num_lines
131    }
132
133    /// The width of each column
134    pub fn column_widths(&self) -> &[usize] {
135        &self.dimensions.widths
136    }
137
138    /// Returns whether this display takes up as many columns as were allotted
139    /// to it.
140    ///
141    /// It’s possible to construct tables that don’t actually use up all the
142    /// columns that they could, such as when there are more columns than
143    /// cells! In this case, a column would have a width of zero. This just
144    /// checks for that.
145    pub fn is_complete(&self) -> bool {
146        self.dimensions.widths.iter().all(|&x| x > 0)
147    }
148
149    fn compute_dimensions(&self, num_lines: usize, num_columns: usize) -> Dimensions {
150        let mut column_widths = vec![0; num_columns];
151        for (index, cell_width) in self.widths.iter().copied().enumerate() {
152            let index = match self.options.direction {
153                Direction::LeftToRight => index % num_columns,
154                Direction::TopToBottom => index / num_lines,
155            };
156            if cell_width > column_widths[index] {
157                column_widths[index] = cell_width;
158            }
159        }
160
161        Dimensions {
162            num_lines,
163            widths: column_widths,
164        }
165    }
166
167    fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize {
168        // TODO: Make code readable / efficient.
169        let mut widths = self.widths.clone();
170
171        // Sort widths in reverse order
172        widths.sort_unstable_by(|a, b| b.cmp(a));
173
174        let mut col_total_width_so_far = 0;
175        for (i, &width) in widths.iter().enumerate() {
176            let adjusted_width = if i == 0 {
177                width
178            } else {
179                width + self.options.filling.width()
180            };
181            if col_total_width_so_far + adjusted_width <= maximum_width {
182                col_total_width_so_far += adjusted_width;
183            } else {
184                return div_ceil(self.cells.len(), i);
185            }
186        }
187
188        // If we make it to this point, we have exhausted all cells before
189        // reaching the maximum width; the theoretical max number of lines
190        // needed to display all cells is 1.
191        1
192    }
193
194    fn width_dimensions(&self, maximum_width: usize) -> Option<Dimensions> {
195        if self.widest_cell_width > maximum_width {
196            // Largest cell is wider than maximum width; it is impossible to fit.
197            return None;
198        }
199
200        if self.cells.is_empty() {
201            return Some(Dimensions {
202                num_lines: 0,
203                widths: Vec::new(),
204            });
205        }
206
207        if self.cells.len() == 1 {
208            let cell_widths = self.widths[0];
209            return Some(Dimensions {
210                num_lines: 1,
211                widths: vec![cell_widths],
212            });
213        }
214
215        let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width);
216        if theoretical_max_num_lines == 1 {
217            // This if—statement is necessary for the function to work correctly
218            // for small inputs.
219            return Some(Dimensions {
220                num_lines: 1,
221                widths: self.widths.clone(),
222            });
223        }
224        // Instead of numbers of columns, try to find the fewest number of *lines*
225        // that the output will fit in.
226        let mut smallest_dimensions_yet = None;
227        for num_lines in (1..=theoretical_max_num_lines).rev() {
228            // The number of columns is the number of cells divided by the number
229            // of lines, *rounded up*.
230            let num_columns = div_ceil(self.cells.len(), num_lines);
231
232            // Early abort: if there are so many columns that the width of the
233            // *column separators* is bigger than the width of the screen, then
234            // don’t even try to tabulate it.
235            // This is actually a necessary check, because the width is stored as
236            // a usize, and making it go negative makes it huge instead, but it
237            // also serves as a speed-up.
238            let total_separator_width = (num_columns - 1) * self.options.filling.width();
239            if maximum_width < total_separator_width {
240                continue;
241            }
242
243            // Remove the separator width from the available space.
244            let adjusted_width = maximum_width - total_separator_width;
245
246            let potential_dimensions = self.compute_dimensions(num_lines, num_columns);
247            if potential_dimensions.widths.iter().sum::<usize>() <= adjusted_width {
248                smallest_dimensions_yet = Some(potential_dimensions);
249            } else {
250                break;
251            }
252        }
253
254        smallest_dimensions_yet
255    }
256}
257
258impl<T: AsRef<str>> fmt::Display for Grid<T> {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
260        let separator = match &self.options.filling {
261            Filling::Spaces(n) => " ".repeat(*n),
262            Filling::Text(s) => s.clone(),
263        };
264
265        // Initialize a buffer of spaces. The idea here is that any cell
266        // that needs padding gets a slice of this buffer of the needed
267        // size. This avoids the need of creating a string of spaces for
268        // each cell that needs padding.
269        //
270        // We overestimate how many spaces we need, but this is not
271        // part of the loop and it's therefore not super important to
272        // get exactly right.
273        let padding = " ".repeat(self.widest_cell_width);
274
275        for y in 0..self.dimensions.num_lines {
276            for x in 0..self.dimensions.widths.len() {
277                let num = match self.options.direction {
278                    Direction::LeftToRight => y * self.dimensions.widths.len() + x,
279                    Direction::TopToBottom => y + self.dimensions.num_lines * x,
280                };
281
282                // Abandon a line mid-way through if that’s where the cells end
283                if num >= self.cells.len() {
284                    continue;
285                }
286
287                let contents = &self.cells[num];
288                let width = self.widths[num];
289                let last_in_row = x == self.dimensions.widths.len() - 1;
290
291                let col_width = self.dimensions.widths[x];
292                let padding_size = col_width - width;
293
294                // The final column doesn’t need to have trailing spaces,
295                // as long as it’s left-aligned.
296                //
297                // We use write_str directly instead of a the write! macro to
298                // avoid some of the formatting overhead. For example, if we pad
299                // using `write!("{contents:>width}")`, the unicode width will
300                // have to be independently calculated by the macro, which is slow and
301                // redundant because we already know the width.
302                //
303                // For the padding, we instead slice into a buffer of spaces defined
304                // above, so we don't need to call `" ".repeat(n)` each loop.
305                // We also only call `write_str` when we actually need padding as
306                // another optimization.
307                f.write_str(contents.as_ref())?;
308                if !last_in_row {
309                    if padding_size > 0 {
310                        f.write_str(&padding[0..padding_size])?;
311                    }
312                    f.write_str(&separator)?;
313                }
314            }
315            f.write_str("\n")?;
316        }
317
318        Ok(())
319    }
320}
321
322// Adapted from the unstable API:
323// https://doc.rust-lang.org/std/primitive.usize.html#method.div_ceil
324// Can be removed on MSRV 1.73.
325/// Division with upward rounding
326pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
327    let d = lhs / rhs;
328    let r = lhs % rhs;
329    if r > 0 && rhs > 0 {
330        d + 1
331    } else {
332        d
333    }
334}