返回 CodeWhale
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 //! Extracted from the Codewhale engine's `crates/tui/src/tui/glyphs.rs`
9 //! (`Hmbown/CodeWhale` `58b1dd3dd`, last changed in `d7712814e`). One meaning
10 //! per glyph: `●` current or running, `✓` done, `✕` failed, `◆` needs you,
11 //! `○` ready, `■` stopped, `▸` selection, `?` unknown.
12
13 /// Current speaker or current human choice — the recurring identity anchor.
14 pub const CURRENT: &str = "●";
15 /// Available but not current.
16 pub const AVAILABLE: &str = "○";
17 /// Keyboard/list selection pointer.
18 pub const SELECTION: &str = "▸";
19 /// Finished user-authored message marker.
20 pub const USER: &str = "▎";
21 /// Transcript continuation rail, including its authored trailing space.
22 pub const TRANSCRIPT_RAIL: &str = "▏ ";
23 /// Settled successful state.
24 pub const DONE: &str = "✓";
25 /// Settled failed state.
26 pub const FAILED: &str = "✕";
27 /// State that needs human attention.
28 pub const ATTENTION: &str = "◆";
29 /// Ready but not active.
30 pub const READY: &str = "○";
31 /// Paused work.
32 pub const PAUSED: &str = "⏸";
33 /// Stopped by a person, or cancelled.
34 pub const STOPPED: &str = "■";
35 /// A state nobody reported. Never shown as a failure.
36 pub const UNKNOWN: &str = "?";
37 /// The one ellipsis.
38 pub const ELLIPSIS: &str = "…";
39 /// Fleet role marks share the same charter while retaining distinct shapes.
40 pub const ROLE_MANAGER: &str = "◆";
41 pub const ROLE_BUILDER: &str = "■";
42 pub const ROLE_REVIEWER: &str = "◇";
43 pub const ROLE_VERIFIER: &str = CURRENT;
44 pub const ROLE_SYNTHESIZER: &str = "▲";
45 pub const NEUTRAL: &str = "·";
46
47 #[must_use]
48 pub const fn selection_marker(selected: bool) -> &'static str {
49 if selected { SELECTION } else { " " }
50 }
51
52 /// `symbol`, or its ASCII fallback when `ascii` is set. Symbols without a
53 /// fallback are returned unchanged.
54 #[must_use]
55 pub fn pick(symbol: &'static str, ascii: bool) -> &'static str {
56 if ascii {
57 ascii_fallback(symbol).unwrap_or(symbol)
58 } else {
59 symbol
60 }
61 }
62
63 /// Reduce a single Codewhale-authored decorative glyph to narrow ASCII.
64 /// Language text and model/user content are intentionally outside this map.
65 #[must_use]
66 pub fn ascii_fallback(symbol: &str) -> Option<&'static str> {
67 match symbol {
68 "─" | "━" | "═" | "╌" | "╍" | "┄" | "┅" | "┈" | "┉" | "—" | "–" => {
69 Some("-")
70 }
71 "│" | "┃" | "║" | "╎" | "╏" | "▏" | "▎" | "▍" | "▌" | "▐" | "▕" => {
72 Some("|")
73 }
74 "┌" | "┐" | "└" | "┘" | "╭" | "╮" | "╰" | "╯" | "├" | "┤" | "┬" | "┴" | "┼" => {
75 Some("+")
76 }
77 "█" | "▉" | "▊" | "▋" | "▀" | "▄" | "▅" | "▆" | "▇" | "▙" | "▛" | "▜" | "▟" | "▰" => {
78 Some("#")
79 }
80 "▁" | "▂" | "▃" => Some("_"),
81 // Tideline action glyphs (spec §2): one cell each, no wide glyphs.
82 "⌁" => Some("+"),
83 "⚙" => Some("*"),
84 "↺" => Some("<"),
85 "▤" => Some("="),
86 "◐" => Some("*"),
87 "⑂" => Some("y"),
88 "∼" | "∿" => Some("~"),
89 "⋯" => Some("."),
90 "▖" | "▗" | "▘" | "▝" => Some("."),
91 "▚" => Some("\\"),
92 "▞" => Some("/"),
93 "░" | "▒" | "▓" => Some(":"),
94 "▱" => Some("-"),
95 "▶" | "▷" | "▸" | "›" | "❯" | "→" | "↗" | "↘" | "»" => Some(">"),
96 "◀" | "◂" | "‹" | "❮" | "←" | "↖" | "↙" | "«" => Some("<"),
97 "▼" | "▾" | "▽" | "↓" => Some("v"),
98 "▲" | "△" | "↑" => Some("^"),
99 "◆" | "◇" | "♦" | "✦" | "◍" | "◉" | "★" | "☆" => Some("*"),
100 "■" | "□" | "▪" | "▫" | "◼" | "◻" => Some("#"),
101 // Filled marks stay a dot; hollow ones become `o` so CURRENT and
102 // AVAILABLE stay distinguishable on ASCII terminals.
103 "●" | "∘" | "•" | "·" => Some("."),
104 "○" | "☐" | "◌" | "˚" | "°" | "◦" => Some("o"),
105 "✓" | "✔" | "☑" => Some("Y"),
106 "✕" | "×" | "⊘" | "✗" | "✘" | "☒" => Some("X"),
107 "⏸" => Some("="),
108 // Schedule/timer (the activity band's automation slot) — cron's `@`.
109 "⏱" => Some("@"),
110 // The launch warning line's gate glyph ("no model connected").
111 "⚠" => Some("!"),
112 // The working screen's MCP chip marker (`⋮ MCP n/m`).
113 "⋮" => Some("|"),
114 "≈≈>" => Some("~>"),
115 "≈" | "~" => Some("~"),
116 "🐳" | "🐋" => Some("w"),
117 "…" => Some("."),
118 "▏ " => Some("| "),
119 _ => None,
120 }
121 }
122
123 /// Preserve the working-bubble fill signal when Braille is unavailable.
124 #[must_use]
125 pub fn braille_ascii_fallback(ch: char) -> Option<&'static str> {
126 if !(('\u{2800}'..='\u{28FF}').contains(&ch)) {
127 return None;
128 }
129 let dots = ((ch as u32) - 0x2800).count_ones();
130 Some(match dots {
131 0 => " ",
132 1..=2 => ".",
133 3..=4 => ":",
134 5..=6 => "+",
135 _ => "#",
136 })
137 }
138
139 #[cfg(test)]
140 mod tests {
141 use super::*;
142
143 #[test]
144 fn charter_has_narrow_semantic_fallbacks() {
145 for (rich, safe) in [
146 (SELECTION, ">"),
147 ("▷", ">"),
148 (CURRENT, "."),
149 (AVAILABLE, "o"),
150 (READY, "o"),
151 ("☐", "o"),
152 ("•", "."),
153 (NEUTRAL, "."),
154 (USER, "|"),
155 (DONE, "Y"),
156 (FAILED, "X"),
157 (ATTENTION, "*"),
158 ("≈≈>", "~>"),
159 ("≈", "~"),
160 ("~", "~"),
161 ("⌁", "+"),
162 ("↺", "<"),
163 ("▤", "="),
164 ("◐", "*"),
165 ("⑂", "y"),
166 ("∼", "~"),
167 ("∿", "~"),
168 ("⋯", "."),
169 ("⏱", "@"),
170 ("🐳", "w"),
171 ("🐋", "w"),
172 (STOPPED, "#"),
173 (ELLIPSIS, "."),
174 ] {
175 assert_eq!(ascii_fallback(rich), Some(safe));
176 }
177 assert_ne!(
178 ascii_fallback(CURRENT),
179 ascii_fallback(AVAILABLE),
180 "current and available must stay distinct in ASCII"
181 );
182 assert_eq!(braille_ascii_fallback('\u{2801}'), Some("."));
183 assert_eq!(braille_ascii_fallback('A'), None);
184 }
185
186 #[test]
187 fn every_public_mark_has_an_ascii_form() {
188 for mark in [
189 CURRENT,
190 AVAILABLE,
191 SELECTION,
192 USER,
193 TRANSCRIPT_RAIL,
194 DONE,
195 FAILED,
196 ATTENTION,
197 READY,
198 PAUSED,
199 STOPPED,
200 UNKNOWN,
201 ELLIPSIS,
202 ROLE_MANAGER,
203 ROLE_BUILDER,
204 ROLE_REVIEWER,
205 ROLE_VERIFIER,
206 ROLE_SYNTHESIZER,
207 NEUTRAL,
208 selection_marker(true),
209 selection_marker(false),
210 ] {
211 let safe = pick(mark, true);
212 assert!(safe.is_ascii(), "{mark:?} -> {safe:?}");
213 assert_eq!(safe.len(), mark.chars().count(), "{mark:?} keeps its width");
214 }
215 }
216 }
217
217 lines RUST