use std::cmp::max;
use std::fmt;
use std::iter::repeat_n;
use unicode_width::UnicodeWidthStr;
fn unicode_width_strip_ansi(astring: &str) -> usize {
nu_utils::strip_ansi_unlikely(astring).width()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Left,
Right,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Cell {
pub contents: String,
pub width: Width,
pub alignment: Alignment,
}
impl From<String> for Cell {
fn from(string: String) -> Self {
Self {
width: unicode_width_strip_ansi(&string),
contents: string,
alignment: Alignment::Left,
}
}
}
impl<'a> From<&'a str> for Cell {
fn from(string: &'a str) -> Self {
Self {
width: unicode_width_strip_ansi(string),
contents: string.into(),
alignment: Alignment::Left,
}
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum Direction {
LeftToRight,
TopToBottom,
}
pub type Width = usize;
#[derive(PartialEq, Eq, Debug)]
pub enum Filling {
Spaces(Width),
Text(String),
}
impl Filling {
fn width(&self) -> Width {
match *self {
Filling::Spaces(w) => w,
Filling::Text(ref t) => unicode_width_strip_ansi(&t[..]),
}
}
}
#[derive(PartialEq, Eq, Debug)]
pub struct GridOptions {
pub direction: Direction,
pub filling: Filling,
}
#[derive(PartialEq, Eq, Debug)]
struct Dimensions {
num_lines: Width,
widths: Vec<Width>,
}
impl Dimensions {
fn total_width(&self, separator_width: Width) -> Width {
if self.widths.is_empty() {
0
} else {
let values = self.widths.iter().sum::<Width>();
let separators = separator_width * (self.widths.len() - 1);
values + separators
}
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct Grid {
options: GridOptions,
cells: Vec<Cell>,
widest_cell_length: Width,
width_sum: Width,
cell_count: usize,
}
impl Grid {
pub fn new(options: GridOptions) -> Self {
let cells = Vec::new();
Self {
options,
cells,
widest_cell_length: 0,
width_sum: 0,
cell_count: 0,
}
}
pub fn reserve(&mut self, additional: usize) {
self.cells.reserve(additional)
}
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)
}
pub fn fit_into_width(&self, maximum_width: Width) -> Option<Display<'_>> {
self.width_dimensions(maximum_width).map(|dims| Display {
grid: self,
dimensions: dims,
})
}
pub fn fit_into_columns(&self, num_columns: usize) -> Display<'_> {
Display {
grid: self,
dimensions: self.columns_dimensions(num_columns),
}
}
fn columns_dimensions(&self, num_columns: usize) -> Dimensions {
let mut num_lines = self.cells.len() / num_columns;
if !self.cells.len().is_multiple_of(num_columns) {
num_lines += 1;
}
self.column_widths(num_lines, num_columns)
}
fn column_widths(&self, num_lines: usize, num_columns: usize) -> Dimensions {
let mut widths: Vec<Width> = repeat_n(0, num_columns).collect();
for (index, cell) in self.cells.iter().enumerate() {
let index = match self.options.direction {
Direction::LeftToRight => index % num_columns,
Direction::TopToBottom => index / num_lines,
};
widths[index] = max(widths[index], cell.width);
}
Dimensions { num_lines, widths }
}
fn theoretical_max_num_lines(&self, maximum_width: usize) -> usize {
let mut theoretical_min_num_cols = 0;
let mut col_total_width_so_far = 0;
let mut cells = self.cells.clone();
cells.sort_unstable_by_key(|cell| std::cmp::Reverse(cell.width));
for cell in &cells {
if cell.width + col_total_width_so_far <= maximum_width {
theoretical_min_num_cols += 1;
col_total_width_so_far += cell.width;
} else {
let mut theoretical_max_num_lines = self.cell_count / theoretical_min_num_cols;
if !self.cell_count.is_multiple_of(theoretical_min_num_cols) {
theoretical_max_num_lines += 1;
}
return theoretical_max_num_lines;
}
col_total_width_so_far += self.options.filling.width()
}
1
}
fn width_dimensions(&self, maximum_width: Width) -> Option<Dimensions> {
if self.widest_cell_length > maximum_width {
return None;
}
if self.cell_count == 0 {
return Some(Dimensions {
num_lines: 0,
widths: Vec::new(),
});
}
if self.cell_count == 1 {
let the_cell = &self.cells[0];
return Some(Dimensions {
num_lines: 1,
widths: vec![the_cell.width],
});
}
let theoretical_max_num_lines = self.theoretical_max_num_lines(maximum_width);
if theoretical_max_num_lines == 1 {
return Some(Dimensions {
num_lines: 1,
widths: self
.cells
.clone()
.into_iter()
.map(|cell| cell.width)
.collect(),
});
}
let mut smallest_dimensions_yet = None;
for num_lines in (1..=theoretical_max_num_lines).rev() {
let mut num_columns = self.cell_count / num_lines;
if !self.cell_count.is_multiple_of(num_lines) {
num_columns += 1;
}
let total_separator_width = (num_columns - 1) * self.options.filling.width();
if maximum_width < total_separator_width {
continue;
}
let adjusted_width = maximum_width - total_separator_width;
let potential_dimensions = self.column_widths(num_lines, num_columns);
if potential_dimensions.widths.iter().sum::<Width>() < adjusted_width {
smallest_dimensions_yet = Some(potential_dimensions);
} else {
return smallest_dimensions_yet;
}
}
None
}
}
#[derive(Eq, PartialEq, Debug)]
pub struct Display<'grid> {
grid: &'grid Grid,
dimensions: Dimensions,
}
impl Display<'_> {
pub fn width(&self) -> Width {
self.dimensions
.total_width(self.grid.options.filling.width())
}
pub fn row_count(&self) -> usize {
self.dimensions.num_lines
}
pub fn is_complete(&self) -> bool {
self.dimensions.widths.iter().all(|&x| x > 0)
}
}
impl fmt::Display for Display<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
for y in 0..self.dimensions.num_lines {
for x in 0..self.dimensions.widths.len() {
let num = match self.grid.options.direction {
Direction::LeftToRight => y * self.dimensions.widths.len() + x,
Direction::TopToBottom => y + self.dimensions.num_lines * x,
};
if num >= self.grid.cells.len() {
continue;
}
let cell = &self.grid.cells[num];
if x == self.dimensions.widths.len() - 1 {
match cell.alignment {
Alignment::Left => {
write!(f, "{}", cell.contents)?;
}
Alignment::Right => {
let extra_spaces = self.dimensions.widths[x] - cell.width;
write!(
f,
"{}",
pad_string(&cell.contents, extra_spaces, Alignment::Right)
)?;
}
}
} else {
assert!(self.dimensions.widths[x] >= cell.width);
match (&self.grid.options.filling, cell.alignment) {
(Filling::Spaces(n), Alignment::Left) => {
let extra_spaces = self.dimensions.widths[x] - cell.width + n;
write!(
f,
"{}",
pad_string(&cell.contents, extra_spaces, cell.alignment)
)?;
}
(Filling::Spaces(n), Alignment::Right) => {
let s = spaces(*n);
let extra_spaces = self.dimensions.widths[x] - cell.width;
write!(
f,
"{}{}",
pad_string(&cell.contents, extra_spaces, cell.alignment),
s
)?;
}
(Filling::Text(t), _) => {
let extra_spaces = self.dimensions.widths[x] - cell.width;
write!(
f,
"{}{}",
pad_string(&cell.contents, extra_spaces, cell.alignment),
t
)?;
}
}
}
}
writeln!(f)?;
}
Ok(())
}
}
fn spaces(length: usize) -> String {
" ".repeat(length)
}
fn pad_string(string: &str, padding: usize, alignment: Alignment) -> String {
if alignment == Alignment::Left {
format!("{}{}", string, spaces(padding))
} else {
format!("{}{}", spaces(padding), string)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn no_items() {
let grid = Grid::new(GridOptions {
direction: Direction::TopToBottom,
filling: Filling::Spaces(2),
});
let display = grid.fit_into_width(40).unwrap();
assert_eq!(display.dimensions.num_lines, 0);
assert!(display.dimensions.widths.is_empty());
assert_eq!(display.width(), 0);
}
#[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!(display.dimensions.num_lines, 1);
assert_eq!(display.dimensions.widths, vec![1]);
assert_eq!(display.width(), 1);
}
#[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 display = grid.fit_into_width(10).unwrap();
assert_eq!(display.dimensions.num_lines, 1);
assert_eq!(display.dimensions.widths, vec![10]);
assert_eq!(display.width(), 10);
}
#[test]
fn one_item_just_over() {
let mut grid = Grid::new(GridOptions {
direction: Direction::TopToBottom,
filling: Filling::Spaces(2),
});
grid.add(Cell::from("1234567890!"));
assert_eq!(grid.fit_into_width(10), None);
}
#[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();
assert_eq!(display.dimensions.num_lines, 1);
assert_eq!(display.dimensions.widths, vec![1, 1]);
assert_eq!(display.width(), 1 + 2 + 1);
}
#[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();
assert_eq!(display.dimensions.num_lines, 1);
assert_eq!(display.dimensions.widths, vec![11, 18]);
assert_eq!(display.width(), 11 + 2 + 18);
}
#[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_eq!(grid.fit_into_width(40), None);
}
#[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 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);
}
#[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 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);
}
#[test]
fn numbers_right() {
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",
] {
let mut cell = Cell::from(*s);
cell.alignment = Alignment::Right;
grid.add(cell);
}
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);
}
#[test]
fn numbers_right_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",
] {
let mut cell = Cell::from(*s);
cell.alignment = Alignment::Right;
grid.add(cell);
}
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);
}
#[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_eq!(grid.fit_into_width(99), None);
}
#[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();
assert_eq!(display.dimensions.num_lines, 1);
assert_eq!(display.dimensions.widths, vec![4]);
assert_eq!(display.width(), 4);
}
}