diff --git a/Cargo.toml b/Cargo.toml index 935913d..5080079 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,4 @@ rust-version = "1.70" name = "term_grid" [dependencies] -unicode-width = "0.1.11" +textwrap = { version = "0.16.0", default-features = false, features = ["unicode-width"] } diff --git a/README.md b/README.md index 0cf0d4c..a56a4ba 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,24 @@ [![dependency status](https://deps.rs/repo/github/uutils/uutils-term-grid/status.svg)](https://deps.rs/repo/github/uutils/uutils-term-grid) [![CodeCov](https://codecov.io/gh/uutils/uutils-term-grid/branch/master/graph/badge.svg)](https://codecov.io/gh/uutils/uutils-term-grid) - # uutils-term-grid -This library arranges textual data in a grid format suitable for fixed-width fonts, using an algorithm to minimise the amount of space needed. +This library arranges textual data in a grid format suitable for fixed-width +fonts, using an algorithm to minimise the amount of space needed. --- -This library is forked from the [`rust-term-grid`](https://github.com/ogham/rust-term-grid) library. +This library is forked from the unmaintained +[`rust-term-grid`](https://github.com/ogham/rust-term-grid) library. The core +functionality has remained the same, with some additional bugfixes, performance +improvements and a new API. --- # Installation -This crate works with `cargo`. Add the following to your `Cargo.toml` dependencies section: +This crate works with `cargo`. Add the following to your `Cargo.toml` +dependencies section: ```toml [dependencies] @@ -24,70 +28,81 @@ uutils_term_grid = "0.3" The Minimum Supported Rust Version is 1.70. +## Creating a grid -## Usage +To add data to a grid, first create a new [`Grid`] value with a list of strings +and a set of options. -This library arranges textual data in a grid format suitable for fixed-width fonts, using an algorithm to minimise the amount of space needed. -For example: +There are three options that must be specified in the [`GridOptions`] value that +dictate how the grid is formatted: -```rust -use term_grid::{Grid, GridOptions, Direction, Filling, Cell}; +- [`filling`][filling]: what to put in between two columns — either a number of + spaces, or a text string; +- [`direction`][direction]: specifies whether the cells should go along rows, or + columns: + - [`Direction::LeftToRight`][LeftToRight] starts them in the top left and + moves _rightwards_, going to the start of a new row after reaching the final + column; + - [`Direction::TopToBottom`][TopToBottom] starts them in the top left and + moves _downwards_, going to the top of a new column after reaching the final + row. +- [`width`][width]: the width to fill the grid into. Usually, this should be the + width of the terminal. -let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(1), - direction: Direction::LeftToRight, -}); +In practice, creating a grid can be done as follows: -for s in &["one", "two", "three", "four", "five", "six", "seven", - "eight", "nine", "ten", "eleven", "twelve"] -{ - grid.add(Cell::from(*s)); -} - -println!("{}", grid.fit_into_width(24).unwrap()); +```rust +use term_grid::{Grid, GridOptions, Direction, Filling}; + +// Create a `Vec` of text to put in the grid +let cells = vec![ + "one", "two", "three", "four", "five", "six", + "seven", "eight", "nine", "ten", "eleven", "twelve" +]; + +// Then create a `Grid` with those cells. +// The grid requires several options: +// - The filling determines the string used as separator +// between the columns. +// - The direction specifies whether the layout should +// be done row-wise or column-wise. +// - The width is the maximum width that the grid might +// have. +let grid = Grid::new( + cells, + GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + width: 24, + } +); + +// A `Grid` implements `Display` and can be printed directly. +println!("{grid}"); ``` Produces the following tabular result: -``` +```text one two three four five six seven eight nine ten eleven twelve ``` +[filling]: struct.GridOptions.html#structfield.filling +[direction]: struct.GridOptions.html#structfield.direction +[width]: struct.GridOptions.html#structfield.width +[LeftToRight]: enum.Direction.html#variant.LeftToRight +[TopToBottom]: enum.Direction.html#variant.TopToBottom -## Creating a grid - -To add data to a grid, first create a new `Grid` value, and then add cells to them with the `add` method. - -There are two options that must be specified in the `GridOptions` value that dictate how the grid is formatted: - -- `filling`: what to put in between two columns - either a number of spaces, or a text string; -- `direction`, which specifies whether the cells should go along rows, or columns: - - `Direction::LeftToRight` starts them in the top left and moves *rightwards*, going to the start of a new row after reaching the final column; - - `Direction::TopToBottom` starts them in the top left and moves *downwards*, going to the top of a new column after reaching the final row. - - -## Displaying a grid - -When display a grid, you can either specify the number of columns in advance, or try to find the maximum number of columns that can fit in an area of a given width. - -Splitting a series of cells into columns - or, in other words, starting a new row every *n* cells - is achieved with the `fit_into_columns` method on a `Grid` value. -It takes as its argument the number of columns. - -Trying to fit as much data onto one screen as possible is the main use case for specifying a maximum width instead. -This is achieved with the `fit_into_width` method. -It takes the maximum allowed width, including separators, as its argument. -However, it returns an *optional* `Display` value, depending on whether any of the cells actually had a width greater than the maximum width! -If this is the case, your best bet is to just output the cells with one per line. - - -## Cells and data +## Width of grid cells -Grids do not take `String`s or `&str`s - they take `Cells`. +This library calculates the width of strings as displayed in the terminal using +the [`textwrap`][textwrap] library (with the [`display_width`][display_width] function). +This takes into account the width of characters and ignores ANSI codes. -A **Cell** is a struct containing an individual cell’s contents, as a string, and its pre-computed length, which gets used when calculating a grid’s final dimensions. -Usually, you want the *Unicode width* of the string to be used for this, so you can turn a `String` into a `Cell` with the `.into()` method. +The width calculation is currently not configurable. If you have a use-case for +which this calculation is wrong, please open an issue. -However, you may also want to supply your own width: when you already know the width in advance, or when you want to change the measurement, such as skipping over terminal control characters. -For cases like these, the fields on the `Cell` values are public, meaning you can construct your own instances as necessary. +[textwrap]: https://docs.rs/textwrap/latest/textwrap/index.html +[display_width]: https://docs.rs/textwrap/latest/textwrap/core/fn.display_width.html diff --git a/examples/basic.rs b/examples/basic.rs index 11a81c3..876d131 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,7 @@ -extern crate term_grid; -use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use term_grid::{Direction, Filling, Grid, GridOptions}; // This produces: // @@ -12,19 +14,16 @@ use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; // 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 | fn main() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Text(" | ".into()), - }); + let cells: Vec<_> = (0..48).map(|i| 2_isize.pow(i).to_string()).collect(); - for i in 0..48 { - let cell = Cell::from(2_isize.pow(i).to_string()); - grid.add(cell) - } + let grid = Grid::new( + cells, + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + width: 80, + }, + ); - if let Some(grid_display) = grid.fit_into_width(80) { - println!("{}", grid_display); - } else { - println!("Couldn't fit grid into 80 columns!"); - } + println!("{}", grid); } diff --git a/examples/big.rs b/examples/big.rs index de42113..6456497 100644 --- a/examples/big.rs +++ b/examples/big.rs @@ -1,26 +1,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -extern crate term_grid; -use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; +use term_grid::{Direction, Filling, Grid, GridOptions}; fn main() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Text(" | ".into()), - }); - let mut n: u64 = 1234; for _ in 0..50 { + let mut cells = Vec::new(); for _ in 0..10000 { - grid.add(Cell::from(n.to_string())); + cells.push(n.to_string()); n = n.overflowing_pow(2).0 % 100000000; } - if let Some(grid_display) = grid.fit_into_width(80) { - println!("{}", grid_display); - } else { - println!("Couldn't fit grid into 80 columns!"); - } + let grid = Grid::new( + cells, + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + width: 80, + }, + ); + + println!("{grid}"); } } diff --git a/src/lib.rs b/src/lib.rs index e2bbfae..6106871 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + #![warn(future_incompatible)] #![warn(missing_copy_implementations)] #![warn(missing_docs)] @@ -5,134 +8,12 @@ #![warn(trivial_casts, trivial_numeric_casts)] #![warn(unused)] #![deny(unsafe_code)] - -//! This library arranges textual data in a grid format suitable for -//! fixed-width fonts, using an algorithm to minimise the amount of space -//! needed. For example: -//! -//! ```rust -//! use term_grid::{Grid, GridOptions, Direction, Filling, Cell}; -//! -//! let mut grid = Grid::new(GridOptions { -//! filling: Filling::Spaces(1), -//! direction: Direction::LeftToRight, -//! }); -//! -//! for s in &["one", "two", "three", "four", "five", "six", "seven", -//! "eight", "nine", "ten", "eleven", "twelve"] -//! { -//! grid.add(Cell::from(*s)); -//! } -//! -//! println!("{}", grid.fit_into_width(24).unwrap()); -//! ``` -//! -//! Produces the following tabular result: -//! -//! ```text -//! one two three four -//! five six seven eight -//! nine ten eleven twelve -//! ``` -//! -//! -//! ## Creating a grid -//! -//! To add data to a grid, first create a new [`Grid`] value, and then add -//! cells to them with the `add` function. -//! -//! There are two options that must be specified in the [`GridOptions`] value -//! that dictate how the grid is formatted: -//! -//! - `filling`: what to put in between two columns — either a number of -//! spaces, or a text string; -//! - `direction`, which specifies whether the cells should go along -//! rows, or columns: -//! - `Direction::LeftToRight` starts them in the top left and -//! moves *rightwards*, going to the start of a new row after reaching the -//! final column; -//! - `Direction::TopToBottom` starts them in the top left and moves -//! *downwards*, going to the top of a new column after reaching the final -//! row. -//! -//! -//! ## Displaying a grid -//! -//! When display a grid, you can either specify the number of columns in advance, -//! or try to find the maximum number of columns that can fit in an area of a -//! given width. -//! -//! Splitting a series of cells into columns — or, in other words, starting a new -//! row every n cells — is achieved with the [`fit_into_columns`] function -//! on a `Grid` value. It takes as its argument the number of columns. -//! -//! Trying to fit as much data onto one screen as possible is the main use case -//! for specifying a maximum width instead. This is achieved with the -//! [`fit_into_width`] function. It takes the maximum allowed width, including -//! separators, as its argument. However, it returns an *optional* [`Display`] -//! value, depending on whether any of the cells actually had a width greater than -//! the maximum width! If this is the case, your best bet is to just output the -//! cells with one per line. -//! -//! -//! ## Cells and data -//! -//! Grids to not take `String`s or `&str`s — they take [`Cell`] values. -//! -//! A **Cell** is a struct containing an individual cell’s contents, as a string, -//! and its pre-computed length, which gets used when calculating a grid’s final -//! dimensions. Usually, you want the *Unicode width* of the string to be used for -//! this, so you can turn a `String` into a `Cell` with the `.into()` function. -//! -//! However, you may also want to supply your own width: when you already know the -//! width in advance, or when you want to change the measurement, such as skipping -//! over terminal control characters. For cases like these, the fields on the -//! `Cell` values are public, meaning you can construct your own instances as -//! necessary. -//! -//! [`Cell`]: ./struct.Cell.html -//! [`Display`]: ./struct.Display.html -//! [`Grid`]: ./struct.Grid.html -//! [`fit_into_columns`]: ./struct.Grid.html#method.fit_into_columns -//! [`fit_into_width`]: ./struct.Grid.html#method.fit_into_width -//! [`GridOptions`]: ./struct.GridOptions.html +#![doc = include_str!("../README.md")] use std::fmt; -use unicode_width::UnicodeWidthStr; - -/// A **Cell** is the combination of a string and its pre-computed length. -/// -/// The easiest way to create a Cell is just by using `string.into()`, which -/// uses the **unicode width** of the string (see the `unicode_width` crate). -/// However, the fields are public, if you wish to provide your own length. -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct Cell { - /// The string to display when this cell gets rendered. - pub contents: String, - - /// The pre-computed length of the string. - pub width: Width, -} - -impl From for Cell { - fn from(string: String) -> Self { - Self { - width: UnicodeWidthStr::width(&*string), - contents: string, - } - } -} +use textwrap::core::display_width; -impl<'a> From<&'a str> for Cell { - fn from(string: &'a str) -> Self { - Self { - width: UnicodeWidthStr::width(string), - contents: string.into(), - } - } -} - -/// Direction cells should be written in — either across, or downwards. +/// Direction cells should be written in: either across or downwards. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum Direction { /// Starts at the top left and moves rightwards, going back to the first @@ -144,58 +25,58 @@ pub enum Direction { TopToBottom, } -/// The width of a cell, in columns. -pub type Width = usize; - /// The text to put in between each pair of columns. +/// /// This does not include any spaces used when aligning cells. #[derive(PartialEq, Eq, Debug)] pub enum Filling { - /// A certain number of spaces should be used as the separator. - Spaces(Width), + /// A number of spaces + Spaces(usize), - /// An arbitrary string. + /// An arbitrary string + /// /// `"|"` is a common choice. Text(String), } impl Filling { - fn width(&self) -> Width { - match *self { - Filling::Spaces(w) => w, - Filling::Text(ref t) => UnicodeWidthStr::width(&t[..]), + fn width(&self) -> usize { + match self { + Filling::Spaces(w) => *w, + Filling::Text(t) => display_width(t), } } } -/// The user-assignable options for a grid view that should be passed to -/// [`Grid::new()`](struct.Grid.html#method.new). +/// The options for a grid view that should be passed to [`Grid::new`] #[derive(Debug)] pub struct GridOptions { - /// The direction that the cells should be written in — either - /// across, or downwards. + /// The direction that the cells should be written in pub direction: Direction, - /// The number of spaces to put in between each column of cells. + /// The string to put in between each column of cells pub filling: Filling, + + /// The width to fill with the grid + pub width: usize, } #[derive(PartialEq, Eq, Debug)] struct Dimensions { /// The number of lines in the grid. - num_lines: Width, + num_lines: usize, /// The width of each column in the grid. The length of this vector serves /// as the number of columns. - widths: Vec, + widths: Vec, } impl Dimensions { - fn total_width(&self, separator_width: Width) -> Width { + fn total_width(&self, separator_width: usize) -> usize { if self.widths.is_empty() { 0 } else { - let values = self.widths.iter().sum::(); + let values = self.widths.iter().sum::(); let separators = separator_width * (self.widths.len() - 1); values + separators } @@ -203,90 +84,84 @@ impl Dimensions { } /// Everything needed to format the cells with the grid options. -/// -/// For more information, see the [`term_grid` crate documentation](index.html). #[derive(Debug)] -pub struct Grid { +pub struct Grid> { options: GridOptions, - cells: Vec, - widest_cell_length: Width, - width_sum: Width, - cell_count: usize, + cells: Vec, + widths: Vec, + widest_cell_width: usize, + dimensions: Dimensions, } -impl Grid { - /// Creates a new grid view with the given options. - pub fn new(options: GridOptions) -> Self { - let cells = Vec::new(); - Self { +impl> Grid { + /// Creates a new grid view with the given cells and options + pub fn new(cells: Vec, options: GridOptions) -> Self { + let widths: Vec = cells.iter().map(|c| display_width(c.as_ref())).collect(); + let widest_cell_width = widths.iter().copied().max().unwrap_or(0); + let width = options.width; + + let mut grid = Self { options, cells, - widest_cell_length: 0, - width_sum: 0, - cell_count: 0, - } - } + widths, + widest_cell_width, + dimensions: Dimensions { + num_lines: 0, + widths: Vec::new(), + }, + }; - /// Reserves space in the vector for the given number of additional cells - /// to be added. (See the `Vec::reserve` function.) - pub fn reserve(&mut self, additional: usize) { - self.cells.reserve(additional) - } + grid.dimensions = grid.width_dimensions(width).unwrap_or(Dimensions { + num_lines: grid.cells.len(), + widths: vec![widest_cell_width], + }); - /// Adds another cell onto the vector. - pub fn add(&mut self, cell: Cell) { - if cell.width > self.widest_cell_length { - self.widest_cell_length = cell.width; - } - self.width_sum += cell.width; - self.cell_count += 1; - self.cells.push(cell) + grid } - /// Returns a displayable grid that’s been packed to fit into the given - /// width in the fewest number of rows. - /// - /// Returns `None` if any of the cells has a width greater than the - /// maximum width. - pub fn fit_into_width(&self, maximum_width: Width) -> Option> { - self.width_dimensions(maximum_width).map(|dims| Display { - grid: self, - dimensions: dims, - }) + /// The number of terminal columns this display takes up, based on the separator + /// width and the number and width of the columns. + pub fn width(&self) -> usize { + self.dimensions.total_width(self.options.filling.width()) } - /// Returns a displayable grid with the given number of columns, and no - /// maximum width. - pub fn fit_into_columns(&self, num_columns: usize) -> Display<'_> { - Display { - grid: self, - dimensions: self.columns_dimensions(num_columns), - } + /// The number of rows this display takes up. + pub fn row_count(&self) -> usize { + self.dimensions.num_lines } - fn columns_dimensions(&self, num_columns: usize) -> Dimensions { - let num_lines = div_ceil(self.cells.len(), num_columns); - self.column_widths(num_lines, num_columns) + /// Returns whether this display takes up as many columns as were allotted + /// to it. + /// + /// It’s possible to construct tables that don’t actually use up all the + /// columns that they could, such as when there are more columns than + /// cells! In this case, a column would have a width of zero. This just + /// checks for that. + pub fn is_complete(&self) -> bool { + self.dimensions.widths.iter().all(|&x| x > 0) } fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions { - let mut widths = vec![0; num_columns]; - for (index, cell) in self.cells.iter().enumerate() { + let mut column_widths = vec![0; num_columns]; + for (index, cell_width) in self.widths.iter().copied().enumerate() { let index = match self.options.direction { Direction::LeftToRight => index % num_columns, Direction::TopToBottom => index / num_lines, }; - if cell.width > widths[index] { - widths[index] = cell.width; + if cell_width > column_widths[index] { + column_widths[index] = cell_width; } } - Dimensions { num_lines, widths } + Dimensions { + num_lines, + widths: column_widths, + } } fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize { // TODO: Make code readable / efficient. - let mut widths: Vec<_> = self.cells.iter().map(|c| c.width).collect(); + let mut widths = self.widths.clone(); // Sort widths in reverse order widths.sort_unstable_by(|a, b| b.cmp(a)); @@ -296,7 +171,7 @@ impl Grid { if width + col_total_width_so_far <= maximum_width { col_total_width_so_far += self.options.filling.width() + width; } else { - return div_ceil(self.cell_count, i); + return div_ceil(self.cells.len(), i); } } @@ -306,34 +181,34 @@ impl Grid { 1 } - fn width_dimensions(&self, maximum_width: Width) -> Option { - if self.widest_cell_length > maximum_width { + fn width_dimensions(&self, maximum_width: usize) -> Option { + if self.widest_cell_width > maximum_width { // Largest cell is wider than maximum width; it is impossible to fit. return None; } - if self.cell_count == 0 { + if self.cells.is_empty() { return Some(Dimensions { num_lines: 0, widths: Vec::new(), }); } - if self.cell_count == 1 { - let the_cell = &self.cells[0]; + if self.cells.len() == 1 { + let cell_widths = self.widths[0]; return Some(Dimensions { num_lines: 1, - widths: vec![the_cell.width], + widths: vec![cell_widths], }); } let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width); if theoretical_max_num_lines == 1 { - // This if—statement is neccesary for the function to work correctly + // This if—statement is necessary for the function to work correctly // for small inputs. return Some(Dimensions { num_lines: 1, - widths: self.cells.iter().map(|cell| cell.width).collect(), + widths: self.widths.clone(), }); } // Instead of numbers of columns, try to find the fewest number of *lines* @@ -342,7 +217,7 @@ impl Grid { for num_lines in (1..=theoretical_max_num_lines).rev() { // The number of columns is the number of cells divided by the number // of lines, *rounded up*. - let num_columns = div_ceil(self.cell_count, num_lines); + let num_columns = div_ceil(self.cells.len(), num_lines); // Early abort: if there are so many columns that the width of the // *column separators* is bigger than the width of the screen, then @@ -359,7 +234,7 @@ impl Grid { let adjusted_width = maximum_width - total_separator_width; let potential_dimensions = self.column_widths(num_lines, num_columns); - if potential_dimensions.widths.iter().sum::() < adjusted_width { + if potential_dimensions.widths.iter().sum::() < adjusted_width { smallest_dimensions_yet = Some(potential_dimensions); } else { return smallest_dimensions_yet; @@ -370,47 +245,9 @@ impl Grid { } } -/// A displayable representation of a [`Grid`](struct.Grid.html). -/// -/// This type implements `Display`, so you can get the textual version -/// of the grid by calling `.to_string()`. -#[derive(Debug)] -pub struct Display<'grid> { - /// The grid to display. - grid: &'grid Grid, - - /// The pre-computed column widths for this grid. - dimensions: Dimensions, -} - -impl Display<'_> { - /// Returns how many columns this display takes up, based on the separator - /// width and the number and width of the columns. - pub fn width(&self) -> Width { - self.dimensions - .total_width(self.grid.options.filling.width()) - } - - /// Returns how many rows this display takes up. - pub fn row_count(&self) -> usize { - self.dimensions.num_lines - } - - /// Returns whether this display takes up as many columns as were allotted - /// to it. - /// - /// It’s possible to construct tables that don’t actually use up all the - /// columns that they could, such as when there are more columns than - /// cells! In this case, a column would have a width of zero. This just - /// checks for that. - pub fn is_complete(&self) -> bool { - self.dimensions.widths.iter().all(|&x| x > 0) - } -} - -impl fmt::Display for Display<'_> { +impl> fmt::Display for Grid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let separator = match &self.grid.options.filling { + let separator = match &self.options.filling { Filling::Spaces(n) => " ".repeat(*n), Filling::Text(s) => s.clone(), }; @@ -423,26 +260,26 @@ impl fmt::Display for Display<'_> { // We overestimate how many spaces we need, but this is not // part of the loop and it's therefore not super important to // get exactly right. - let padding = " ".repeat(self.grid.widest_cell_length); + let padding = " ".repeat(self.widest_cell_width); for y in 0..self.dimensions.num_lines { for x in 0..self.dimensions.widths.len() { - let num = match self.grid.options.direction { + let num = match self.options.direction { Direction::LeftToRight => y * self.dimensions.widths.len() + x, Direction::TopToBottom => y + self.dimensions.num_lines * x, }; // Abandon a line mid-way through if that’s where the cells end - if num >= self.grid.cells.len() { + if num >= self.cells.len() { continue; } - let cell = &self.grid.cells[num]; - let contents = &cell.contents; + let contents = &self.cells[num]; + let width = self.widths[num]; let last_in_row = x == self.dimensions.widths.len() - 1; let col_width = self.dimensions.widths[x]; - let padding_size = col_width - cell.width; + let padding_size = col_width - width; // The final column doesn’t need to have trailing spaces, // as long as it’s left-aligned. @@ -457,7 +294,7 @@ impl fmt::Display for Display<'_> { // above, so we don't need to call `" ".repeat(n)` each loop. // We also only call `write_str` when we actually need padding as // another optimization. - f.write_str(contents)?; + f.write_str(contents.as_ref())?; if !last_in_row { if padding_size > 0 { f.write_str(&padding[0..padding_size])?; @@ -474,7 +311,8 @@ impl fmt::Display for Display<'_> { // Adapted from the unstable API: // https://doc.rust-lang.org/std/primitive.usize.html#method.div_ceil -/// Division with upward rouding +// Can be removed on MSRV 1.73. +/// Division with upward rounding pub const fn div_ceil(lhs: usize, rhs: usize) -> usize { let d = lhs / rhs; let r = lhs % rhs; diff --git a/tests/test.rs b/tests/test.rs index 5913ca8..03532d5 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,167 +1,176 @@ -use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore underflowed + +use term_grid::{Direction, Filling, Grid, GridOptions}; #[test] fn no_items() { - let grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); + let grid = Grid::new( + Vec::::new(), + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 40, + }, + ); - let display = grid.fit_into_width(40).unwrap(); - assert_eq!("", display.to_string()); + assert_eq!("", grid.to_string()); } #[test] fn one_item() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from("1")); - - let display = grid.fit_into_width(40).unwrap(); - assert_eq!("1\n", display.to_string()); + let grid = Grid::new( + vec!["1"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 40, + }, + ); + assert_eq!("1\n", grid.to_string()); } #[test] fn one_item_exact_width() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from("1234567890")); + let grid = Grid::new( + vec!["1234567890"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 10, + }, + ); - let display = grid.fit_into_width(10).unwrap(); - assert_eq!("1234567890\n", display.to_string()); + assert_eq!("1234567890\n", grid.to_string()); } #[test] fn one_item_just_over() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from("1234567890!")); + let grid = Grid::new( + vec!["1234567890!"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 10, + }, + ); - assert!(grid.fit_into_width(10).is_none()); + assert_eq!(grid.row_count(), 1); } #[test] fn two_small_items() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from("1")); - grid.add(Cell::from("2")); - - let display = grid.fit_into_width(40).unwrap(); + let grid = Grid::new( + vec!["1", "2"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 40, + }, + ); - assert_eq!(display.width(), 1 + 2 + 1); - assert_eq!("1 2\n", display.to_string()); + assert_eq!(grid.width(), 1 + 2 + 1); + assert_eq!("1 2\n", grid.to_string()); } #[test] fn two_medium_size_items() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from("hello there")); - grid.add(Cell::from("how are you today?")); - - let display = grid.fit_into_width(40).unwrap(); + let grid = Grid::new( + vec!["hello there", "how are you today?"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 40, + }, + ); - assert_eq!(display.width(), 11 + 2 + 18); - assert_eq!("hello there how are you today?\n", display.to_string()); + assert_eq!(grid.width(), 11 + 2 + 18); + assert_eq!("hello there how are you today?\n", grid.to_string()); } #[test] fn two_big_items() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - grid.add(Cell::from( - "nuihuneihsoenhisenouiuteinhdauisdonhuisudoiosadiuohnteihaosdinhteuieudi", - )); - grid.add(Cell::from( - "oudisnuthasuouneohbueobaugceoduhbsauglcobeuhnaeouosbubaoecgueoubeohubeo", - )); - - assert!(grid.fit_into_width(40).is_none()); + let grid = Grid::new( + vec![ + "nuihuneihsoenhisenouiuteinhdauisdonhuisudoiosadiuohnteihaosdinhteuieudi", + "oudisnuthasuouneohbueobaugceoduhbsauglcobeuhnaeouosbubaoecgueoubeohubeo", + ], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 40, + }, + ); + + assert_eq!(grid.row_count(), 2); } #[test] fn that_example_from_earlier() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(1), - direction: Direction::LeftToRight, - }); - - for s in &[ - "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", - "twelve", - ] { - grid.add(Cell::from(*s)); - } + let grid = Grid::new( + vec![ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ], + GridOptions { + filling: Filling::Spaces(1), + direction: Direction::LeftToRight, + width: 24, + }, + ); let bits = "one two three four\nfive six seven eight\nnine ten eleven twelve\n"; - assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); - assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + assert_eq!(grid.to_string(), bits); + assert_eq!(grid.row_count(), 3); } #[test] fn number_grid_with_pipe() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Text("|".into()), - direction: Direction::LeftToRight, - }); - - for s in &[ - "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", - "twelve", - ] { - grid.add(Cell::from(*s)); - } + let grid = Grid::new( + vec![ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ], + GridOptions { + filling: Filling::Text("|".into()), + direction: Direction::LeftToRight, + width: 24, + }, + ); let bits = "one |two|three |four\nfive|six|seven |eight\nnine|ten|eleven|twelve\n"; - assert_eq!(grid.fit_into_width(24).unwrap().to_string(), bits); - assert_eq!(grid.fit_into_width(24).unwrap().row_count(), 3); + assert_eq!(grid.to_string(), bits); + assert_eq!(grid.row_count(), 3); } #[test] fn huge_separator() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(100), - direction: Direction::LeftToRight, - }); - - grid.add("a".into()); - grid.add("b".into()); - - assert!(grid.fit_into_width(99).is_none()); + let grid = Grid::new( + vec!["a", "b"], + GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + width: 99, + }, + ); + assert_eq!(grid.row_count(), 2); } #[test] fn huge_yet_unused_separator() { - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(100), - direction: Direction::LeftToRight, - }); - - grid.add("abcd".into()); - - let display = grid.fit_into_width(99).unwrap(); + let grid = Grid::new( + vec!["abcd"], + GridOptions { + filling: Filling::Spaces(100), + direction: Direction::LeftToRight, + width: 99, + }, + ); - assert_eq!(display.width(), 4); - assert_eq!("abcd\n", display.to_string()); + assert_eq!(grid.width(), 4); + assert_eq!("abcd\n", grid.to_string()); } // Note: This behaviour is right or wrong depending on your terminal @@ -169,17 +178,33 @@ fn huge_yet_unused_separator() { // behaviour, unless we explicitly want to do that. #[test] fn emoji() { - let mut grid = Grid::new(GridOptions { - direction: Direction::LeftToRight, - filling: Filling::Spaces(2), - }); + let grid = Grid::new( + vec!["🦀", "hello", "👩‍🔬", "hello"], + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Spaces(2), + width: 12, + }, + ); + assert_eq!("🦀 hello\n👩‍🔬 hello\n", grid.to_string()); +} - for s in ["🦀", "hello", "👩‍🔬", "hello"] { - grid.add(s.into()); - } +// This test once underflowed, which should never happen. The test is just +// checking that we do not get a panic. +#[test] +fn possible_underflow() { + let cells: Vec<_> = (0..48).map(|i| 2_isize.pow(i).to_string()).collect(); + + let grid = Grid::new( + cells, + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Text(" | ".into()), + width: 15, + }, + ); - let display = grid.fit_into_width(12).unwrap(); - assert_eq!("🦀 hello\n👩‍🔬 hello\n", display.to_string()); + println!("{}", grid); } // These test are based on the tests in uutils ls, to ensure we won't break @@ -203,83 +228,78 @@ mod uutils_ls { "test-width-1\ntest-width-2\ntest-width-3\ntest-width-4\n", ), ] { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - for s in [ - "test-width-1", - "test-width-2", - "test-width-3", - "test-width-4", - ] { - grid.add(s.into()); - } - - let display = grid.fit_into_width(width).unwrap(); - assert_eq!(expected, display.to_string()); + let grid = Grid::new( + vec![ + "test-width-1", + "test-width-2", + "test-width-3", + "test-width-4", + ], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width, + }, + ); + assert_eq!(expected, grid.to_string()); } } #[test] fn across_width_30() { - let mut grid = Grid::new(GridOptions { - direction: Direction::LeftToRight, - filling: Filling::Spaces(2), - }); - - for s in [ - "test-across1", - "test-across2", - "test-across3", - "test-across4", - ] { - grid.add(s.into()); - } + let grid = Grid::new( + vec![ + "test-across1", + "test-across2", + "test-across3", + "test-across4", + ], + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Spaces(2), + width: 30, + }, + ); - let display = grid.fit_into_width(30).unwrap(); assert_eq!( "test-across1 test-across2\ntest-across3 test-across4\n", - display.to_string() + grid.to_string() ); } #[test] fn columns_width_30() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - for s in [ - "test-columns1", - "test-columns2", - "test-columns3", - "test-columns4", - ] { - grid.add(s.into()); - } + let grid = Grid::new( + vec![ + "test-columns1", + "test-columns2", + "test-columns3", + "test-columns4", + ], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 30, + }, + ); - let display = grid.fit_into_width(30).unwrap(); assert_eq!( "test-columns1 test-columns3\ntest-columns2 test-columns4\n", - display.to_string() + grid.to_string() ); } #[test] fn three_short_one_long() { - let mut grid = Grid::new(GridOptions { - direction: Direction::TopToBottom, - filling: Filling::Spaces(2), - }); - - for s in ["a", "b", "a-long-name", "z"] { - grid.add(s.into()); - } + let grid = Grid::new( + vec!["a", "b", "a-long-name", "z"], + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 15, + }, + ); - let display = grid.fit_into_width(15).unwrap(); - assert_eq!("a a-long-name\nb z\n", display.to_string()); + assert_eq!("a a-long-name\nb z\n", grid.to_string()); } }