add \t filling option (#46)

* chore(deps): update codecov/codecov-action action to v5

* add new Tabs filling and rework fmt process

* add filling_with_tabs test

* add tabs example

* update readme

* fix misspelling

* fix fmt in tests

* optimize padding size calc

* remove separator duplication to reduce memory usage

* make DEFAULT_SEPARATOR_SIZE pub

* fix padding calculation

* fix required padding is bigger than widest cell

* add next entry check before printing the separator

* update docs for tabs

* update comments

* add diff size separator with Tabs

* simplify the tabs number calculation

* update readme

* remove comments

* change last in row check to break

* fix some grammar in readme

* update docs with quotes for \t

* rename postion vars and add comment

* rename neares tab var and update comment

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This commit is contained in:
Leo Emar-Kar
2025-04-08 23:21:57 +02:00
committed by GitHub
co-authored by renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
parent ad32b2c6b3
commit c8db34bd41
5 changed files with 204 additions and 21 deletions
+7 -2
View File
@@ -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
+1 -1
View File
@@ -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();
+32
View File
@@ -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);
}
+76 -17
View File
@@ -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<T: AsRef<str>> Grid<T> {
impl<T: AsRef<str>> fmt::Display for Grid<T> {
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<T: AsRef<str>> fmt::Display for Grid<T> {
// 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 thats where the cells end
if num >= self.cells.len() {
continue;
// Abandon a line mid-way through if thats 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<T: AsRef<str>> fmt::Display for Grid<T> {
// 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")?;
+88 -1
View File
@@ -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 {