#![allow(dead_code)]
pub const BRAILLE_UP: [char; 25] = [
' ', '⢀', '⢠', '⢰', '⢸', '⡀', '⣀', '⣠', '⣰', '⣸', '⡄', '⣄', '⣤', '⣴', '⣼', '⡆', '⣆', '⣦', '⣶', '⣾', '⡇', '⣇', '⣧', '⣷', '⣿', ];
pub const BRAILLE_DOWN: [char; 25] = [
' ', '⠈', '⠘', '⠸', '⢸', '⠁', '⠉', '⠙', '⠹', '⢹', '⠃', '⠋', '⠛', '⠻', '⢻', '⠇', '⠏', '⠟', '⠿', '⢿', '⡇', '⡏', '⡟', '⡿', '⣿', ];
pub const BLOCK_UP: [char; 25] = [
' ', '▁', '▂', '▃', '▄', '▁', '▂', '▃', '▄', '▅', '▂', '▃', '▄', '▅', '▆', '▃', '▄', '▅', '▆', '▇', '▄', '▅', '▆', '▇', '█', ];
pub const BLOCK_DOWN: [char; 25] = [
' ', '▔', '▔', '▀', '▀', '▔', '▔', '▀', '▀', '█', '▔', '▀', '▀', '█', '█', '▀', '▀', '█', '█', '█', '▀', '█', '█', '█', '█', ];
pub const TTY_UP: [char; 25] = [
' ', '.', '.', 'o', 'o', '.', '.', 'o', 'o', 'O', '.', 'o', 'o', 'O', 'O', 'o', 'o', 'O', 'O', '#', 'o', 'O', 'O', '#', '#', ];
pub const TTY_DOWN: [char; 25] = [
' ', '\'', '\'', '"', '"', '\'', '\'', '"', '"', '*', '\'', '"', '"', '*', '*', '"', '"', '*', '*', '#', '"', '*', '*', '#', '#', ];
pub const SPARKLINE: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
pub const SUPERSCRIPT: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
pub const SUBSCRIPT: [char; 10] = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
pub(super) const CATEGORY_FILLED: [char; 6] = ['●', '◐', '◑', '◒', '◓', '○'];
pub(super) const CATEGORY_SHAPES: [char; 6] = ['●', '■', '▲', '◆', '★', '◯'];
pub(super) const CATEGORY_STATUS: [char; 4] = ['✓', '⚠', '✗', '?'];
#[inline]
pub(super) const fn category_symbol(index: usize) -> char {
CATEGORY_FILLED[index % CATEGORY_FILLED.len()]
}
#[inline]
pub(super) const fn shape_symbol(index: usize) -> char {
CATEGORY_SHAPES[index % CATEGORY_SHAPES.len()]
}
pub(super) const ARROWS_FLOW: [&str; 4] = ["━▶", "──▶", "···▶", "→"];
#[inline]
pub(super) const fn flow_arrow(strength: usize) -> &'static str {
let idx = if strength > 3 { 3 } else { strength };
ARROWS_FLOW[idx]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SymbolSet {
#[default]
Braille,
Block,
Tty,
Custom,
}
#[derive(Debug, Clone)]
pub struct CustomSymbols {
pub up: [char; 25],
pub down: [char; 25],
}
impl Default for CustomSymbols {
fn default() -> Self {
Self {
up: BRAILLE_UP,
down: BRAILLE_DOWN,
}
}
}
impl CustomSymbols {
#[must_use]
pub fn from_chars(chars: &str) -> Option<Self> {
let chars: Vec<char> = chars.chars().collect();
if chars.len() < 50 {
return None;
}
let mut up = [' '; 25];
let mut down = [' '; 25];
up.copy_from_slice(&chars[0..25]);
down.copy_from_slice(&chars[25..50]);
Some(Self { up, down })
}
}
#[derive(Debug, Clone)]
pub struct BrailleSymbols {
set: SymbolSet,
custom: Option<CustomSymbols>,
}
impl Default for BrailleSymbols {
fn default() -> Self {
Self::new(SymbolSet::default())
}
}
impl BrailleSymbols {
#[must_use]
pub fn new(set: SymbolSet) -> Self {
Self { set, custom: None }
}
#[must_use]
pub fn with_custom(custom: CustomSymbols) -> Self {
Self {
set: SymbolSet::Custom,
custom: Some(custom),
}
}
#[must_use]
pub fn set(&self) -> SymbolSet {
self.set
}
#[must_use]
pub fn up_chars(&self) -> &[char; 25] {
match self.set {
SymbolSet::Braille => &BRAILLE_UP,
SymbolSet::Block => &BLOCK_UP,
SymbolSet::Tty => &TTY_UP,
SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_UP, |c| &c.up),
}
}
#[must_use]
pub fn down_chars(&self) -> &[char; 25] {
match self.set {
SymbolSet::Braille => &BRAILLE_DOWN,
SymbolSet::Block => &BLOCK_DOWN,
SymbolSet::Tty => &TTY_DOWN,
SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_DOWN, |c| &c.down),
}
}
#[inline]
#[must_use]
pub fn char_up(&self, value: f64) -> char {
let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
self.up_chars()[level.min(4) * 5] }
#[inline]
#[must_use]
pub fn char_down(&self, value: f64) -> char {
let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
self.down_chars()[level.min(4) * 5]
}
#[inline]
#[must_use]
pub fn char_pair(&self, left: u8, right: u8) -> char {
let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
self.up_chars()[idx]
}
#[inline]
#[must_use]
pub fn char_pair_down(&self, left: u8, right: u8) -> char {
let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
self.down_chars()[idx]
}
#[inline]
#[must_use]
pub fn sparkline_char(value: f64) -> char {
let level = (value.clamp(0.0, 1.0) * 7.0).round() as usize;
SPARKLINE[level.min(7)]
}
#[must_use]
pub fn to_superscript(n: u32) -> String {
n.to_string()
.chars()
.filter_map(|c| c.to_digit(10).map(|d| SUPERSCRIPT[d as usize]))
.collect()
}
#[must_use]
pub fn to_subscript(n: u32) -> String {
n.to_string()
.chars()
.filter_map(|c| c.to_digit(10).map(|d| SUBSCRIPT[d as usize]))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_braille_up_array_length() {
assert_eq!(BRAILLE_UP.len(), 25);
}
#[test]
fn test_braille_down_array_length() {
assert_eq!(BRAILLE_DOWN.len(), 25);
}
#[test]
fn test_block_up_array_length() {
assert_eq!(BLOCK_UP.len(), 25);
}
#[test]
fn test_block_down_array_length() {
assert_eq!(BLOCK_DOWN.len(), 25);
}
#[test]
fn test_tty_up_array_length() {
assert_eq!(TTY_UP.len(), 25);
}
#[test]
fn test_tty_down_array_length() {
assert_eq!(TTY_DOWN.len(), 25);
}
#[test]
fn test_sparkline_array_length() {
assert_eq!(SPARKLINE.len(), 8);
}
#[test]
fn test_superscript_array_length() {
assert_eq!(SUPERSCRIPT.len(), 10);
}
#[test]
fn test_subscript_array_length() {
assert_eq!(SUBSCRIPT.len(), 10);
}
#[test]
fn test_braille_up_empty_is_space() {
assert_eq!(BRAILLE_UP[0], ' '); }
#[test]
fn test_braille_up_full_is_full() {
assert_eq!(BRAILLE_UP[24], '⣿'); }
#[test]
fn test_braille_up_left_only() {
assert_eq!(BRAILLE_UP[5], '⡀'); assert_eq!(BRAILLE_UP[10], '⡄'); assert_eq!(BRAILLE_UP[15], '⡆'); assert_eq!(BRAILLE_UP[20], '⡇'); }
#[test]
fn test_braille_up_right_only() {
assert_eq!(BRAILLE_UP[1], '⢀'); assert_eq!(BRAILLE_UP[2], '⢠'); assert_eq!(BRAILLE_UP[3], '⢰'); assert_eq!(BRAILLE_UP[4], '⢸'); }
#[test]
fn test_braille_down_empty_is_space() {
assert_eq!(BRAILLE_DOWN[0], ' ');
}
#[test]
fn test_braille_down_full_is_full() {
assert_eq!(BRAILLE_DOWN[24], '⣿');
}
#[test]
fn test_block_up_empty_is_space() {
assert_eq!(BLOCK_UP[0], ' ');
}
#[test]
fn test_block_up_full_is_full_block() {
assert_eq!(BLOCK_UP[24], '█');
}
#[test]
fn test_block_up_progression() {
assert_eq!(BLOCK_UP[0], ' ');
assert_eq!(BLOCK_UP[1], '▁');
assert_eq!(BLOCK_UP[2], '▂');
assert_eq!(BLOCK_UP[3], '▃');
assert_eq!(BLOCK_UP[4], '▄');
}
#[test]
fn test_tty_up_empty_is_space() {
assert_eq!(TTY_UP[0], ' ');
}
#[test]
fn test_tty_up_uses_ascii_only() {
for c in TTY_UP.iter() {
assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
}
}
#[test]
fn test_tty_down_uses_ascii_only() {
for c in TTY_DOWN.iter() {
assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
}
}
#[test]
fn test_symbol_set_default_is_braille() {
assert_eq!(SymbolSet::default(), SymbolSet::Braille);
}
#[test]
fn test_symbol_set_variants() {
let _ = SymbolSet::Braille;
let _ = SymbolSet::Block;
let _ = SymbolSet::Tty;
let _ = SymbolSet::Custom;
}
#[test]
fn test_custom_symbols_default() {
let custom = CustomSymbols::default();
assert_eq!(custom.up, BRAILLE_UP);
assert_eq!(custom.down, BRAILLE_DOWN);
}
#[test]
fn test_custom_symbols_from_chars_valid() {
let chars: String = std::iter::repeat('X').take(50).collect();
let custom = CustomSymbols::from_chars(&chars);
assert!(custom.is_some());
let custom = custom.unwrap();
assert_eq!(custom.up[0], 'X');
assert_eq!(custom.down[0], 'X');
}
#[test]
fn test_custom_symbols_from_chars_too_short() {
let custom = CustomSymbols::from_chars("short");
assert!(custom.is_none());
}
#[test]
fn test_custom_symbols_from_chars_49_chars() {
let chars: String = std::iter::repeat('X').take(49).collect();
let custom = CustomSymbols::from_chars(&chars);
assert!(custom.is_none());
}
#[test]
fn test_braille_symbols_new_braille() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.set(), SymbolSet::Braille);
}
#[test]
fn test_braille_symbols_new_block() {
let sym = BrailleSymbols::new(SymbolSet::Block);
assert_eq!(sym.set(), SymbolSet::Block);
}
#[test]
fn test_braille_symbols_new_tty() {
let sym = BrailleSymbols::new(SymbolSet::Tty);
assert_eq!(sym.set(), SymbolSet::Tty);
}
#[test]
fn test_braille_symbols_default() {
let sym = BrailleSymbols::default();
assert_eq!(sym.set(), SymbolSet::Braille);
}
#[test]
fn test_braille_symbols_with_custom() {
let custom = CustomSymbols::default();
let sym = BrailleSymbols::with_custom(custom);
assert_eq!(sym.set(), SymbolSet::Custom);
}
#[test]
fn test_up_chars_braille() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.up_chars(), &BRAILLE_UP);
}
#[test]
fn test_up_chars_block() {
let sym = BrailleSymbols::new(SymbolSet::Block);
assert_eq!(sym.up_chars(), &BLOCK_UP);
}
#[test]
fn test_up_chars_tty() {
let sym = BrailleSymbols::new(SymbolSet::Tty);
assert_eq!(sym.up_chars(), &TTY_UP);
}
#[test]
fn test_up_chars_custom() {
let mut custom = CustomSymbols::default();
custom.up[0] = 'A';
let sym = BrailleSymbols::with_custom(custom);
assert_eq!(sym.up_chars()[0], 'A');
}
#[test]
fn test_down_chars_braille() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
}
#[test]
fn test_down_chars_block() {
let sym = BrailleSymbols::new(SymbolSet::Block);
assert_eq!(sym.down_chars(), &BLOCK_DOWN);
}
#[test]
fn test_down_chars_tty() {
let sym = BrailleSymbols::new(SymbolSet::Tty);
assert_eq!(sym.down_chars(), &TTY_DOWN);
}
#[test]
fn test_char_up_zero() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_up(0.0), ' ');
}
#[test]
fn test_char_up_one() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_up(1.0), '⡇'); }
#[test]
fn test_char_up_half() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
let c = sym.char_up(0.5);
assert_eq!(c, '⡄'); }
#[test]
fn test_char_up_clamps_negative() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_up(-1.0), ' ');
}
#[test]
fn test_char_up_clamps_over_one() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_up(2.0), '⡇');
}
#[test]
fn test_char_down_zero() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_down(0.0), ' ');
}
#[test]
fn test_char_down_one() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_down(1.0), '⡇');
}
#[test]
fn test_char_pair_zero_zero() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(0, 0), ' ');
}
#[test]
fn test_char_pair_four_four() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(4, 4), '⣿');
}
#[test]
fn test_char_pair_left_only() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(2, 0), '⡄');
}
#[test]
fn test_char_pair_right_only() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(0, 2), '⢠');
}
#[test]
fn test_char_pair_clamps_left() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(10, 0), '⡇'); }
#[test]
fn test_char_pair_clamps_right() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair(0, 10), '⢸'); }
#[test]
fn test_char_pair_down() {
let sym = BrailleSymbols::new(SymbolSet::Braille);
assert_eq!(sym.char_pair_down(0, 0), ' ');
assert_eq!(sym.char_pair_down(4, 4), '⣿');
}
#[test]
fn test_sparkline_char_zero() {
assert_eq!(BrailleSymbols::sparkline_char(0.0), '▁');
}
#[test]
fn test_sparkline_char_one() {
assert_eq!(BrailleSymbols::sparkline_char(1.0), '█');
}
#[test]
fn test_sparkline_char_half() {
let c = BrailleSymbols::sparkline_char(0.5);
assert_eq!(c, '▅');
}
#[test]
fn test_sparkline_char_clamps() {
assert_eq!(BrailleSymbols::sparkline_char(-0.5), '▁');
assert_eq!(BrailleSymbols::sparkline_char(1.5), '█');
}
#[test]
fn test_to_superscript_single_digit() {
assert_eq!(BrailleSymbols::to_superscript(5), "⁵");
}
#[test]
fn test_to_superscript_multi_digit() {
assert_eq!(BrailleSymbols::to_superscript(123), "¹²³");
}
#[test]
fn test_to_superscript_zero() {
assert_eq!(BrailleSymbols::to_superscript(0), "⁰");
}
#[test]
fn test_to_subscript_single_digit() {
assert_eq!(BrailleSymbols::to_subscript(5), "₅");
}
#[test]
fn test_to_subscript_multi_digit() {
assert_eq!(BrailleSymbols::to_subscript(123), "₁₂₃");
}
#[test]
fn test_to_subscript_zero() {
assert_eq!(BrailleSymbols::to_subscript(0), "₀");
}
#[test]
fn test_superscript_all_digits() {
let result = BrailleSymbols::to_superscript(1234567890);
assert_eq!(result, "¹²³⁴⁵⁶⁷⁸⁹⁰");
}
#[test]
fn test_subscript_all_digits() {
let result = BrailleSymbols::to_subscript(1234567890);
assert_eq!(result, "₁₂₃₄₅₆₇₈₉₀");
}
#[test]
fn test_index_calculation() {
for left in 0..5u8 {
for right in 0..5u8 {
let idx = (left as usize) * 5 + (right as usize);
assert!(idx < 25, "Index out of bounds: {}", idx);
}
}
}
#[test]
fn test_all_braille_up_unique() {
let mut seen = std::collections::HashSet::new();
for &c in BRAILLE_UP.iter() {
if c != ' ' {
assert!(seen.insert(c), "Duplicate character: {}", c);
}
}
}
#[test]
fn test_all_braille_down_unique() {
let mut seen = std::collections::HashSet::new();
for &c in BRAILLE_DOWN.iter() {
if c != ' ' {
assert!(seen.insert(c), "Duplicate character: {}", c);
}
}
}
#[test]
fn test_block_char_up() {
let sym = BrailleSymbols::new(SymbolSet::Block);
assert_eq!(sym.char_up(0.0), ' ');
assert_eq!(sym.char_up(1.0), '▄'); }
#[test]
fn test_block_char_pair() {
let sym = BrailleSymbols::new(SymbolSet::Block);
assert_eq!(sym.char_pair(0, 0), ' ');
assert_eq!(sym.char_pair(4, 4), '█');
}
#[test]
fn test_tty_char_up() {
let sym = BrailleSymbols::new(SymbolSet::Tty);
assert_eq!(sym.char_up(0.0), ' ');
assert_eq!(sym.char_up(1.0), 'o'); }
#[test]
fn test_tty_char_pair() {
let sym = BrailleSymbols::new(SymbolSet::Tty);
assert_eq!(sym.char_pair(0, 0), ' ');
assert_eq!(sym.char_pair(4, 4), '#');
}
#[test]
fn test_custom_without_data_uses_braille() {
let sym = BrailleSymbols {
set: SymbolSet::Custom,
custom: None,
};
assert_eq!(sym.up_chars(), &BRAILLE_UP);
assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
}
}