| 1 | //! Codewhale's terminal glyph charter. |
| 2 | //! |
| 3 | //! Renderers use semantic names from this module instead of choosing visual |
| 4 | //! punctuation ad hoc. The solid current marker (`●`) is the recurring |
| 5 | //! Codewhale anchor: it marks the active speaker or current human choice. |
| 6 | //! ASCII-safe terminals receive the semantic fallback from the same owner. |
| 7 | |
| 8 | /// Current speaker or current human choice — the recurring identity anchor. |
| 9 | pub const CURRENT: &str = "●"; |
| 10 | /// Available but not current. |
| 11 | pub const AVAILABLE: &str = "○"; |
| 12 | /// Keyboard/list selection pointer. |
| 13 | pub const SELECTION: &str = "▸"; |
| 14 | /// Finished user-authored message marker. |
| 15 | pub const USER: &str = "▎"; |
| 16 | /// Transcript continuation rail, including its authored trailing space. |
| 17 | pub const TRANSCRIPT_RAIL: &str = "▏ "; |
| 18 | /// Settled successful state. |
| 19 | pub const DONE: &str = "✓"; |
| 20 | /// Settled failed state. |
| 21 | pub const FAILED: &str = "✕"; |
| 22 | /// State that needs human attention. |
| 23 | pub const ATTENTION: &str = "◆"; |
| 24 | /// Ready but not active. |
| 25 | pub const READY: &str = "○"; |
| 26 | /// Paused work. |
| 27 | pub const PAUSED: &str = "⏸"; |
| 28 | /// Fleet role marks share the same charter while retaining distinct shapes. |
| 29 | pub const ROLE_MANAGER: &str = "◆"; |
| 30 | pub const ROLE_BUILDER: &str = "■"; |
| 31 | pub const ROLE_REVIEWER: &str = "◇"; |
| 32 | pub const ROLE_VERIFIER: &str = CURRENT; |
| 33 | pub const ROLE_SYNTHESIZER: &str = "▲"; |
| 34 | pub const NEUTRAL: &str = "·"; |
| 35 | |
| 36 | #[must_use] |
| 37 | pub const fn selection_marker(selected: bool) -> &'static str { |
| 38 | if selected { SELECTION } else { " " } |
| 39 | } |
| 40 | |
| 41 | /// Reduce a single Codewhale-authored decorative glyph to narrow ASCII. |
| 42 | /// Language text and model/user content are intentionally outside this map. |
| 43 | #[must_use] |
| 44 | pub fn ascii_fallback(symbol: &str) -> Option<&'static str> { |
| 45 | match symbol { |
| 46 | "─" | "━" | "═" | "╌" | "╍" | "┄" | "┅" | "┈" | "┉" | "—" | "–" => { |
| 47 | Some("-") |
| 48 | } |
| 49 | "│" | "┃" | "║" | "╎" | "╏" | "▏" | "▎" | "▍" | "▌" | "▐" | "▕" => { |
| 50 | Some("|") |
| 51 | } |
| 52 | "┌" | "┐" | "└" | "┘" | "╭" | "╮" | "╰" | "╯" | "├" | "┤" | "┬" | "┴" | "┼" => { |
| 53 | Some("+") |
| 54 | } |
| 55 | "█" | "▉" | "▊" | "▋" | "▀" | "▄" | "▅" | "▆" | "▇" | "▙" | "▛" | "▜" | "▟" | "▰" => { |
| 56 | Some("#") |
| 57 | } |
| 58 | "▁" | "▂" | "▃" => Some("_"), |
| 59 | "▖" | "▗" | "▘" | "▝" => Some("."), |
| 60 | "▚" => Some("\\"), |
| 61 | "▞" => Some("/"), |
| 62 | "░" | "▒" | "▓" => Some(":"), |
| 63 | "▱" => Some("-"), |
| 64 | "▶" | "▷" | "▸" | "›" | "❯" | "→" | "↗" | "↘" | "»" => Some(">"), |
| 65 | "◀" | "◂" | "‹" | "❮" | "←" | "↖" | "↙" | "«" => Some("<"), |
| 66 | "▼" | "▾" | "▽" | "↓" => Some("v"), |
| 67 | "▲" | "△" | "↑" => Some("^"), |
| 68 | "◆" | "◇" | "♦" | "✦" | "◍" | "◉" | "★" | "☆" => Some("*"), |
| 69 | "■" | "□" | "▪" | "▫" | "◼" | "◻" => Some("#"), |
| 70 | "●" | "○" | "∘" | "•" | "·" | "☐" => Some("."), |
| 71 | "◌" | "˚" | "°" | "◦" => Some("o"), |
| 72 | "✓" | "✔" | "☑" => Some("Y"), |
| 73 | "✕" | "×" | "⊘" | "✗" | "✘" | "☒" => Some("X"), |
| 74 | "⏸" => Some("="), |
| 75 | "≈≈>" => Some("~>"), |
| 76 | "≈" | "~" => Some("~"), |
| 77 | "🐳" | "🐋" => Some("w"), |
| 78 | "…" => Some("."), |
| 79 | _ => None, |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | /// Preserve the working-bubble fill signal when Braille is unavailable. |
| 84 | #[must_use] |
| 85 | pub fn braille_ascii_fallback(ch: char) -> Option<&'static str> { |
| 86 | if !(('\u{2800}'..='\u{28FF}').contains(&ch)) { |
| 87 | return None; |
| 88 | } |
| 89 | let dots = ((ch as u32) - 0x2800).count_ones(); |
| 90 | Some(match dots { |
| 91 | 0 => " ", |
| 92 | 1..=2 => ".", |
| 93 | 3..=4 => ":", |
| 94 | 5..=6 => "+", |
| 95 | _ => "#", |
| 96 | }) |
| 97 | } |
| 98 | |
| 99 | #[cfg(test)] |
| 100 | mod tests { |
| 101 | use super::*; |
| 102 | |
| 103 | #[test] |
| 104 | fn charter_has_narrow_semantic_fallbacks() { |
| 105 | for (rich, safe) in [ |
| 106 | (SELECTION, ">"), |
| 107 | ("▷", ">"), |
| 108 | (CURRENT, "."), |
| 109 | (USER, "|"), |
| 110 | (DONE, "Y"), |
| 111 | (FAILED, "X"), |
| 112 | (ATTENTION, "*"), |
| 113 | ("≈≈>", "~>"), |
| 114 | ("≈", "~"), |
| 115 | ("~", "~"), |
| 116 | ] { |
| 117 | assert_eq!(ascii_fallback(rich), Some(safe)); |
| 118 | } |
| 119 | assert_eq!(braille_ascii_fallback('\u{2801}'), Some(".")); |
| 120 | assert_eq!(braille_ascii_fallback('A'), None); |
| 121 | } |
| 122 | } |
| 123 |