diff --git a/README.md b/README.md index c5af0a1..2aa0235 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,10 @@ and a set of options. There are three options that must be specified in the [`GridOptions`] value that dictate how the grid is formatted: -- [`filling`][filling]: what to put in between two columns — either a number of - spaces, or a text string; +- [`filling`][filling]: how to fill empty space between columns: + - [`Filling::Spaces`][Spaces] number of spaces between columns; + - [`Filling::Text`][Text] text string separator between columns; + - [`Filling::Tabs`][Tabs] special option which allows to set number of spaces between columns and set the size of `\t` character. - [`direction`][direction]: specifies whether the cells should go along rows, or columns: - [`Direction::LeftToRight`][LeftToRight] starts them in the top left and @@ -94,6 +96,9 @@ nine ten eleven twelve [width]: https://docs.rs/uutils_term_grid/latest/term_grid/struct.GridOptions.html#structfield.width [LeftToRight]: https://docs.rs/uutils_term_grid/latest/term_grid/enum.Direction.html#variant.LeftToRight [TopToBottom]: https://docs.rs/uutils_term_grid/latest/term_grid/enum.Direction.html#variant.TopToBottom +[Spaces]: https://docs.rs/uutils_term_grid/latest/term_grid/enum.Filling.html#variant.Spaces +[Text]: https://docs.rs/uutils_term_grid/latest/term_grid/enum.Filling.html#variant.Text +[Tabs]:https://docs.rs/uutils_term_grid/latest/term_grid/enum.Filling.html#variant.Tabs ## Width of grid cells diff --git a/examples/basic.rs b/examples/basic.rs index 876d131..eeb98a5 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -11,7 +11,7 @@ use term_grid::{Direction, Filling, Grid, GridOptions}; // 8 | 1024 | 131072 | 16777216 | 2147483648 | 274877906944 | 35184372088832 // 16 | 2048 | 262144 | 33554432 | 4294967296 | 549755813888 | 70368744177664 // 32 | 4096 | 524288 | 67108864 | 8589934592 | 1099511627776 | 140737488355328 -// 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 | +// 64 | 8192 | 1048576 | 134217728 | 17179869184 | 2199023255552 fn main() { let cells: Vec<_> = (0..48).map(|i| 2_isize.pow(i).to_string()).collect(); diff --git a/examples/tabs.rs b/examples/tabs.rs new file mode 100644 index 0000000..b32ef0f --- /dev/null +++ b/examples/tabs.rs @@ -0,0 +1,32 @@ +// 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, DEFAULT_SEPARATOR_SIZE}; + +// This produces: +// +// 1···128↹··16384↹···2097152····268435456↹···34359738368├┤··4398046511104␊ +// 2···256↹··32768↹···4194304····536870912↹···68719476736├┤··8796093022208␊ +// 4···512↹··65536↹···8388608····1073741824···137438953472↹··17592186044416␊ +// 8···1024··131072···16777216···2147483648···274877906944↹··35184372088832␊ +// 16··2048··262144···33554432···4294967296···549755813888↹··70368744177664␊ +// 32··4096··524288···67108864···8589934592···1099511627776··140737488355328␊ +// 64··8192··1048576··134217728··17179869184··2199023255552␊ + +fn main() { + 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::Tabs { + spaces: DEFAULT_SEPARATOR_SIZE, + tab_size: 8, + }, + width: 80, + }, + ); + + println!("{}", grid); +} diff --git a/src/lib.rs b/src/lib.rs index 9253b81..b6a0d68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,12 @@ use ansi_width::ansi_width; use std::fmt; +/// Number of spaces in one \t. +pub const SPACES_IN_TAB: usize = 8; + +/// Default size for separator in spaces. +pub const DEFAULT_SEPARATOR_SIZE: usize = 2; + /// Direction cells should be written in: either across or downwards. #[derive(PartialEq, Eq, Debug, Copy, Clone)] pub enum Direction { @@ -37,6 +43,14 @@ pub enum Filling { /// /// `"|"` is a common choice. Text(String), + + /// Fill spaces with `\t` + Tabs { + /// A number of spaces + spaces: usize, + /// Size of `\t` in spaces + tab_size: usize, + }, } impl Filling { @@ -44,6 +58,7 @@ impl Filling { match self { Filling::Spaces(w) => *w, Filling::Text(t) => ansi_width(t), + Filling::Tabs { spaces, .. } => *spaces, } } } @@ -257,9 +272,15 @@ impl> Grid { impl> fmt::Display for Grid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - let separator = match &self.options.filling { - Filling::Spaces(n) => " ".repeat(*n), - Filling::Text(s) => s.clone(), + // If cells are empty then, nothing to print, skip. + if self.cells.is_empty() { + return Ok(()); + } + + let (tab_size, separator) = match &self.options.filling { + Filling::Spaces(n) => (0, " ".repeat(*n)), + Filling::Text(s) => (0, s.clone()), + Filling::Tabs { spaces, tab_size } => (*tab_size, " ".repeat(*spaces)), }; // Initialize a buffer of spaces. The idea here is that any cell @@ -270,24 +291,33 @@ impl> fmt::Display for Grid { // 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.widest_cell_width); + let padding = " ".repeat(self.widest_cell_width + self.options.filling.width()); for y in 0..self.dimensions.num_lines { + // Current position on the line. + let mut cursor: usize = 0; for x in 0..self.dimensions.widths.len() { - let num = match self.options.direction { - Direction::LeftToRight => y * self.dimensions.widths.len() + x, - Direction::TopToBottom => y + self.dimensions.num_lines * x, + // Calculate position of the current element of the grid + // in cells and widths vectors and the offset to the next value. + let (current, offset) = match self.options.direction { + Direction::LeftToRight => (y * self.dimensions.widths.len() + x, 1), + Direction::TopToBottom => { + (y + self.dimensions.num_lines * x, self.dimensions.num_lines) + } }; - // Abandon a line mid-way through if that’s where the cells end - if num >= self.cells.len() { - continue; + // Abandon a line mid-way through if that’s where the cells end. + if current >= self.cells.len() { + break; } - let contents = &self.cells[num]; - let width = self.widths[num]; + // Last in row checks only the predefined grid width. + // It does not check if there will be more entries. + // For this purpose we define next value as well. + // This prevents printing separator after the actual last value in a row. let last_in_row = x == self.dimensions.widths.len() - 1; - + let contents = &self.cells[current]; + let width = self.widths[current]; let col_width = self.dimensions.widths[x]; let padding_size = col_width - width; @@ -305,11 +335,40 @@ impl> fmt::Display for Grid { // We also only call `write_str` when we actually need padding as // another optimization. f.write_str(contents.as_ref())?; - if !last_in_row { - if padding_size > 0 { - f.write_str(&padding[0..padding_size])?; - } + + // In case this entry was the last on the current line, + // there is no need to print the separator and padding. + if last_in_row || current + offset >= self.cells.len() { + break; + } + + // Special case if tab size was not set. Fill with spaces and separator. + if tab_size == 0 { + f.write_str(&padding[..padding_size])?; f.write_str(&separator)?; + } else { + // Move cursor to the end of the current contents. + cursor += width; + let total_spaces = padding_size + self.options.filling.width(); + // The size of \t can be inconsistent in terminal. + // Tab stops are relative to the cursor position e.g., + // * cursor = 0, \t moves to column 8; + // * cursor = 5, \t moves to column 8 (3 spaces); + // * cursor = 9, \t moves to column 16 (7 spaces). + // Calculate the nearest \t position in relation to cursor. + let closest_tab = tab_size - (cursor % tab_size); + + if closest_tab > total_spaces { + f.write_str(&padding[..total_spaces])?; + } else { + let rest_spaces = total_spaces - closest_tab; + let tabs = 1 + (rest_spaces / tab_size); + let spaces = rest_spaces % tab_size; + f.write_str(&"\t".repeat(tabs))?; + f.write_str(&padding[..spaces])?; + } + + cursor += total_spaces; } } f.write_str("\n")?; diff --git a/tests/test.rs b/tests/test.rs index b761d5d..b1a0db4 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3,7 +3,7 @@ // spell-checker:ignore underflowed -use term_grid::{Direction, Filling, Grid, GridOptions}; +use term_grid::{Direction, Filling, Grid, GridOptions, DEFAULT_SEPARATOR_SIZE, SPACES_IN_TAB}; #[test] fn no_items() { @@ -238,6 +238,93 @@ fn eza_many_folders() { assert_eq!(grid.row_count(), 20); } +#[test] +fn filling_with_tabs() { + let grid = Grid::new( + vec![ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ], + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Tabs { + spaces: DEFAULT_SEPARATOR_SIZE, + tab_size: 2, + }, + width: 24, + }, + ); + + let bits = "one\t\t two\t\t three\nfour\t five\t\t six\nseven\t eight\t nine\nten\t\t eleven\t twelve\n"; + assert_eq!(grid.to_string(), bits); + assert_eq!(grid.row_count(), 4); +} + +#[test] +fn padding_bigger_than_widest() { + let grid = Grid::new( + vec!["1", "2", "3"], + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Tabs { + spaces: DEFAULT_SEPARATOR_SIZE, + tab_size: SPACES_IN_TAB, + }, + width: 20, + }, + ); + + let bits = "1 2 3\n"; + assert_eq!(grid.to_string(), bits); +} + +#[test] +fn odd_number_of_entries() { + let cells = vec!["one", "two", "three", "four", "five"]; + let grid = Grid::new( + cells.clone(), + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Spaces(2), + width: 15, + }, + ); + + assert_eq!(grid.to_string(), "one two\nthree four\nfive\n"); + + let grid = Grid::new( + cells.clone(), + GridOptions { + direction: Direction::TopToBottom, + filling: Filling::Spaces(2), + width: 15, + }, + ); + + assert_eq!(grid.to_string(), "one four\ntwo five\nthree\n"); +} + +#[test] +fn different_size_separator_with_tabs() { + let grid = Grid::new( + vec![ + "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", + "eleven", "twelve", + ], + GridOptions { + direction: Direction::LeftToRight, + filling: Filling::Tabs { + spaces: 4, + tab_size: 2, + }, + width: 40, + }, + ); + + let bits = "one\t\t\ttwo\t\t three\t\t four\nfive\t\tsix\t\t seven\t\t eight\nnine\t\tten\t\t eleven\t\t twelve\n"; + assert_eq!(grid.to_string(), bits); +} + // These test are based on the tests in uutils ls, to ensure we won't break // it while editing this library. mod uutils_ls {