uutils_term_grid 0.8.0

Library for formatting strings into a grid layout. Fork of term_grid.
Documentation
// 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)]
#![warn(nonstandard_style)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused)]
#![deny(unsafe_code)]
#![doc = include_str!("../README.md")]

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 {
    /// Starts at the top left and moves rightwards, going back to the first
    /// column for a new row, like a typewriter.
    LeftToRight,

    /// Starts at the top left and moves downwards, going back to the first
    /// row for a new column, like how `ls` lists files by default.
    TopToBottom,
}

/// 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 number of spaces
    Spaces(usize),

    /// An arbitrary string
    ///
    /// `"|"` 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 {
    fn width(&self) -> usize {
        match self {
            Self::Spaces(w) => *w,
            Self::Text(t) => ansi_width(t),
            Self::Tabs { spaces, .. } => *spaces,
        }
    }
}

/// 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
    pub direction: Direction,

    /// 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_rows: usize,

    /// The width of each column in the grid. The length of this vector serves
    /// as the number of columns.
    widths: Vec<usize>,
}

impl Dimensions {
    fn total_width(&self, separator_width: usize) -> usize {
        if self.widths.is_empty() {
            0
        } else {
            let values = self.widths.iter().sum::<usize>();
            let separators = separator_width * (self.widths.len() - 1);
            values + separators
        }
    }
}

/// Everything needed to format the cells with the grid options.
#[derive(Debug)]
pub struct Grid<T: AsRef<str>> {
    options: GridOptions,
    cells: Vec<T>,
    widths: Vec<usize>,
    widest_cell_width: usize,
    dimensions: Dimensions,
}

impl<T: AsRef<str>> Grid<T> {
    /// Creates a new grid view with the given cells and options
    #[must_use]
    pub fn new(cells: Vec<T>, options: GridOptions) -> Self {
        let widths: Vec<usize> = cells.iter().map(|c| ansi_width(c.as_ref())).collect();
        let widest_cell_width = widths.iter().copied().max().unwrap_or(0);

        let mut grid = Self {
            options,
            cells,
            widths,
            widest_cell_width,
            dimensions: Dimensions {
                num_rows: 0,
                widths: Vec::new(),
            },
        };

        if !grid.cells.is_empty() {
            grid.dimensions = grid.width_dimensions();
        }

        grid
    }

    /// The number of terminal columns this display takes up, based on the separator
    /// width and the number and width of the columns.
    #[must_use]
    pub fn width(&self) -> usize {
        self.dimensions.total_width(self.options.filling.width())
    }

    /// The number of rows this display takes up.
    #[must_use]
    pub const fn row_count(&self) -> usize {
        self.dimensions.num_rows
    }

    /// The width of each column
    #[must_use]
    pub fn column_widths(&self) -> &[usize] {
        &self.dimensions.widths
    }

    /// 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.
    #[must_use]
    pub fn is_complete(&self) -> bool {
        self.dimensions.widths.iter().all(|&x| x > 0)
    }

    fn compute_dimensions(&self, num_lines: usize, num_columns: usize) -> Dimensions {
        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 > column_widths[index] {
                column_widths[index] = cell_width;
            }
        }

        Dimensions {
            num_rows: num_lines,
            widths: column_widths,
        }
    }

    fn width_dimensions(&self) -> Dimensions {
        if self.cells.len() == 1 {
            let cell_widths = self.widths[0];
            return Dimensions {
                num_rows: 1,
                widths: vec![cell_widths],
            };
        }

        // Calculate widest column size with separator.
        let widest_column = self.widest_cell_width + self.options.filling.width();
        // If it exceeds terminal's width, return, since it is impossible to fit.
        if widest_column > self.options.width {
            return Dimensions {
                num_rows: self.cells.len(),
                widths: vec![self.widest_cell_width],
            };
        }

        // Calculate the number of columns if all columns had the size of the largest
        // column. This is a lower bound on the number of columns.
        let min_columns = self
            .cells
            .len()
            .min((self.options.width + self.options.filling.width()) / widest_column);

        // Calculate maximum number of lines and columns.
        let max_rows = div_ceil(self.cells.len(), min_columns);

        // This is a potential dimension, which can definitely fit all of the cells.
        let mut potential_dimension = self.compute_dimensions(max_rows, min_columns);

        // If all of the cells can be fit on one line, return immediately.
        if max_rows == 1 {
            return potential_dimension;
        }

        // Try to increase number of columns, to see if new dimension can still fit.
        for num_columns in min_columns + 1..=self.cells.len() {
            let Some(adjusted_width) = self
                .options
                .width
                .checked_sub((num_columns - 1) * self.options.filling.width())
            else {
                break;
            };
            let num_rows = div_ceil(self.cells.len(), num_columns);
            let new_dimension = self.compute_dimensions(num_rows, num_columns);
            if new_dimension.widths.iter().sum::<usize>() <= adjusted_width {
                potential_dimension = new_dimension;
            }
        }

        potential_dimension
    }
}

impl<T: AsRef<str>> fmt::Display for Grid<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        // 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
        // that needs padding gets a slice of this buffer of the needed
        // size. This avoids the need of creating a string of spaces for
        // each cell that needs padding.
        //
        // 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 + self.options.filling.width());

        for y in 0..self.dimensions.num_rows {
            // Current position on the line.
            let mut cursor: usize = 0;
            for x in 0..self.dimensions.widths.len() {
                // 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_rows * x, self.dimensions.num_rows)
                    }
                };

                // Abandon a line mid-way through if that’s where the cells end.
                if current >= self.cells.len() {
                    break;
                }

                // 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;

                // The final column doesn’t need to have trailing spaces,
                // as long as it’s left-aligned.
                //
                // We use write_str directly instead of a the write! macro to
                // avoid some of the formatting overhead. For example, if we pad
                // using `write!("{contents:>width}")`, the unicode width will
                // have to be independently calculated by the macro, which is slow and
                // redundant because we already know the width.
                //
                // For the padding, we instead slice into a buffer of spaces defined
                // 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.as_ref())?;

                // 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")?;
        }

        Ok(())
    }
}

// Adapted from the unstable API:
// https://doc.rust-lang.org/std/primitive.usize.html#method.div_ceil
// Can be removed on MSRV 1.73.
/// Division with upward rounding
#[must_use]
pub const fn div_ceil(lhs: usize, rhs: usize) -> usize {
    let d = lhs / rhs;
    let r = lhs % rhs;
    if r > 0 && rhs > 0 {
        d + 1
    } else {
        d
    }
}