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}