use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowLevel {
Top,
HeadRow,
Mid,
Row,
FootRow,
Bottom,
}
#[derive(Debug, Clone)]
pub struct BoxChars {
pub top: [char; 4],
pub head: [char; 4],
pub head_row: [char; 4],
pub mid: [char; 4],
pub row: [char; 4],
pub foot_row: [char; 4],
pub foot: [char; 4],
pub bottom: [char; 4],
pub ascii: bool,
}
impl BoxChars {
#[must_use]
#[expect(
clippy::too_many_arguments,
reason = "struct constructor needs all fields"
)]
pub const fn new(
top: [char; 4],
head: [char; 4],
head_row: [char; 4],
mid: [char; 4],
row: [char; 4],
foot_row: [char; 4],
foot: [char; 4],
bottom: [char; 4],
ascii: bool,
) -> Self {
Self {
top,
head,
head_row,
mid,
row,
foot_row,
foot,
bottom,
ascii,
}
}
#[must_use]
pub fn get_row_chars(&self, level: RowLevel) -> &[char; 4] {
match level {
RowLevel::Top => &self.top,
RowLevel::HeadRow => &self.head_row,
RowLevel::Mid => &self.mid,
RowLevel::Row => &self.row,
RowLevel::FootRow => &self.foot_row,
RowLevel::Bottom => &self.bottom,
}
}
#[must_use]
pub fn build_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
let chars = self.get_row_chars(level);
let left = chars[0];
let middle = chars[1];
let cross = chars[2];
let right = chars[3];
let mut result = String::new();
if edge && left != ' ' {
result.push(left);
}
for (i, &width) in widths.iter().enumerate() {
for _ in 0..width {
result.push(middle);
}
if i < widths.len() - 1 {
result.push(cross);
} else if edge && right != ' ' {
result.push(right);
}
}
result
}
#[must_use]
pub fn get_top(&self, widths: &[usize]) -> String {
self.build_row(widths, RowLevel::Top, true)
}
#[must_use]
pub fn get_bottom(&self, widths: &[usize]) -> String {
self.build_row(widths, RowLevel::Bottom, true)
}
#[must_use]
pub fn get_head_row(&self, widths: &[usize]) -> String {
self.build_row(widths, RowLevel::HeadRow, true)
}
#[must_use]
pub fn get_mid(&self, widths: &[usize]) -> String {
self.build_row(widths, RowLevel::Mid, true)
}
#[must_use]
pub fn get_row(&self, widths: &[usize]) -> String {
self.build_row(widths, RowLevel::Row, true)
}
#[must_use]
pub fn cell_left(&self) -> char {
self.head[0]
}
#[must_use]
pub fn cell_divider(&self) -> char {
self.head[2]
}
#[must_use]
pub fn cell_right(&self) -> char {
self.head[3]
}
#[must_use]
pub fn substitute(&self, safe: bool) -> &Self {
if safe && !self.ascii { &ASCII } else { self }
}
}
impl fmt::Display for BoxChars {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let widths = [3, 3, 3];
writeln!(f, "{}", self.get_top(&widths))?;
writeln!(
f,
"{} {} {} {}",
self.head[0], self.head[2], self.head[2], self.head[3]
)?;
writeln!(f, "{}", self.get_head_row(&widths))?;
writeln!(
f,
"{} {} {} {}",
self.head[0], self.head[2], self.head[2], self.head[3]
)?;
write!(f, "{}", self.get_bottom(&widths))
}
}
pub const ASCII: BoxChars = BoxChars::new(
['+', '-', '+', '+'],
['|', ' ', '|', '|'],
['|', '-', '+', '|'],
['|', '-', '+', '|'],
['|', '-', '+', '|'],
['|', '-', '+', '|'],
['|', ' ', '|', '|'],
['+', '-', '+', '+'],
true,
);
pub const ASCII2: BoxChars = BoxChars::new(
['+', '-', '+', '+'],
['|', ' ', '|', '|'],
['+', '-', '+', '+'],
['+', '-', '+', '+'],
['+', '-', '+', '+'],
['+', '-', '+', '+'],
['|', ' ', '|', '|'],
['+', '-', '+', '+'],
true,
);
pub const ASCII_DOUBLE_HEAD: BoxChars = BoxChars::new(
['+', '-', '+', '+'],
['|', ' ', '|', '|'],
['+', '=', '+', '+'],
['|', '-', '+', '|'],
['|', '-', '+', '|'],
['|', '-', '+', '|'],
['|', ' ', '|', '|'],
['+', '-', '+', '+'],
true,
);
pub const ROUNDED: BoxChars = BoxChars::new(
['\u{256D}', '\u{2500}', '\u{252C}', '\u{256E}'], ['\u{2502}', ' ', '\u{2502}', '\u{2502}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{2502}', ' ', '\u{2502}', '\u{2502}'], ['\u{2570}', '\u{2500}', '\u{2534}', '\u{256F}'], false,
);
pub const SQUARE: BoxChars = BoxChars::new(
['\u{250C}', '\u{2500}', '\u{252C}', '\u{2510}'], ['\u{2502}', ' ', '\u{2502}', '\u{2502}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{2502}', ' ', '\u{2502}', '\u{2502}'], ['\u{2514}', '\u{2500}', '\u{2534}', '\u{2518}'], false,
);
pub const DOUBLE: BoxChars = BoxChars::new(
['\u{2554}', '\u{2550}', '\u{2566}', '\u{2557}'], ['\u{2551}', ' ', '\u{2551}', '\u{2551}'], ['\u{2560}', '\u{2550}', '\u{256C}', '\u{2563}'], ['\u{2560}', '\u{2550}', '\u{256C}', '\u{2563}'], ['\u{2560}', '\u{2550}', '\u{256C}', '\u{2563}'], ['\u{2560}', '\u{2550}', '\u{256C}', '\u{2563}'], ['\u{2551}', ' ', '\u{2551}', '\u{2551}'], ['\u{255A}', '\u{2550}', '\u{2569}', '\u{255D}'], false,
);
pub const HEAVY: BoxChars = BoxChars::new(
['\u{250F}', '\u{2501}', '\u{2533}', '\u{2513}'], ['\u{2503}', ' ', '\u{2503}', '\u{2503}'], ['\u{2523}', '\u{2501}', '\u{254B}', '\u{252B}'], ['\u{2523}', '\u{2501}', '\u{254B}', '\u{252B}'], ['\u{2523}', '\u{2501}', '\u{254B}', '\u{252B}'], ['\u{2523}', '\u{2501}', '\u{254B}', '\u{252B}'], ['\u{2503}', ' ', '\u{2503}', '\u{2503}'], ['\u{2517}', '\u{2501}', '\u{253B}', '\u{251B}'], false,
);
pub const HEAVY_HEAD: BoxChars = BoxChars::new(
['\u{250F}', '\u{2501}', '\u{2533}', '\u{2513}'], ['\u{2503}', ' ', '\u{2503}', '\u{2503}'], ['\u{2521}', '\u{2501}', '\u{2547}', '\u{2529}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{251C}', '\u{2500}', '\u{253C}', '\u{2524}'], ['\u{2502}', ' ', '\u{2502}', '\u{2502}'], ['\u{2514}', '\u{2500}', '\u{2534}', '\u{2518}'], false,
);
pub const MINIMAL: BoxChars = BoxChars::new(
[' ', ' ', ' ', ' '],
[' ', ' ', '\u{2502}', ' '], [' ', '\u{2500}', '\u{253C}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', ' ', '\u{2502}', ' '], [' ', ' ', ' ', ' '],
false,
);
pub const SIMPLE: BoxChars = BoxChars::new(
[' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', '\u{2500}', '\u{2500}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', '\u{2500}', '\u{2500}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
false,
);
pub const SIMPLE_HEAVY: BoxChars = BoxChars::new(
[' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', '\u{2501}', '\u{2501}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', '\u{2501}', '\u{2501}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
false,
);
pub const HORIZONTALS: BoxChars = BoxChars::new(
[' ', '\u{2500}', '\u{2500}', ' '], [' ', ' ', ' ', ' '], [' ', '\u{2500}', '\u{2500}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', '\u{2500}', '\u{2500}', ' '], [' ', ' ', ' ', ' '],
[' ', '\u{2500}', '\u{2500}', ' '], false,
);
pub const MARKDOWN: BoxChars = BoxChars::new(
[' ', ' ', ' ', ' '], ['|', ' ', '|', '|'], ['|', '-', '|', '|'], ['|', '-', '|', '|'], ['|', ' ', '|', '|'], ['|', '-', '|', '|'], ['|', ' ', '|', '|'], [' ', ' ', ' ', ' '], true, );
pub const MINIMAL_HEAVY_HEAD: BoxChars = BoxChars::new(
[' ', ' ', ' ', ' '],
[' ', ' ', '\u{2502}', ' '], [' ', '\u{2501}', '\u{2547}', ' '], [' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', ' ', ' ', ' '],
[' ', ' ', '\u{2502}', ' '], [' ', ' ', ' ', ' '],
false,
);
#[must_use]
pub fn get_box(name: &str) -> Option<&'static BoxChars> {
match name.to_lowercase().as_str() {
"ascii" => Some(&ASCII),
"ascii2" => Some(&ASCII2),
"ascii_double_head" => Some(&ASCII_DOUBLE_HEAD),
"rounded" => Some(&ROUNDED),
"square" => Some(&SQUARE),
"double" => Some(&DOUBLE),
"heavy" => Some(&HEAVY),
"heavy_head" => Some(&HEAVY_HEAD),
"minimal" => Some(&MINIMAL),
"minimal_heavy_head" => Some(&MINIMAL_HEAVY_HEAD),
"simple" => Some(&SIMPLE),
"simple_heavy" => Some(&SIMPLE_HEAVY),
"horizontals" => Some(&HORIZONTALS),
"markdown" => Some(&MARKDOWN),
_ => None,
}
}
#[must_use]
pub fn get_safe_box(name: &str) -> &'static BoxChars {
let box_style = get_box(name).unwrap_or(&SQUARE);
if box_style.ascii { box_style } else { &ASCII }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ascii_box() {
const { assert!(ASCII.ascii) };
assert_eq!(ASCII.top[0], '+');
}
#[test]
fn test_get_top() {
let widths = [5, 3, 7];
let top = ASCII.get_top(&widths);
assert_eq!(top, "+-----+---+-------+");
}
#[test]
fn test_get_bottom() {
let widths = [5, 3];
let bottom = ASCII.get_bottom(&widths);
assert_eq!(bottom, "+-----+---+");
}
#[test]
fn test_unicode_square() {
const { assert!(!SQUARE.ascii) };
assert_eq!(SQUARE.top[0], '\u{250C}'); }
#[test]
fn test_get_box() {
assert!(get_box("ascii").is_some());
assert!(get_box("SQUARE").is_some()); assert!(get_box("nonexistent").is_none());
}
#[test]
fn test_get_safe_box() {
let safe = get_safe_box("double");
assert!(safe.ascii); }
#[test]
fn test_build_row_widths() {
let widths = [4, 4];
let row = SQUARE.build_row(&widths, RowLevel::HeadRow, true);
assert!(!row.is_empty());
assert!(row.contains('\u{253C}')); }
#[test]
fn test_cell_characters() {
assert_eq!(ASCII.cell_left(), '|');
assert_eq!(ASCII.cell_divider(), '|');
assert_eq!(ASCII.cell_right(), '|');
}
#[test]
fn test_rounded_box() {
const { assert!(!ROUNDED.ascii) };
assert_eq!(ROUNDED.top[0], '\u{256D}'); assert_eq!(ROUNDED.bottom[0], '\u{2570}'); }
#[test]
fn test_double_box() {
const { assert!(!DOUBLE.ascii) };
assert_eq!(DOUBLE.top[0], '\u{2554}'); assert_eq!(DOUBLE.top[1], '\u{2550}'); assert_eq!(DOUBLE.bottom[3], '\u{255D}'); }
#[test]
fn test_heavy_box() {
const { assert!(!HEAVY.ascii) };
assert_eq!(HEAVY.top[0], '\u{250F}'); assert_eq!(HEAVY.top[1], '\u{2501}'); assert_eq!(HEAVY.bottom[0], '\u{2517}'); }
#[test]
fn test_heavy_head_box() {
const { assert!(!HEAVY_HEAD.ascii) };
assert_eq!(HEAVY_HEAD.top[0], '\u{250F}'); assert_eq!(HEAVY_HEAD.bottom[0], '\u{2514}'); }
#[test]
fn test_minimal_box() {
const { assert!(!MINIMAL.ascii) };
assert_eq!(MINIMAL.top[0], ' ');
assert_eq!(MINIMAL.bottom[0], ' ');
assert_eq!(MINIMAL.head[2], '\u{2502}'); }
#[test]
fn test_simple_box() {
const { assert!(!SIMPLE.ascii) };
assert_eq!(SIMPLE.top[0], ' ');
assert_eq!(SIMPLE.head_row[1], '\u{2500}'); }
#[test]
fn test_simple_heavy_box() {
const { assert!(!SIMPLE_HEAVY.ascii) };
assert_eq!(SIMPLE_HEAVY.head_row[1], '\u{2501}'); }
#[test]
fn test_ascii2_box() {
const { assert!(ASCII2.ascii) };
assert_eq!(ASCII2.head_row[0], '+');
assert_eq!(ASCII2.head_row[2], '+');
}
#[test]
fn test_ascii_double_head_box() {
const { assert!(ASCII_DOUBLE_HEAD.ascii) };
assert_eq!(ASCII_DOUBLE_HEAD.head_row[1], '=');
assert_eq!(ASCII_DOUBLE_HEAD.row[1], '-');
}
#[test]
fn test_get_row_chars_all_levels() {
assert_eq!(SQUARE.get_row_chars(RowLevel::Top), &SQUARE.top);
assert_eq!(SQUARE.get_row_chars(RowLevel::HeadRow), &SQUARE.head_row);
assert_eq!(SQUARE.get_row_chars(RowLevel::Mid), &SQUARE.mid);
assert_eq!(SQUARE.get_row_chars(RowLevel::Row), &SQUARE.row);
assert_eq!(SQUARE.get_row_chars(RowLevel::FootRow), &SQUARE.foot_row);
assert_eq!(SQUARE.get_row_chars(RowLevel::Bottom), &SQUARE.bottom);
}
#[test]
fn test_get_mid() {
let widths = [3, 3];
let mid = SQUARE.get_mid(&widths);
assert!(mid.starts_with('\u{251C}')); assert!(mid.ends_with('\u{2524}')); }
#[test]
fn test_get_row() {
let widths = [3, 3];
let row = SQUARE.get_row(&widths);
assert_eq!(row, SQUARE.get_mid(&widths));
}
#[test]
fn test_get_head_row() {
let widths = [3, 3];
let head_row = SQUARE.get_head_row(&widths);
assert!(head_row.contains('\u{253C}')); }
#[test]
fn test_build_row_no_edge() {
let widths = [3, 3];
let row = MINIMAL.build_row(&widths, RowLevel::Top, false);
assert!(!row.contains('\u{250C}')); }
#[test]
fn test_build_row_single_column() {
let widths = [5];
let top = ASCII.get_top(&widths);
assert_eq!(top, "+-----+");
}
#[test]
fn test_display_trait() {
let display = format!("{ASCII}");
assert!(display.contains('+'));
assert!(display.contains('-'));
assert!(display.contains('|'));
}
#[test]
fn test_substitute_ascii() {
let subst = ASCII.substitute(true);
assert!(subst.ascii);
assert_eq!(subst.top, ASCII.top);
assert_eq!(subst.head, ASCII.head);
assert_eq!(subst.bottom, ASCII.bottom);
}
#[test]
fn test_substitute_unicode() {
let subst = SQUARE.substitute(true);
assert!(subst.ascii);
assert_eq!(subst.top, ASCII.top);
assert_eq!(subst.head, ASCII.head);
assert_eq!(subst.bottom, ASCII.bottom);
}
#[test]
fn test_get_box_all_styles() {
assert!(get_box("ascii").is_some());
assert!(get_box("ascii2").is_some());
assert!(get_box("ascii_double_head").is_some());
assert!(get_box("rounded").is_some());
assert!(get_box("square").is_some());
assert!(get_box("double").is_some());
assert!(get_box("heavy").is_some());
assert!(get_box("heavy_head").is_some());
assert!(get_box("minimal").is_some());
assert!(get_box("minimal_heavy_head").is_some());
assert!(get_box("simple").is_some());
assert!(get_box("simple_heavy").is_some());
assert!(get_box("horizontals").is_some());
assert!(get_box("markdown").is_some());
}
#[test]
fn test_horizontals_box() {
const { assert!(!HORIZONTALS.ascii) };
assert_eq!(HORIZONTALS.head[2], ' ');
assert_eq!(HORIZONTALS.top[1], '\u{2500}'); assert_eq!(HORIZONTALS.head_row[1], '\u{2500}'); }
#[test]
fn test_markdown_box() {
const { assert!(MARKDOWN.ascii) };
assert_eq!(MARKDOWN.head[0], '|');
assert_eq!(MARKDOWN.head[2], '|');
assert_eq!(MARKDOWN.head_row[1], '-');
assert_eq!(MARKDOWN.top[0], ' ');
assert_eq!(MARKDOWN.bottom[0], ' ');
}
#[test]
fn test_minimal_heavy_head_box() {
const { assert!(!MINIMAL_HEAVY_HEAD.ascii) };
assert_eq!(MINIMAL_HEAVY_HEAD.top[0], ' ');
assert_eq!(MINIMAL_HEAVY_HEAD.bottom[0], ' ');
assert_eq!(MINIMAL_HEAVY_HEAD.head[2], '\u{2502}'); assert_eq!(MINIMAL_HEAVY_HEAD.head_row[1], '\u{2501}'); }
#[test]
fn test_get_box_case_insensitive() {
assert!(get_box("ASCII").is_some());
assert!(get_box("Ascii").is_some());
assert!(get_box("ROUNDED").is_some());
assert!(get_box("Rounded").is_some());
}
#[test]
fn test_get_safe_box_returns_ascii_for_ascii_input() {
let safe = get_safe_box("ascii");
assert!(safe.ascii);
}
#[test]
fn test_get_safe_box_unknown_returns_ascii() {
let safe = get_safe_box("nonexistent");
assert!(safe.ascii);
}
#[test]
fn test_cell_characters_unicode() {
assert_eq!(SQUARE.cell_left(), '\u{2502}'); assert_eq!(SQUARE.cell_divider(), '\u{2502}'); assert_eq!(SQUARE.cell_right(), '\u{2502}'); }
#[test]
fn test_empty_widths() {
let widths: [usize; 0] = [];
let top = ASCII.get_top(&widths);
assert_eq!(top, "+");
}
}