1#![allow(dead_code)] pub const BRAILLE_UP: [char; 25] = [
17 ' ', '⢀', '⢠', '⢰', '⢸', '⡀', '⣀', '⣠', '⣰', '⣸', '⡄', '⣄', '⣤', '⣴', '⣼', '⡆', '⣆', '⣦', '⣶', '⣾', '⡇', '⣇', '⣧', '⣷', '⣿', ];
23
24pub const BRAILLE_DOWN: [char; 25] = [
26 ' ', '⠈', '⠘', '⠸', '⢸', '⠁', '⠉', '⠙', '⠹', '⢹', '⠃', '⠋', '⠛', '⠻', '⢻', '⠇', '⠏', '⠟', '⠿', '⢿', '⡇', '⡏', '⡟', '⡿', '⣿', ];
32
33pub const BLOCK_UP: [char; 25] = [
36 ' ', '▁', '▂', '▃', '▄', '▁', '▂', '▃', '▄', '▅', '▂', '▃', '▄', '▅', '▆', '▃', '▄', '▅', '▆', '▇', '▄', '▅', '▆', '▇', '█', ];
42
43pub const BLOCK_DOWN: [char; 25] = [
45 ' ', '▔', '▔', '▀', '▀', '▔', '▔', '▀', '▀', '█', '▔', '▀', '▀', '█', '█', '▀', '▀', '█', '█', '█', '▀', '█', '█', '█', '█', ];
51
52pub const TTY_UP: [char; 25] = [
54 ' ', '.', '.', 'o', 'o', '.', '.', 'o', 'o', 'O', '.', 'o', 'o', 'O', 'O', 'o', 'o', 'O', 'O', '#', 'o', 'O', 'O', '#', '#', ];
60
61pub const TTY_DOWN: [char; 25] = [
63 ' ', '\'', '\'', '"', '"', '\'', '\'', '"', '"', '*', '\'', '"', '"', '*', '*', '"', '"', '*', '*', '#', '"', '*', '*', '#', '#', ];
69
70pub const SPARKLINE: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
72
73pub const SUPERSCRIPT: [char; 10] = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
75
76pub const SUBSCRIPT: [char; 10] = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'];
78
79pub(super) const CATEGORY_FILLED: [char; 6] = ['●', '◐', '◑', '◒', '◓', '○'];
86
87pub(super) const CATEGORY_SHAPES: [char; 6] = ['●', '■', '▲', '◆', '★', '◯'];
90
91pub(super) const CATEGORY_STATUS: [char; 4] = ['✓', '⚠', '✗', '?'];
94
95#[inline]
98pub(super) const fn category_symbol(index: usize) -> char {
99 CATEGORY_FILLED[index % CATEGORY_FILLED.len()]
100}
101
102#[inline]
105pub(super) const fn shape_symbol(index: usize) -> char {
106 CATEGORY_SHAPES[index % CATEGORY_SHAPES.len()]
107}
108
109pub(super) const ARROWS_FLOW: [&str; 4] = ["━▶", "──▶", "···▶", "→"];
111
112#[inline]
115pub(super) const fn flow_arrow(strength: usize) -> &'static str {
116 let idx = if strength > 3 { 3 } else { strength };
118 ARROWS_FLOW[idx]
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub enum SymbolSet {
124 #[default]
126 Braille,
127 Block,
129 Tty,
131 Custom,
133}
134
135#[derive(Debug, Clone)]
137pub struct CustomSymbols {
138 pub up: [char; 25],
140 pub down: [char; 25],
142}
143
144impl Default for CustomSymbols {
145 fn default() -> Self {
146 Self {
147 up: BRAILLE_UP,
148 down: BRAILLE_DOWN,
149 }
150 }
151}
152
153impl CustomSymbols {
154 #[must_use]
157 pub fn from_chars(chars: &str) -> Option<Self> {
158 let chars: Vec<char> = chars.chars().collect();
159 if chars.len() < 50 {
160 return None;
161 }
162 let mut up = [' '; 25];
163 let mut down = [' '; 25];
164 up.copy_from_slice(&chars[0..25]);
165 down.copy_from_slice(&chars[25..50]);
166 Some(Self { up, down })
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct BrailleSymbols {
173 set: SymbolSet,
174 custom: Option<CustomSymbols>,
175}
176
177impl Default for BrailleSymbols {
178 fn default() -> Self {
179 Self::new(SymbolSet::default())
180 }
181}
182
183impl BrailleSymbols {
184 #[must_use]
186 pub fn new(set: SymbolSet) -> Self {
187 Self { set, custom: None }
188 }
189
190 #[must_use]
192 pub fn with_custom(custom: CustomSymbols) -> Self {
193 Self {
194 set: SymbolSet::Custom,
195 custom: Some(custom),
196 }
197 }
198
199 #[must_use]
201 pub fn set(&self) -> SymbolSet {
202 self.set
203 }
204
205 #[must_use]
207 pub fn up_chars(&self) -> &[char; 25] {
208 match self.set {
209 SymbolSet::Braille => &BRAILLE_UP,
210 SymbolSet::Block => &BLOCK_UP,
211 SymbolSet::Tty => &TTY_UP,
212 SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_UP, |c| &c.up),
213 }
214 }
215
216 #[must_use]
218 pub fn down_chars(&self) -> &[char; 25] {
219 match self.set {
220 SymbolSet::Braille => &BRAILLE_DOWN,
221 SymbolSet::Block => &BLOCK_DOWN,
222 SymbolSet::Tty => &TTY_DOWN,
223 SymbolSet::Custom => self.custom.as_ref().map_or(&BRAILLE_DOWN, |c| &c.down),
224 }
225 }
226
227 #[inline]
230 #[must_use]
231 pub fn char_up(&self, value: f64) -> char {
232 let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
233 self.up_chars()[level.min(4) * 5] }
235
236 #[inline]
238 #[must_use]
239 pub fn char_down(&self, value: f64) -> char {
240 let level = (value.clamp(0.0, 1.0) * 4.0).round() as usize;
241 self.down_chars()[level.min(4) * 5]
242 }
243
244 #[inline]
247 #[must_use]
248 pub fn char_pair(&self, left: u8, right: u8) -> char {
249 let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
250 self.up_chars()[idx]
251 }
252
253 #[inline]
255 #[must_use]
256 pub fn char_pair_down(&self, left: u8, right: u8) -> char {
257 let idx = (left.min(4) as usize) * 5 + (right.min(4) as usize);
258 self.down_chars()[idx]
259 }
260
261 #[inline]
264 #[must_use]
265 pub fn sparkline_char(value: f64) -> char {
266 let level = (value.clamp(0.0, 1.0) * 7.0).round() as usize;
267 SPARKLINE[level.min(7)]
268 }
269
270 #[must_use]
272 pub fn to_superscript(n: u32) -> String {
273 n.to_string()
274 .chars()
275 .filter_map(|c| c.to_digit(10).map(|d| SUPERSCRIPT[d as usize]))
276 .collect()
277 }
278
279 #[must_use]
281 pub fn to_subscript(n: u32) -> String {
282 n.to_string()
283 .chars()
284 .filter_map(|c| c.to_digit(10).map(|d| SUBSCRIPT[d as usize]))
285 .collect()
286 }
287}
288
289#[cfg(test)]
290#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
291mod tests {
292 use super::*;
293
294 #[test]
299 fn test_braille_up_array_length() {
300 assert_eq!(BRAILLE_UP.len(), 25);
301 }
302
303 #[test]
304 fn test_braille_down_array_length() {
305 assert_eq!(BRAILLE_DOWN.len(), 25);
306 }
307
308 #[test]
309 fn test_block_up_array_length() {
310 assert_eq!(BLOCK_UP.len(), 25);
311 }
312
313 #[test]
314 fn test_block_down_array_length() {
315 assert_eq!(BLOCK_DOWN.len(), 25);
316 }
317
318 #[test]
319 fn test_tty_up_array_length() {
320 assert_eq!(TTY_UP.len(), 25);
321 }
322
323 #[test]
324 fn test_tty_down_array_length() {
325 assert_eq!(TTY_DOWN.len(), 25);
326 }
327
328 #[test]
329 fn test_sparkline_array_length() {
330 assert_eq!(SPARKLINE.len(), 8);
331 }
332
333 #[test]
334 fn test_superscript_array_length() {
335 assert_eq!(SUPERSCRIPT.len(), 10);
336 }
337
338 #[test]
339 fn test_subscript_array_length() {
340 assert_eq!(SUBSCRIPT.len(), 10);
341 }
342
343 #[test]
348 fn test_braille_up_empty_is_space() {
349 assert_eq!(BRAILLE_UP[0], ' '); }
351
352 #[test]
353 fn test_braille_up_full_is_full() {
354 assert_eq!(BRAILLE_UP[24], '⣿'); }
356
357 #[test]
358 fn test_braille_up_left_only() {
359 assert_eq!(BRAILLE_UP[5], '⡀'); assert_eq!(BRAILLE_UP[10], '⡄'); assert_eq!(BRAILLE_UP[15], '⡆'); assert_eq!(BRAILLE_UP[20], '⡇'); }
364
365 #[test]
366 fn test_braille_up_right_only() {
367 assert_eq!(BRAILLE_UP[1], '⢀'); assert_eq!(BRAILLE_UP[2], '⢠'); assert_eq!(BRAILLE_UP[3], '⢰'); assert_eq!(BRAILLE_UP[4], '⢸'); }
372
373 #[test]
374 fn test_braille_down_empty_is_space() {
375 assert_eq!(BRAILLE_DOWN[0], ' ');
376 }
377
378 #[test]
379 fn test_braille_down_full_is_full() {
380 assert_eq!(BRAILLE_DOWN[24], '⣿');
381 }
382
383 #[test]
388 fn test_block_up_empty_is_space() {
389 assert_eq!(BLOCK_UP[0], ' ');
390 }
391
392 #[test]
393 fn test_block_up_full_is_full_block() {
394 assert_eq!(BLOCK_UP[24], '█');
395 }
396
397 #[test]
398 fn test_block_up_progression() {
399 assert_eq!(BLOCK_UP[0], ' ');
401 assert_eq!(BLOCK_UP[1], '▁');
402 assert_eq!(BLOCK_UP[2], '▂');
403 assert_eq!(BLOCK_UP[3], '▃');
404 assert_eq!(BLOCK_UP[4], '▄');
405 }
406
407 #[test]
412 fn test_tty_up_empty_is_space() {
413 assert_eq!(TTY_UP[0], ' ');
414 }
415
416 #[test]
417 fn test_tty_up_uses_ascii_only() {
418 for c in TTY_UP.iter() {
419 assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
420 }
421 }
422
423 #[test]
424 fn test_tty_down_uses_ascii_only() {
425 for c in TTY_DOWN.iter() {
426 assert!(c.is_ascii() || *c == ' ', "Non-ASCII char: {}", c);
427 }
428 }
429
430 #[test]
435 fn test_symbol_set_default_is_braille() {
436 assert_eq!(SymbolSet::default(), SymbolSet::Braille);
437 }
438
439 #[test]
440 fn test_symbol_set_variants() {
441 let _ = SymbolSet::Braille;
442 let _ = SymbolSet::Block;
443 let _ = SymbolSet::Tty;
444 let _ = SymbolSet::Custom;
445 }
446
447 #[test]
452 fn test_custom_symbols_default() {
453 let custom = CustomSymbols::default();
454 assert_eq!(custom.up, BRAILLE_UP);
455 assert_eq!(custom.down, BRAILLE_DOWN);
456 }
457
458 #[test]
459 fn test_custom_symbols_from_chars_valid() {
460 let chars: String = std::iter::repeat_n('X', 50).collect();
461 let custom = CustomSymbols::from_chars(&chars);
462 assert!(custom.is_some());
463 let custom = custom.unwrap();
464 assert_eq!(custom.up[0], 'X');
465 assert_eq!(custom.down[0], 'X');
466 }
467
468 #[test]
469 fn test_custom_symbols_from_chars_too_short() {
470 let custom = CustomSymbols::from_chars("short");
471 assert!(custom.is_none());
472 }
473
474 #[test]
475 fn test_custom_symbols_from_chars_49_chars() {
476 let chars: String = std::iter::repeat_n('X', 49).collect();
477 let custom = CustomSymbols::from_chars(&chars);
478 assert!(custom.is_none());
479 }
480
481 #[test]
486 fn test_braille_symbols_new_braille() {
487 let sym = BrailleSymbols::new(SymbolSet::Braille);
488 assert_eq!(sym.set(), SymbolSet::Braille);
489 }
490
491 #[test]
492 fn test_braille_symbols_new_block() {
493 let sym = BrailleSymbols::new(SymbolSet::Block);
494 assert_eq!(sym.set(), SymbolSet::Block);
495 }
496
497 #[test]
498 fn test_braille_symbols_new_tty() {
499 let sym = BrailleSymbols::new(SymbolSet::Tty);
500 assert_eq!(sym.set(), SymbolSet::Tty);
501 }
502
503 #[test]
504 fn test_braille_symbols_default() {
505 let sym = BrailleSymbols::default();
506 assert_eq!(sym.set(), SymbolSet::Braille);
507 }
508
509 #[test]
510 fn test_braille_symbols_with_custom() {
511 let custom = CustomSymbols::default();
512 let sym = BrailleSymbols::with_custom(custom);
513 assert_eq!(sym.set(), SymbolSet::Custom);
514 }
515
516 #[test]
521 fn test_up_chars_braille() {
522 let sym = BrailleSymbols::new(SymbolSet::Braille);
523 assert_eq!(sym.up_chars(), &BRAILLE_UP);
524 }
525
526 #[test]
527 fn test_up_chars_block() {
528 let sym = BrailleSymbols::new(SymbolSet::Block);
529 assert_eq!(sym.up_chars(), &BLOCK_UP);
530 }
531
532 #[test]
533 fn test_up_chars_tty() {
534 let sym = BrailleSymbols::new(SymbolSet::Tty);
535 assert_eq!(sym.up_chars(), &TTY_UP);
536 }
537
538 #[test]
539 fn test_up_chars_custom() {
540 let mut custom = CustomSymbols::default();
541 custom.up[0] = 'A';
542 let sym = BrailleSymbols::with_custom(custom);
543 assert_eq!(sym.up_chars()[0], 'A');
544 }
545
546 #[test]
547 fn test_down_chars_braille() {
548 let sym = BrailleSymbols::new(SymbolSet::Braille);
549 assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
550 }
551
552 #[test]
553 fn test_down_chars_block() {
554 let sym = BrailleSymbols::new(SymbolSet::Block);
555 assert_eq!(sym.down_chars(), &BLOCK_DOWN);
556 }
557
558 #[test]
559 fn test_down_chars_tty() {
560 let sym = BrailleSymbols::new(SymbolSet::Tty);
561 assert_eq!(sym.down_chars(), &TTY_DOWN);
562 }
563
564 #[test]
569 fn test_char_up_zero() {
570 let sym = BrailleSymbols::new(SymbolSet::Braille);
571 assert_eq!(sym.char_up(0.0), ' ');
572 }
573
574 #[test]
575 fn test_char_up_one() {
576 let sym = BrailleSymbols::new(SymbolSet::Braille);
577 assert_eq!(sym.char_up(1.0), '⡇'); }
579
580 #[test]
581 fn test_char_up_half() {
582 let sym = BrailleSymbols::new(SymbolSet::Braille);
583 let c = sym.char_up(0.5);
584 assert_eq!(c, '⡄'); }
586
587 #[test]
588 fn test_char_up_clamps_negative() {
589 let sym = BrailleSymbols::new(SymbolSet::Braille);
590 assert_eq!(sym.char_up(-1.0), ' ');
591 }
592
593 #[test]
594 fn test_char_up_clamps_over_one() {
595 let sym = BrailleSymbols::new(SymbolSet::Braille);
596 assert_eq!(sym.char_up(2.0), '⡇');
597 }
598
599 #[test]
604 fn test_char_down_zero() {
605 let sym = BrailleSymbols::new(SymbolSet::Braille);
606 assert_eq!(sym.char_down(0.0), ' ');
607 }
608
609 #[test]
610 fn test_char_down_one() {
611 let sym = BrailleSymbols::new(SymbolSet::Braille);
612 assert_eq!(sym.char_down(1.0), '⡇');
613 }
614
615 #[test]
620 fn test_char_pair_zero_zero() {
621 let sym = BrailleSymbols::new(SymbolSet::Braille);
622 assert_eq!(sym.char_pair(0, 0), ' ');
623 }
624
625 #[test]
626 fn test_char_pair_four_four() {
627 let sym = BrailleSymbols::new(SymbolSet::Braille);
628 assert_eq!(sym.char_pair(4, 4), '⣿');
629 }
630
631 #[test]
632 fn test_char_pair_left_only() {
633 let sym = BrailleSymbols::new(SymbolSet::Braille);
634 assert_eq!(sym.char_pair(2, 0), '⡄');
635 }
636
637 #[test]
638 fn test_char_pair_right_only() {
639 let sym = BrailleSymbols::new(SymbolSet::Braille);
640 assert_eq!(sym.char_pair(0, 2), '⢠');
641 }
642
643 #[test]
644 fn test_char_pair_clamps_left() {
645 let sym = BrailleSymbols::new(SymbolSet::Braille);
646 assert_eq!(sym.char_pair(10, 0), '⡇'); }
648
649 #[test]
650 fn test_char_pair_clamps_right() {
651 let sym = BrailleSymbols::new(SymbolSet::Braille);
652 assert_eq!(sym.char_pair(0, 10), '⢸'); }
654
655 #[test]
656 fn test_char_pair_down() {
657 let sym = BrailleSymbols::new(SymbolSet::Braille);
658 assert_eq!(sym.char_pair_down(0, 0), ' ');
659 assert_eq!(sym.char_pair_down(4, 4), '⣿');
660 }
661
662 #[test]
667 fn test_sparkline_char_zero() {
668 assert_eq!(BrailleSymbols::sparkline_char(0.0), '▁');
669 }
670
671 #[test]
672 fn test_sparkline_char_one() {
673 assert_eq!(BrailleSymbols::sparkline_char(1.0), '█');
674 }
675
676 #[test]
677 fn test_sparkline_char_half() {
678 let c = BrailleSymbols::sparkline_char(0.5);
680 assert_eq!(c, '▅');
681 }
682
683 #[test]
684 fn test_sparkline_char_clamps() {
685 assert_eq!(BrailleSymbols::sparkline_char(-0.5), '▁');
686 assert_eq!(BrailleSymbols::sparkline_char(1.5), '█');
687 }
688
689 #[test]
694 fn test_to_superscript_single_digit() {
695 assert_eq!(BrailleSymbols::to_superscript(5), "⁵");
696 }
697
698 #[test]
699 fn test_to_superscript_multi_digit() {
700 assert_eq!(BrailleSymbols::to_superscript(123), "¹²³");
701 }
702
703 #[test]
704 fn test_to_superscript_zero() {
705 assert_eq!(BrailleSymbols::to_superscript(0), "⁰");
706 }
707
708 #[test]
709 fn test_to_subscript_single_digit() {
710 assert_eq!(BrailleSymbols::to_subscript(5), "₅");
711 }
712
713 #[test]
714 fn test_to_subscript_multi_digit() {
715 assert_eq!(BrailleSymbols::to_subscript(123), "₁₂₃");
716 }
717
718 #[test]
719 fn test_to_subscript_zero() {
720 assert_eq!(BrailleSymbols::to_subscript(0), "₀");
721 }
722
723 #[test]
724 fn test_superscript_all_digits() {
725 let result = BrailleSymbols::to_superscript(1234567890);
726 assert_eq!(result, "¹²³⁴⁵⁶⁷⁸⁹⁰");
727 }
728
729 #[test]
730 fn test_subscript_all_digits() {
731 let result = BrailleSymbols::to_subscript(1234567890);
732 assert_eq!(result, "₁₂₃₄₅₆₇₈₉₀");
733 }
734
735 #[test]
740 fn test_index_calculation() {
741 for left in 0..5u8 {
743 for right in 0..5u8 {
744 let idx = (left as usize) * 5 + (right as usize);
745 assert!(idx < 25, "Index out of bounds: {}", idx);
746 }
747 }
748 }
749
750 #[test]
751 fn test_all_braille_up_unique() {
752 let mut seen = std::collections::HashSet::new();
753 for &c in BRAILLE_UP.iter() {
754 if c != ' ' {
756 assert!(seen.insert(c), "Duplicate character: {}", c);
757 }
758 }
759 }
760
761 #[test]
762 fn test_all_braille_down_unique() {
763 let mut seen = std::collections::HashSet::new();
764 for &c in BRAILLE_DOWN.iter() {
765 if c != ' ' {
766 assert!(seen.insert(c), "Duplicate character: {}", c);
767 }
768 }
769 }
770
771 #[test]
776 fn test_block_char_up() {
777 let sym = BrailleSymbols::new(SymbolSet::Block);
778 assert_eq!(sym.char_up(0.0), ' ');
779 assert_eq!(sym.char_up(1.0), '▄'); }
781
782 #[test]
783 fn test_block_char_pair() {
784 let sym = BrailleSymbols::new(SymbolSet::Block);
785 assert_eq!(sym.char_pair(0, 0), ' ');
786 assert_eq!(sym.char_pair(4, 4), '█');
787 }
788
789 #[test]
794 fn test_tty_char_up() {
795 let sym = BrailleSymbols::new(SymbolSet::Tty);
796 assert_eq!(sym.char_up(0.0), ' ');
797 assert_eq!(sym.char_up(1.0), 'o'); }
799
800 #[test]
801 fn test_tty_char_pair() {
802 let sym = BrailleSymbols::new(SymbolSet::Tty);
803 assert_eq!(sym.char_pair(0, 0), ' ');
804 assert_eq!(sym.char_pair(4, 4), '#');
805 }
806
807 #[test]
812 fn test_custom_without_data_uses_braille() {
813 let sym = BrailleSymbols {
815 set: SymbolSet::Custom,
816 custom: None,
817 };
818 assert_eq!(sym.up_chars(), &BRAILLE_UP);
820 assert_eq!(sym.down_chars(), &BRAILLE_DOWN);
821 }
822}