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}