Skip to main content

yarli_cli/stream/
spinner.rs

1//! Braille spinner for active task indicators.
2//!
3//! Uses fixed-width braille dots (`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) to avoid layout jitter.
4//! Transitions to `✓`/`✗` on completion (Section 30).
5
6/// Braille spinner frames — each character occupies the same visual width.
7const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
8
9/// A braille spinner that advances through frames.
10#[derive(Debug, Clone)]
11pub struct Spinner {
12    frame_idx: usize,
13}
14
15impl Default for Spinner {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl Spinner {
22    pub fn new() -> Self {
23        Self { frame_idx: 0 }
24    }
25
26    /// Get the current frame character.
27    pub fn frame(&self) -> char {
28        FRAMES[self.frame_idx % FRAMES.len()]
29    }
30
31    /// Advance to the next frame.
32    pub fn tick(&mut self) {
33        self.frame_idx = (self.frame_idx + 1) % FRAMES.len();
34    }
35}
36
37/// Terminal glyphs for task states (Section 16.3).
38pub const GLYPH_COMPLETE: char = '✓';
39pub const GLYPH_FAILED: char = '✗';
40pub const GLYPH_BLOCKED: char = '◌';
41pub const GLYPH_PENDING: char = '○';
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn spinner_cycles_through_frames() {
49        let mut s = Spinner::new();
50        assert_eq!(s.frame(), '⠋');
51        s.tick();
52        assert_eq!(s.frame(), '⠙');
53        for _ in 0..9 {
54            s.tick();
55        }
56        // Back to start after 10 ticks total
57        assert_eq!(s.frame(), '⠋');
58    }
59
60    #[test]
61    fn spinner_wraps_around() {
62        let mut s = Spinner::new();
63        for _ in 0..25 {
64            s.tick();
65        }
66        // 25 % 10 = 5
67        assert_eq!(s.frame(), FRAMES[5]);
68    }
69
70    #[test]
71    fn all_frames_are_unique() {
72        let mut seen = std::collections::HashSet::new();
73        for &f in FRAMES {
74            assert!(seen.insert(f), "duplicate frame: {f}");
75        }
76    }
77}