boxy-cli 2.1.0

Styled terminal boxes with multi-segment layouts, columnar grids, true-color borders, Unicode box-drawing, and word wrapping.
Documentation
//! The main datatypes and enums used by the `Boxy` struct.

use std::{borrow::Cow, fmt::Display};

use colored::Color;

/// Defines the border style for the text box.
///
/// Each variant represents a different visual style for the box borders.
///
/// # Examples
///
/// ```
/// use boxy_cli::prelude::*;
///
/// let mut box1 = Boxy::new(BoxType::Double, "#00ffff");
/// let mut box2 = Boxy::new(BoxType::Rounded, "#00ffff");
/// let mut box3 = Boxy::new(BoxType::Bold, "#00ffff");
/// ```
#[derive(Debug, Default)]
pub enum BoxType {
    /// Simple ASCII-style box using `+` for corners and `-` for borders
    Classic,
    /// Default style using single-line Unicode box drawing characters
    #[default]
    Single,
    /// Double horizontal lines with single vertical lines
    DoubleHorizontal,
    /// Double vertical lines with single horizontal lines
    DoubleVertical,
    /// Double lines for all borders
    Double,
    /// Thicker/bold lines for all borders
    Bold,
    /// Box with rounded corners
    Rounded,
    /// Box with bold corners and normal edges
    BoldCorners,
    /// Box with no borders (invisible)
    Empty,
}

// Added Display Fucntion to resolve type errors in the macro
impl Display for BoxType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let str = match &self {
            BoxType::Classic => "classic".to_string(),
            BoxType::Single => "single".to_string(),
            BoxType::DoubleHorizontal => "double_horizontal".to_string(),
            BoxType::DoubleVertical => "double_vertical".to_string(),
            BoxType::Double => "double".to_string(),
            BoxType::Bold => "bold".to_string(),
            BoxType::Rounded => "rounded".to_string(),
            BoxType::BoldCorners => "bold_corners".to_string(),
            BoxType::Empty => "empty".to_string(),
        };
        write!(f, "{}", str)
    }
}

/// Specifies the alignment of text within the text box or the box itself within the terminal.
///
/// This enum is used in two contexts:
/// 1. To control the alignment of text segments within the box
/// 2. To control the alignment of the entire box within the terminal width
///
/// # Examples
///
/// ```
/// use boxy_cli::prelude::*;
///
/// let mut my_box = Boxy::new(BoxType::Single, "#00ffff");
///
/// // Center the box in the terminal
/// my_box.set_align(BoxAlign::Center);
///
/// // Add a left-aligned text segment
/// my_box.add_text_sgmt("Left aligned text", "#ffffff", BoxAlign::Left);
///
/// // Add a right-aligned text segment
/// my_box.add_text_sgmt("Right aligned text", "#ffffff", BoxAlign::Right);
/// ```
#[derive(Debug, Default)]
pub enum BoxAlign {
    /// Align the box to the left in the terminal, or align text to the left within a segment
    Left,
    /// center the box in the terminal, or center text within a segment.
    /// When used as box alignment with external padding, the padding values
    /// affect box width but not position — see [`Boxy::set_align`](crate::boxer::Boxy::set_align).
    #[default]
    Center,
    /// Align the box to the right in the terminal, or align text to the right within a segment
    Right,
}

// Added Display Fucntion to resolve type errors in the macro
impl Display for BoxAlign {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let str = match self {
            BoxAlign::Left => "left".to_string(),
            BoxAlign::Center => "center".to_string(),
            BoxAlign::Right => "right".to_string(),
        };
        write!(f, "{}", str)
    }
}

/// Represents padding values for the text box in all four directions.
///
/// `BoxPad` is used to specify padding between:
/// - The box border and the contained text (internal padding)
/// - The terminal edges and the box itself (external padding)
///
/// # Examples
///
/// ```
/// use boxy_cli::prelude::*;
///
/// // Create uniform padding of 2 spaces on all sides
/// let uniform_padding = BoxPad::uniform(2);
///
/// // Create custom padding for each side
/// let custom_padding = BoxPad::from_tldr(1, 3, 1, 3); // top, left, down, right
///
/// // Create horizontal/vertical padding
/// let h_v_padding = BoxPad::vh(1, 3); // 1 vertical, 3 horizontal
/// ```
#[derive(Debug)]
pub struct BoxPad {
    /// Padding at the top
    pub top: usize,
    /// Padding at the bottom
    pub down: usize,
    /// Padding on the left side
    pub left: usize,
    /// Padding on the right side
    pub right: usize,
}

impl Default for BoxPad {
    fn default() -> Self {
        Self::new()
    }
}

impl BoxPad {
    /// Creates a new `BoxPad` instance with zero padding on all sides.
    ///
    /// # Examples
    ///
    /// ```
    /// use boxy_cli::prelude::*;
    ///
    /// let padding = BoxPad::new();
    /// // Equivalent to BoxPad { top: 0, down: 0, left: 0, right: 0 }
    /// ```
    pub fn new() -> Self {
        BoxPad {
            top: 0,
            down: 0,
            left: 0,
            right: 0,
        }
    }
    /// Creates a new `BoxPad` with specific values for each side.
    ///
    /// # Argument order
    ///
    /// The parameter order follows the mnemonic **tldr**: **t**op, **l**eft, **d**own, **r**ight.
    /// Note this is not the CSS order (top/right/bottom/left) — left and right are swapped
    /// relative to what you might expect. When in doubt, use [`vh`](Self::vh) for symmetric
    /// padding or name the fields directly.
    ///
    /// # Arguments
    ///
    /// * `top` - Padding above the content
    /// * `left` - Padding to the left of the content
    /// * `down` - Padding below the content
    /// * `right` - Padding to the right of the content
    ///
    /// # Examples
    ///
    /// ```
    /// use boxy_cli::prelude::*;
    ///
    /// let padding = BoxPad::from_tldr(1, 4, 1, 4); // 1 line top/bottom, 4 chars left/right
    /// assert_eq!(padding.top, 1);
    /// assert_eq!(padding.left, 4);
    /// assert_eq!(padding.down, 1);
    /// assert_eq!(padding.right, 4);
    /// ```
    pub fn from_tldr(top: usize, left: usize, down: usize, right: usize) -> Self {
        BoxPad {
            top,
            down,
            left,
            right,
        }
    }

