返回 CodeWhale
header.rs
根目录 / crates / tui / src / tui / widgets / header.rs
1 //! Status-indicator frame resolution for the header chip cluster.
2 //!
3 //! The classic-shell `HeaderWidget` was removed with the classic shell in
4 //! 0.9.4 (rail unification); what remains here is the frame picker shared
5 //! by the underwater header.
6
7 use std::time::Instant;
8
9 /// Milliseconds between status-indicator frame advances. The original
10 /// `deepseek_squiggle` (v0.3.5 → v0.8.x) used 420 ms; the dot replacement
11 /// used the same cadence. Keep both at 420 ms so the visual rhythm matches
12 /// what long-time users remember.
13 const STATUS_INDICATOR_FRAME_MS: u128 = 420;
14
15 /// Geometric replacement frames shipped between v0.8.x and v0.8.29.
16 /// Every frame is one cell wide so the provider/model label never shifts
17 /// while the animation advances.
18 const STATUS_INDICATOR_DOT_FRAMES: &[&str] = &["◍", "◉", "◌", "◌", "◉", "◍"];
19
20 /// Resolve the current status-indicator frame to render in the header
21 /// chip cluster.
22 ///
23 /// `turn_started_at = None` (no active turn) returns the first frame so the
24 /// chip is *visible* but not animating — it's a chip, not a spinner. As
25 /// soon as a turn starts, the elapsed time keys the cycle.
26 ///
27 /// `mode` accepts the canonical names `"cw"`, `"dots"`, `"off"`. The whale
28 /// emoji chip is retired from the header (2026-07-23 product decision): the
29 /// whale lives in the terminal window title and the idle water, never
30 /// beside the model/mode chips. Legacy `"whale"` values (still present in
31 /// persisted settings) normalize to the typographic `cw` mark; unknown
32 /// values fall back to `"cw"` as well. `"off"` returns `None` so the
33 /// caller can hide the chip outright.
34 #[must_use]
35 pub fn header_status_indicator_frame(
36 turn_started_at: Option<Instant>,
37 mode: &str,
38 ) -> Option<&'static str> {
39 let frames: &[&str] = match mode.trim().to_ascii_lowercase().as_str() {
40 "off" | "none" | "hidden" | "false" => return None,
41 "dots" | "dot" => STATUS_INDICATOR_DOT_FRAMES,
42 // Canonical mark, legacy whale opt-ins, and unknown values all land
43 // on the static typographic mark so the header never reintroduces
44 // an emoji chip beside the model/mode cluster.
45 _ => return Some("cw"),
46 };
47 let elapsed_ms = turn_started_at
48 .map(|t| t.elapsed().as_millis())
49 .unwrap_or(0);
50 let idx = (elapsed_ms / STATUS_INDICATOR_FRAME_MS) as usize % frames.len();
51 Some(frames[idx])
52 }
53
54 #[cfg(test)]
55 mod tests {
56 #[test]
57 fn legacy_whale_indicator_settings_normalize_to_the_cw_mark() {
58 // The whale emoji chip is retired from the header (2026-07-23):
59 // persisted `status_indicator = "whale"` opt-ins render the static
60 // typographic mark instead, idle or mid-turn.
61 for legacy in ["whale", "🐳", "🐋"] {
62 assert_eq!(
63 super::header_status_indicator_frame(None, legacy),
64 Some("cw"),
65 "legacy mode {legacy:?} must normalize to the cw mark"
66 );
67 assert_eq!(
68 super::header_status_indicator_frame(Some(std::time::Instant::now()), legacy),
69 Some("cw"),
70 "legacy mode {legacy:?} must stay static mid-turn"
71 );
72 }
73 }
74
75 #[test]
76 fn cw_indicator_is_static_and_typographic() {
77 assert_eq!(super::header_status_indicator_frame(None, "cw"), Some("cw"));
78 assert_eq!(
79 super::header_status_indicator_frame(Some(std::time::Instant::now()), "cw"),
80 Some("cw")
81 );
82 }
83
84 #[test]
85 fn dots_indicator_uses_geometric_frames() {
86 let frame = super::header_status_indicator_frame(None, "dots");
87 assert_eq!(frame, Some("\u{25CD}"));
88 }
89
90 #[test]
91 fn off_indicator_returns_none_so_chip_is_hidden() {
92 assert!(super::header_status_indicator_frame(None, "off").is_none());
93 // Aliases mirror the parser in Settings.
94 assert!(super::header_status_indicator_frame(None, "none").is_none());
95 assert!(super::header_status_indicator_frame(None, "hidden").is_none());
96 assert!(super::header_status_indicator_frame(None, "false").is_none());
97 }
98
99 #[test]
100 fn unknown_indicator_mode_defaults_to_cw() {
101 let frame = super::header_status_indicator_frame(None, "wahel-typo");
102 assert_eq!(frame, Some("cw"));
103 }
104
105 #[test]
106 fn whale_glyphs_have_narrow_ascii_fallbacks() {
107 assert_eq!(crate::tui::glyphs::ascii_fallback("🐳"), Some("w"));
108 assert_eq!(crate::tui::glyphs::ascii_fallback("🐋"), Some("w"));
109 }
110 }
111
111 lines RUST