返回 CodeWhale
glyphs.rs
根目录 / crates / tui / src / tui / glyphs.rs
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 // Tideline action glyphs (spec §2): one cell each, no wide glyphs.
60 "⌁" => Some("+"),
61 "⚙" => Some("*"),
62 "↺" => Some("<"),
63 "▤" => Some("="),
64 "◐" => Some("*"),
65 "⑂" => Some("y"),
66 "∼" | "∿" => Some("~"),
67 "⋯" => Some("."),
68 "▖" | "▗" | "▘" | "▝" => Some("."),
69 "▚" => Some("\\"),
70 "▞" => Some("/"),
71 "░" | "▒" | "▓" => Some(":"),
72 "▱" => Some("-"),
73 "▶" | "▷" | "▸" | "›" | "❯" | "→" | "↗" | "↘" | "»" => Some(">"),
74 "◀" | "◂" | "‹" | "❮" | "←" | "↖" | "↙" | "«" => Some("<"),
75 "▼" | "▾" | "▽" | "↓" => Some("v"),
76 "▲" | "△" | "↑" => Some("^"),
77 "◆" | "◇" | "♦" | "✦" | "◍" | "◉" | "★" | "☆" => Some("*"),
78 "■" | "□" | "▪" | "▫" | "◼" | "◻" => Some("#"),
79 "●" | "○" | "∘" | "•" | "·" | "☐" => Some("."),
80 "◌" | "˚" | "°" | "◦" => Some("o"),
81 "✓" | "✔" | "☑" => Some("Y"),
82 "✕" | "×" | "⊘" | "✗" | "✘" | "☒" => Some("X"),
83 "⏸" => Some("="),
84 // Schedule/timer (the activity band's automation slot) — cron's `@`.
85 "⏱" => Some("@"),
86 // The launch warning line's gate glyph ("no model connected").
87 "⚠" => Some("!"),
88 // The working screen's MCP chip marker (`⋮ MCP n/m`).
89 "⋮" => Some("|"),
90 "≈≈>" => Some("~>"),
91 "≈" | "~" => Some("~"),
92 "🐳" | "🐋" => Some("w"),
93 "…" => Some("."),
94 _ => None,
95 }
96 }
97
98 /// Preserve the working-bubble fill signal when Braille is unavailable.
99 #[must_use]
100 pub fn braille_ascii_fallback(ch: char) -> Option<&'static str> {
101 if !(('\u{2800}'..='\u{28FF}').contains(&ch)) {
102 return None;
103 }
104 let dots = ((ch as u32) - 0x2800).count_ones();
105 Some(match dots {
106 0 => " ",
107 1..=2 => ".",
108 3..=4 => ":",
109 5..=6 => "+",
110 _ => "#",
111 })
112 }
113
114 #[cfg(test)]
115 mod tests {
116 use super::*;
117
118 #[test]
119 fn charter_has_narrow_semantic_fallbacks() {
120 for (rich, safe) in [
121 (SELECTION, ">"),
122 ("▷", ">"),
123 (CURRENT, "."),
124 (USER, "|"),
125 (DONE, "Y"),
126 (FAILED, "X"),
127 (ATTENTION, "*"),
128 ("≈≈>", "~>"),
129 ("≈", "~"),
130 ("~", "~"),
131 ("⌁", "+"),
132 ("↺", "<"),
133 ("▤", "="),
134 ("◐", "*"),
135 ("⑂", "y"),
136 ("∼", "~"),
137 ("∿", "~"),
138 ("⋯", "."),
139 ("⏱", "@"),
140 ("🐳", "w"),
141 ("🐋", "w"),
142 ] {
143 assert_eq!(ascii_fallback(rich), Some(safe));
144 }
145 assert_eq!(braille_ascii_fallback('\u{2801}'), Some("."));
146 assert_eq!(braille_ascii_fallback('A'), None);
147 }
148 }
149
149 lines RUST