#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![warn(clippy::cargo)]
#![doc(html_root_url = "https://docs.rs/cli-boxes/0.1.0")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/sabry-awad97/boxen-rs/main/assets/logo.png"
)]
#![doc(
html_favicon_url = "https://raw.githubusercontent.com/sabry-awad97/boxen-rs/main/assets/favicon.ico"
)]
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use strum::{EnumIter, IntoEnumIterator};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct BoxChars {
pub top_left: char,
pub top: char,
pub top_right: char,
pub right: char,
pub bottom_right: char,
pub bottom: char,
pub bottom_left: char,
pub left: char,
}
macro_rules! box_chars {
($name:ident, $tl:literal, $t:literal, $tr:literal, $r:literal, $br:literal, $b:literal, $bl:literal, $l:literal, $doc:literal) => {
#[doc = $doc]
pub const $name: Self = Self {
top_left: $tl,
top: $t,
top_right: $tr,
right: $r,
bottom_right: $br,
bottom: $b,
bottom_left: $bl,
left: $l,
};
};
}
impl BoxChars {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn new(
top_left: char,
top: char,
top_right: char,
right: char,
bottom_right: char,
bottom: char,
bottom_left: char,
left: char,
) -> Self {
Self {
top_left,
top,
top_right,
right,
bottom_right,
bottom,
bottom_left,
left,
}
}
#[must_use]
pub fn builder() -> BoxCharsBuilder {
BoxCharsBuilder::default()
}
box_chars!(
NONE,
' ',
' ',
' ',
' ',
' ',
' ',
' ',
' ',
"Creates a box with no visible characters (all spaces).\n\nThis is useful when you want to maintain box structure without visible borders, or as a fallback when no box styling is desired."
);
box_chars!(
SINGLE,
'┌',
'─',
'┐',
'│',
'┘',
'─',
'└',
'│',
"Creates a box using single-line Unicode box-drawing characters.\n\nThis is the most commonly used box style, providing clean and professional-looking borders that work well in most terminal environments.\n\nCharacters used: `┌─┐│┘─└│`"
);
box_chars!(
DOUBLE,
'╔',
'═',
'╗',
'║',
'╝',
'═',
'╚',
'║',
"Creates a box using double-line Unicode box-drawing characters.\n\nThis style provides a bold, prominent appearance that's excellent for highlighting important content or creating emphasis in CLI applications.\n\nCharacters used: `╔═╗║╝═╚║`"
);
box_chars!(
ROUND,
'╭',
'─',
'╮',
'│',
'╯',
'─',
'╰',
'│',
"Creates a box using rounded corner Unicode box-drawing characters.\n\nThis style provides a softer, more modern appearance with curved corners that's aesthetically pleasing and less harsh than sharp corners.\n\nCharacters used: `╭─╮│╯─╰│`"
);
box_chars!(
BOLD,
'┏',
'━',
'┓',
'┃',
'┛',
'━',
'┗',
'┃',
"Creates a box using bold/thick line Unicode box-drawing characters.\n\nThis style provides maximum visual impact with thick, bold lines that command attention and create strong visual separation.\n\nCharacters used: `┏━┓┃┛━┗┃`"
);
box_chars!(
SINGLE_DOUBLE,
'╓',
'─',
'╖',
'║',
'╜',
'─',
'╙',
'║',
"Creates a box using single horizontal, double vertical box-drawing characters.\n\nThis mixed style combines single-line horizontal borders with double-line vertical borders, creating a unique visual effect.\n\nCharacters used: `╓─╖║╜─╙║`"
);
box_chars!(
DOUBLE_SINGLE,
'╒',
'═',
'╕',
'│',
'╛',
'═',
'╘',
'│',
"Creates a box using double horizontal, single vertical box-drawing characters.\n\nThis mixed style combines double-line horizontal borders with single-line vertical borders, creating an alternative visual effect to `SINGLE_DOUBLE`.\n\nCharacters used: `╒═╕│╛═╘│`"
);
box_chars!(
CLASSIC,
'+',
'-',
'+',
'|',
'+',
'-',
'+',
'|',
"Creates a box using classic ASCII characters for maximum compatibility.\n\nThis style uses only basic ASCII characters, ensuring compatibility with all terminal environments, including those that don't support Unicode.\n\nCharacters used: `+-+|+-+|`"
);
box_chars!(
ARROW,
'↘',
'↓',
'↙',
'←',
'↖',
'↑',
'↗',
'→',
"Creates a decorative box using arrow Unicode characters.\n\nThis unique style uses directional arrows to create an unconventional but eye-catching border effect. Best used for special emphasis or creative CLI applications.\n\nCharacters used: `↘↓↙←↖↑↗→`"
);
}
impl Default for BoxChars {
fn default() -> Self {
Self::SINGLE
}
}
impl fmt::Display for BoxChars {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}{}{}{}{}{}{}",
self.top_left,
self.top,
self.top_right,
self.right,
self.bottom_right,
self.bottom,
self.bottom_left,
self.left
)
}
}
#[derive(Debug, Clone)]
pub struct BoxCharsBuilder {
chars: BoxChars,
}
impl Default for BoxCharsBuilder {
fn default() -> Self {
Self {
chars: BoxChars::NONE,
}
}
}
impl BoxCharsBuilder {
#[must_use]
pub const fn corners(mut self, char: char) -> Self {
self.chars.top_left = char;
self.chars.top_right = char;
self.chars.bottom_left = char;
self.chars.bottom_right = char;
self
}
#[must_use]
pub const fn horizontal(mut self, char: char) -> Self {
self.chars.top = char;
self.chars.bottom = char;
self
}
#[must_use]
pub const fn vertical(mut self, char: char) -> Self {
self.chars.left = char;
self.chars.right = char;
self
}
#[must_use]
pub const fn top_left(mut self, char: char) -> Self {
self.chars.top_left = char;
self
}
#[must_use]
pub const fn top(mut self, char: char) -> Self {
self.chars.top = char;
self
}
#[must_use]
pub const fn top_right(mut self, char: char) -> Self {
self.chars.top_right = char;
self
}
#[must_use]
pub const fn right(mut self, char: char) -> Self {
self.chars.right = char;
self
}
#[must_use]
pub const fn bottom_right(mut self, char: char) -> Self {
self.chars.bottom_right = char;
self
}
#[must_use]
pub const fn bottom(mut self, char: char) -> Self {
self.chars.bottom = char;
self
}
#[must_use]
pub const fn bottom_left(mut self, char: char) -> Self {
self.chars.bottom_left = char;
self
}
#[must_use]
pub const fn left(mut self, char: char) -> Self {
self.chars.left = char;
self
}
#[must_use]
pub const fn build(self) -> BoxChars {
self.chars
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum BorderStyle {
None,
Single,
Double,
Round,
Bold,
SingleDouble,
DoubleSingle,
Classic,
Arrow,
}
impl From<BorderStyle> for BoxChars {
fn from(style: BorderStyle) -> Self {
match style {
BorderStyle::None => Self::NONE,
BorderStyle::Single => Self::SINGLE,
BorderStyle::Double => Self::DOUBLE,
BorderStyle::Round => Self::ROUND,
BorderStyle::Bold => Self::BOLD,
BorderStyle::SingleDouble => Self::SINGLE_DOUBLE,
BorderStyle::DoubleSingle => Self::DOUBLE_SINGLE,
BorderStyle::Classic => Self::CLASSIC,
BorderStyle::Arrow => Self::ARROW,
}
}
}
impl BorderStyle {
#[must_use]
pub fn chars(self) -> BoxChars {
BoxChars::from(self)
}
pub fn all() -> impl Iterator<Item = Self> {
Self::iter()
}
}
impl fmt::Display for BorderStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::None => "none",
Self::Single => "single",
Self::Double => "double",
Self::Round => "round",
Self::Bold => "bold",
Self::SingleDouble => "single_double",
Self::DoubleSingle => "double_single",
Self::Classic => "classic",
Self::Arrow => "arrow",
};
write!(f, "{s}")
}
}
impl FromStr for BorderStyle {
type Err = ParseBorderStyleError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let matches = |target: &str| -> bool {
if s.len() != target.len() {
return false;
}
s.bytes().zip(target.bytes()).all(|(a, b)| {
let a_norm = if a == b'-' {
b'_'
} else {
a.to_ascii_lowercase()
};
a_norm == b
})
};
if matches("none") {
Ok(Self::None)
} else if matches("single") {
Ok(Self::Single)
} else if matches("double") {
Ok(Self::Double)
} else if matches("round") {
Ok(Self::Round)
} else if matches("bold") {
Ok(Self::Bold)
} else if matches("single_double") {
Ok(Self::SingleDouble)
} else if matches("double_single") {
Ok(Self::DoubleSingle)
} else if matches("classic") {
Ok(Self::Classic)
} else if matches("arrow") {
Ok(Self::Arrow)
} else {
Err(ParseBorderStyleError::InvalidStyle(s.to_string()))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseBorderStyleError {
InvalidStyle(String),
}
impl fmt::Display for ParseBorderStyleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidStyle(style) => {
let suggestions = BorderStyle::all()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"Invalid border style: '{style}'. Did you mean one of: {suggestions}?"
)
}
}
}
}
impl std::error::Error for ParseBorderStyleError {}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_box_chars_constants() {
assert_eq!(BoxChars::NONE.top_left, ' ');
assert_eq!(BoxChars::SINGLE.top_left, '┌');
assert_eq!(BoxChars::DOUBLE.top_left, '╔');
assert_eq!(BoxChars::ROUND.top_left, '╭');
assert_eq!(BoxChars::BOLD.top_left, '┏');
assert_eq!(BoxChars::SINGLE_DOUBLE.top_left, '╓');
assert_eq!(BoxChars::DOUBLE_SINGLE.top_left, '╒');
assert_eq!(BoxChars::CLASSIC.top_left, '+');
assert_eq!(BoxChars::ARROW.top_left, '↘');
}
#[test]
fn test_box_chars_new() {
let custom = BoxChars::new('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
assert_eq!(custom.top_left, 'a');
assert_eq!(custom.top, 'b');
assert_eq!(custom.top_right, 'c');
assert_eq!(custom.right, 'd');
assert_eq!(custom.bottom_right, 'e');
assert_eq!(custom.bottom, 'f');
assert_eq!(custom.bottom_left, 'g');
assert_eq!(custom.left, 'h');
}
#[test]
fn test_box_chars_default() {
let default = BoxChars::default();
assert_eq!(default, BoxChars::SINGLE);
}
#[test]
fn test_box_chars_display() {
let single = BoxChars::SINGLE;
assert_eq!(single.to_string(), "┌─┐│┘─└│");
let custom = BoxChars::new('*', '-', '*', '|', '*', '-', '*', '|');
assert_eq!(custom.to_string(), "*-*|*-*|");
}
#[test]
fn test_box_chars_traits() {
let single = BoxChars::SINGLE;
let single_copy = single;
assert_eq!(single, single_copy);
let mut set = HashSet::new();
set.insert(single);
assert!(set.contains(&single));
let debug_str = format!("{single:?}");
assert!(debug_str.contains("BoxChars"));
}
#[test]
fn test_builder_pattern() {
let custom = BoxChars::builder()
.corners('*')
.horizontal('-')
.vertical('|')
.build();
assert_eq!(custom.top_left, '*');
assert_eq!(custom.top_right, '*');
assert_eq!(custom.bottom_left, '*');
assert_eq!(custom.bottom_right, '*');
assert_eq!(custom.top, '-');
assert_eq!(custom.bottom, '-');
assert_eq!(custom.left, '|');
assert_eq!(custom.right, '|');
}
#[test]
fn test_builder_individual_methods() {
let custom = BoxChars::builder()
.top_left('a')
.top('b')
.top_right('c')
.right('d')
.bottom_right('e')
.bottom('f')
.bottom_left('g')
.left('h')
.build();
assert_eq!(custom.top_left, 'a');
assert_eq!(custom.top, 'b');
assert_eq!(custom.top_right, 'c');
assert_eq!(custom.right, 'd');
assert_eq!(custom.bottom_right, 'e');
assert_eq!(custom.bottom, 'f');
assert_eq!(custom.bottom_left, 'g');
assert_eq!(custom.left, 'h');
}
#[test]
fn test_builder_chaining() {
let result = BoxChars::builder()
.corners('●')
.horizontal('═')
.vertical('║')
.top_left('╔') .build();
assert_eq!(result.top_left, '╔'); assert_eq!(result.top_right, '●'); assert_eq!(result.top, '═'); assert_eq!(result.left, '║'); }
#[test]
fn test_builder_default() {
let default_builder = BoxCharsBuilder::default();
let result = default_builder.build();
assert_eq!(result, BoxChars::NONE);
}
#[test]
fn test_border_style_enum() {
let styles = [
BorderStyle::None,
BorderStyle::Single,
BorderStyle::Double,
BorderStyle::Round,
BorderStyle::Bold,
BorderStyle::SingleDouble,
BorderStyle::DoubleSingle,
BorderStyle::Classic,
BorderStyle::Arrow,
];
assert_eq!(styles.len(), 9);
}
#[test]
fn test_border_style_to_box_chars() {
assert_eq!(BoxChars::from(BorderStyle::None), BoxChars::NONE);
assert_eq!(BoxChars::from(BorderStyle::Single), BoxChars::SINGLE);
assert_eq!(BoxChars::from(BorderStyle::Double), BoxChars::DOUBLE);
assert_eq!(BoxChars::from(BorderStyle::Round), BoxChars::ROUND);
assert_eq!(BoxChars::from(BorderStyle::Bold), BoxChars::BOLD);
assert_eq!(
BoxChars::from(BorderStyle::SingleDouble),
BoxChars::SINGLE_DOUBLE
);
assert_eq!(
BoxChars::from(BorderStyle::DoubleSingle),
BoxChars::DOUBLE_SINGLE
);
assert_eq!(BoxChars::from(BorderStyle::Classic), BoxChars::CLASSIC);
assert_eq!(BoxChars::from(BorderStyle::Arrow), BoxChars::ARROW);
}
#[test]
fn test_border_style_chars_method() {
let style = BorderStyle::Bold;
let chars = style.chars();
assert_eq!(chars, BoxChars::BOLD);
assert_eq!(chars.top, '━');
}
#[test]
fn test_border_style_display() {
assert_eq!(BorderStyle::None.to_string(), "none");
assert_eq!(BorderStyle::Single.to_string(), "single");
assert_eq!(BorderStyle::Double.to_string(), "double");
assert_eq!(BorderStyle::Round.to_string(), "round");
assert_eq!(BorderStyle::Bold.to_string(), "bold");
assert_eq!(BorderStyle::SingleDouble.to_string(), "single_double");
assert_eq!(BorderStyle::DoubleSingle.to_string(), "double_single");
assert_eq!(BorderStyle::Classic.to_string(), "classic");
assert_eq!(BorderStyle::Arrow.to_string(), "arrow");
}
#[test]
fn test_border_style_from_str() {
assert_eq!("none".parse::<BorderStyle>().unwrap(), BorderStyle::None);
assert_eq!(
"single".parse::<BorderStyle>().unwrap(),
BorderStyle::Single
);
assert_eq!(
"double".parse::<BorderStyle>().unwrap(),
BorderStyle::Double
);
assert_eq!("round".parse::<BorderStyle>().unwrap(), BorderStyle::Round);
assert_eq!("bold".parse::<BorderStyle>().unwrap(), BorderStyle::Bold);
assert_eq!(
"single_double".parse::<BorderStyle>().unwrap(),
BorderStyle::SingleDouble
);
assert_eq!(
"double_single".parse::<BorderStyle>().unwrap(),
BorderStyle::DoubleSingle
);
assert_eq!(
"classic".parse::<BorderStyle>().unwrap(),
BorderStyle::Classic
);
assert_eq!("arrow".parse::<BorderStyle>().unwrap(), BorderStyle::Arrow);
}
#[test]
fn test_border_style_from_conversion() {
assert_eq!(BoxChars::from(BorderStyle::Single), BoxChars::SINGLE);
assert_eq!(BoxChars::from(BorderStyle::Double), BoxChars::DOUBLE);
assert_eq!(BoxChars::from(BorderStyle::Round), BoxChars::ROUND);
assert_eq!(BoxChars::from(BorderStyle::Bold), BoxChars::BOLD);
assert_eq!(
BoxChars::from(BorderStyle::SingleDouble),
BoxChars::SINGLE_DOUBLE
);
assert_eq!(
BoxChars::from(BorderStyle::DoubleSingle),
BoxChars::DOUBLE_SINGLE
);
assert_eq!(BoxChars::from(BorderStyle::Classic), BoxChars::CLASSIC);
assert_eq!(BoxChars::from(BorderStyle::Arrow), BoxChars::ARROW);
assert_eq!(BoxChars::from(BorderStyle::None), BoxChars::NONE);
}
#[test]
fn test_border_style_all() {
let all_styles: Vec<_> = BorderStyle::all().collect();
assert_eq!(all_styles.len(), 9);
assert!(all_styles.contains(&BorderStyle::Single));
assert!(all_styles.contains(&BorderStyle::Double));
}
#[test]
fn test_border_style_from_str_case_insensitive() {
assert_eq!(
"SINGLE".parse::<BorderStyle>().unwrap(),
BorderStyle::Single
);
assert_eq!(
"Double".parse::<BorderStyle>().unwrap(),
BorderStyle::Double
);
assert_eq!("ROUND".parse::<BorderStyle>().unwrap(), BorderStyle::Round);
assert_eq!("Bold".parse::<BorderStyle>().unwrap(), BorderStyle::Bold);
}
#[test]
fn test_border_style_from_str_kebab_case() {
assert_eq!(
"single-double".parse::<BorderStyle>().unwrap(),
BorderStyle::SingleDouble
);
assert_eq!(
"double-single".parse::<BorderStyle>().unwrap(),
BorderStyle::DoubleSingle
);
assert_eq!(
"SINGLE-DOUBLE".parse::<BorderStyle>().unwrap(),
BorderStyle::SingleDouble
);
assert_eq!(
"Double-Single".parse::<BorderStyle>().unwrap(),
BorderStyle::DoubleSingle
);
}
#[test]
fn test_border_style_from_str_invalid() {
assert!("invalid".parse::<BorderStyle>().is_err());
assert!("".parse::<BorderStyle>().is_err());
assert!("singleee".parse::<BorderStyle>().is_err());
assert!("single_".parse::<BorderStyle>().is_err());
}
#[test]
fn test_parse_border_style_error() {
let error = "invalid".parse::<BorderStyle>().unwrap_err();
assert_eq!(
error,
ParseBorderStyleError::InvalidStyle("invalid".to_string())
);
let error_display = error.to_string();
assert!(error_display.contains("Invalid border style: 'invalid'"));
assert!(error_display.contains("Did you mean one of:"));
assert!(error_display.contains("single"));
assert!(error_display.contains("double"));
}
#[test]
fn test_parse_border_style_error_traits() {
let error = ParseBorderStyleError::InvalidStyle("test".to_string());
let debug_str = format!("{error:?}");
assert!(debug_str.contains("InvalidStyle"));
let _: &dyn std::error::Error = &error;
}
#[test]
fn test_zero_allocation_parsing() {
assert!("single".parse::<BorderStyle>().is_ok());
assert!("singlee".parse::<BorderStyle>().is_err()); assert!("singl".parse::<BorderStyle>().is_err());
assert_eq!(
"SINGLE".parse::<BorderStyle>().unwrap(),
BorderStyle::Single
);
assert_eq!(
"single".parse::<BorderStyle>().unwrap(),
BorderStyle::Single
);
assert_eq!(
"Single".parse::<BorderStyle>().unwrap(),
BorderStyle::Single
);
assert_eq!(
"single-double".parse::<BorderStyle>().unwrap(),
BorderStyle::SingleDouble
);
assert_eq!(
"single_double".parse::<BorderStyle>().unwrap(),
BorderStyle::SingleDouble
);
}
#[test]
fn test_comprehensive_style_coverage() {
for style in BorderStyle::all() {
let chars = BoxChars::from(style);
let chars_method = style.chars();
assert_eq!(chars, chars_method);
let style_str = style.to_string();
let parsed_style: BorderStyle = style_str.parse().unwrap();
assert_eq!(style, parsed_style);
}
}
#[test]
fn test_unicode_characters() {
let single = BoxChars::SINGLE;
assert_eq!(single.top_left as u32, 0x250C); assert_eq!(single.top as u32, 0x2500);
let double = BoxChars::DOUBLE;
assert_eq!(double.top_left as u32, 0x2554); assert_eq!(double.top as u32, 0x2550);
let round = BoxChars::ROUND;
assert_eq!(round.top_left as u32, 0x256D); assert_eq!(round.bottom_right as u32, 0x256F); }
#[cfg(feature = "serde")]
#[test]
fn test_serde_serialization() {
use serde_json;
let single = BoxChars::SINGLE;
let serialized = serde_json::to_string(&single).unwrap();
let deserialized: BoxChars = serde_json::from_str(&serialized).unwrap();
assert_eq!(single, deserialized);
assert!(serialized.contains("topLeft"));
assert!(serialized.contains("topRight"));
assert!(serialized.contains("bottomLeft"));
assert!(serialized.contains("bottomRight"));
}
#[test]
fn test_edge_cases() {
let empty = BoxChars::builder().build();
assert_eq!(empty, BoxChars::NONE);
let box1 = BoxChars::builder()
.corners('*')
.horizontal('-')
.vertical('|')
.build();
let box2 = BoxChars::builder()
.vertical('|')
.corners('*')
.horizontal('-')
.build();
assert_eq!(box1, box2);
let overridden = BoxChars::builder().corners('*').corners('#').build();
assert_eq!(overridden.top_left, '#');
assert_eq!(overridden.top_right, '#');
}
#[test]
fn test_memory_layout() {
use std::mem;
assert_eq!(mem::size_of::<BoxChars>(), mem::size_of::<char>() * 8);
assert!(mem::size_of::<BorderStyle>() <= mem::size_of::<u8>());
assert_eq!(mem::align_of::<BoxChars>(), mem::align_of::<char>());
}
}