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}