    /// Creates a new `BoxPad` with the same padding value on all sides.
    ///
    /// # Arguments
    ///
    /// * `pad` - The padding value to use for all sides
    ///
    /// # Examples
    ///
    /// ```
    /// use boxy_cli::prelude::*;
    ///
    /// // Create uniform padding of 3 spaces on all sides
    /// let padding = BoxPad::uniform(3);
    /// // Equivalent to BoxPad { top: 3, down: 3, left: 3, right: 3 }
    /// ```
    pub fn uniform(pad: usize) -> Self {
        BoxPad {
            top: pad,
            down: pad,
            left: pad,
            right: pad,
        }
    }
    /// Creates a new `BoxPad` with separate values for vertical and horizontal padding.
    ///
    /// This is a convenience method that applies the same padding to top/bottom
    /// and left/right sides.
    ///
    /// # Arguments
    ///
    /// * `vertical` - Padding for top and bottom
    /// * `horizontal` - Padding for left and right
    ///
    /// # Examples
    ///
    /// ```
    /// use boxy_cli::prelude::*;
    ///
    /// // Create padding with 1 space vertically and 3 spaces horizontally
    /// let padding = BoxPad::vh(1, 3);
    /// // Equivalent to BoxPad { top: 1, down: 1, left: 3, right: 3 }
    /// ```
    pub fn vh(vertical: usize, horizontal: usize) -> Self {
        BoxPad {
            top: vertical,
            down: vertical,
            left: horizontal,
            right: horizontal,
        }
    }

    /// Returns the total horizontal padding (left + right).
    ///
    /// Used internally for text wrapping and width calculations.
    ///
    /// # Examples
    ///
    /// ```
    /// use boxy_cli::prelude::*;
    ///
    /// let pad = BoxPad::vh(1, 3);
    /// assert_eq!(pad.lr(), 6); // left (3) + right (3)
    /// ```
    pub fn lr(&self) -> usize {
        self.right + self.left
    }
}

#[allow(dead_code)]
#[derive(Debug)]
/// Represents the data layout of a single segment in a [`Boxy`](crate::boxer::Boxy) box.
///
/// Each segment is either a [`Single`](SegType::Single) (plain text, one line per entry)
/// or a [`Columnar`](SegType::Columnar) (side-by-side columns, each with their own lines).
pub enum SegType<'a> {
    /// A plain text segment. Each `Cow<str>` is one line of text content.
    Single(Vec<Cow<'a, str>>),
    /// A columnar segment. The outer `Vec` is the list of columns; each inner `Vec` is
    /// the lines of text within that column.
    Columnar(Vec<Vec<Cow<'a, str>>>),
}

#[allow(dead_code)]
impl<'a> SegType<'a> {
    /// Pushes a line into this segment. For [`Columnar`](SegType::Columnar), pushes into
    /// the last column.
    pub(crate) fn push(&mut self, p0: Cow<'a, str>) {
        match self {
            SegType::Single(vec) => vec.push(p0),
            SegType::Columnar(vec) => {
                if let Some(vec) = vec.last_mut() {
                    vec.push(p0);
                }
            }
        }
    }
    pub(crate) fn get_single(&self, index: usize) -> Option<&Cow<'a, str>> {
        match self {
            SegType::Single(vec) => vec.get(index),
            _ => None,
        }
    }
    pub(crate) fn get_columnar(&self, index: usize) -> Option<&Vec<Cow<'a, str>>> {
        match self {
            SegType::Columnar(vec) => vec.get(index),
            _ => None,
        }
    }
}

#[derive(Debug, Clone)]
/// Stores pre-parsed text colors for each segment, mirroring the shape of [`SegType`].
///
/// Colors are parsed from hex strings once at segment-creation time and stored as
/// [`Color`](colored::Color) values, so [`display()`](crate::boxer::Boxy::display) never
/// needs to re-parse them.
pub enum SegColor {
    /// Colors for a [`SegType::Single`] segment — one `Color` per line of text.
    Single(Vec<Color>),
    /// Colors for a [`SegType::Columnar`] segment — one `Vec<Color>` per column,
    /// with one `Color` per line within that column.
    Columnar(Vec<Vec<Color>>),
}

use hex_color::HexColor;

impl SegColor {
    /// Parses a hex color string (e.g. `"#00ffff"`) into a [`Color::TrueColor`](colored::Color).
    /// Falls back to [`Color::White`](colored::Color::White) and prints a warning on parse failure.
    pub(crate) fn parse_hexcolor(hex: &str) -> Color {
        let box_col = match HexColor::parse(hex) {
            Ok(color) => Color::TrueColor {
                r: color.r,
                g: color.g,
                b: color.b,
            },
            Err(e) => {
                eprintln!("Error parsing box color '{}': {}", hex, e);
                Color::White // Default color
            }
        };
        box_col
    }
}