返回 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 // Filled marks stay a dot; hollow ones become `o` so CURRENT and
80 // AVAILABLE stay distinguishable on ASCII terminals.
81 "●" | "∘" | "•" | "·" => Some("."),
82 "○" | "☐" | "◌" | "˚" | "°" | "◦" => Some("o"),
83 "✓" | "✔" | "☑" => Some("Y"),
84 "✕" | "×" | "⊘" | "✗" | "✘" | "☒" => Some("X"),
85 "⏸" => Some("="),
86 // Schedule/timer (the activity band's automation slot) — cron's `@`.
87 "⏱" => Some("@"),
88 // The launch warning line's gate glyph ("no model connected").
89 "⚠" => Some("!"),
90 // The working screen's MCP chip marker (`⋮ MCP n/m`).
91 "⋮" => Some("|"),
92 "≈≈>" => Some("~>"),
93 "≈" | "~" => Some("~"),
94 "🐳" | "🐋" => Some("w"),
95 "…" => Some("."),
96 // Affordance and menu glyphs. Each fallback is one ASCII cell: the
97 // backend adapter swaps a single cell's symbol, so a wider spelling
98 // would shift every cell after it on the row.
99 "✎" => Some("e"),
100 "↳" | "⇒" => Some(">"),
101 "↩" => Some("<"),
102 "⎇" => Some("y"),
103 "◔" | "◎" => Some("*"),
104 "⎘" | "⧉" => Some("="),
105 "▣" | "⌘" => Some("#"),
106 "⚑" => Some("!"),
107 // A joined line break in a command preview: `;` keeps it reading as
108 // a separate command rather than a redirect.
109 "⏎" => Some(";"),
110 "⇧" => Some("^"),
111 "⌥" => Some("~"),
112 "ⓘ" => Some("i"),
113 _ => None,
114 }
115 }
116
117 /// Preserve the working-bubble fill signal when Braille is unavailable.
118 #[must_use]
119 pub fn braille_ascii_fallback(ch: char) -> Option<&'static str> {
120 if !(('\u{2800}'..='\u{28FF}').contains(&ch)) {
121 return None;
122 }
123 let dots = ((ch as u32) - 0x2800).count_ones();
124 Some(match dots {
125 0 => " ",
126 1..=2 => ".",
127 3..=4 => ":",
128 5..=6 => "+",
129 _ => "#",
130 })
131 }
132
133 #[cfg(test)]
134 mod tests {
135 use super::*;
136
137 #[test]
138 fn charter_has_narrow_semantic_fallbacks() {
139 for (rich, safe) in [
140 (SELECTION, ">"),
141 ("▷", ">"),
142 (CURRENT, "."),
143 (AVAILABLE, "o"),
144 (READY, "o"),
145 ("☐", "o"),
146 ("•", "."),
147 (NEUTRAL, "."),
148 (USER, "|"),
149 (DONE, "Y"),
150 (FAILED, "X"),
151 (ATTENTION, "*"),
152 ("≈≈>", "~>"),
153 ("≈", "~"),
154 ("~", "~"),
155 ("⌁", "+"),
156 ("↺", "<"),
157 ("▤", "="),
158 ("◐", "*"),
159 ("⑂", "y"),
160 ("∼", "~"),
161 ("∿", "~"),
162 ("⋯", "."),
163 ("⏱", "@"),
164 ("🐳", "w"),
165 ("🐋", "w"),
166 ("✎", "e"),
167 ("↳", ">"),
168 ("⎇", "y"),
169 ("◔", "*"),
170 ("⎘", "="),
171 ("▣", "#"),
172 ("⧉", "="),
173 ("⚑", "!"),
174 ("⏎", ";"),
175 ("⇧", "^"),
176 ("⌥", "~"),
177 ("⌘", "#"),
178 ("⇒", ">"),
179 ("◎", "*"),
180 ("↩", "<"),
181 ("ⓘ", "i"),
182 ] {
183 assert_eq!(ascii_fallback(rich), Some(safe));
184 }
185 assert_ne!(
186 ascii_fallback(CURRENT),
187 ascii_fallback(AVAILABLE),
188 "current and available must stay distinct in ASCII"
189 );
190 assert_eq!(braille_ascii_fallback('\u{2801}'), Some("."));
191 // The backend adapter replaces one cell's symbol: every single-glyph
192 // fallback must stay one ASCII cell or the rest of the row shifts.
193 for (rich, safe) in [("…", "."), ("✎", "e"), ("ⓘ", "i")] {
194 assert_eq!(ascii_fallback(rich), Some(safe));
195 assert_eq!(unicode_width::UnicodeWidthStr::width(safe), 1);
196 }
197 assert_eq!(braille_ascii_fallback('A'), None);
198 }
199 }
200
200 lines RUST