Skip to main content

testx/watcher/
terminal.rs

1use std::io::{self, Read};
2use std::sync::mpsc::{self, Receiver, TryRecvError};
3use std::thread;
4
5/// Actions the user can perform in watch mode.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum WatchAction {
8    /// Re-run all tests.
9    RunAll,
10    /// Re-run only failed tests.
11    RunFailed,
12    /// Quit watch mode.
13    Quit,
14    /// No action (continue waiting).
15    Continue,
16    /// Clear screen and re-run.
17    ClearAndRun,
18}
19
20/// Non-blocking keypresses reader for watch mode's interactive terminal.
21pub struct TerminalInput {
22    rx: Receiver<u8>,
23    _handle: thread::JoinHandle<()>,
24}
25
26impl TerminalInput {
27    /// Start reading stdin in a background thread.
28    pub fn new() -> Self {
29        let (tx, rx) = mpsc::channel();
30
31        let handle = thread::spawn(move || {
32            let stdin = io::stdin();
33            let mut buf = [0u8; 1];
34            loop {
35                match stdin.lock().read(&mut buf) {
36                    Ok(0) => break,
37                    Ok(_) => {
38                        if tx.send(buf[0]).is_err() {
39                            break;
40                        }
41                    }
42                    Err(_) => break,
43                }
44            }
45        });
46
47        Self {
48            rx,
49            _handle: handle,
50        }
51    }
52
53    /// Poll for a keypress (non-blocking).
54    pub fn poll(&self) -> WatchAction {
55        match self.rx.try_recv() {
56            Ok(key) => Self::key_to_action(key),
57            Err(TryRecvError::Empty) => WatchAction::Continue,
58            Err(TryRecvError::Disconnected) => WatchAction::Quit,
59        }
60    }
61
62    /// Convert a keypress to an action.
63    fn key_to_action(key: u8) -> WatchAction {
64        match key {
65            b'q' | b'Q' => WatchAction::Quit,
66            b'a' | b'A' => WatchAction::RunAll,
67            b'f' | b'F' => WatchAction::RunFailed,
68            b'c' | b'C' => WatchAction::ClearAndRun,
69            b'\n' | b'\r' => WatchAction::RunAll,
70            _ => WatchAction::Continue,
71        }
72    }
73}
74
75impl Default for TerminalInput {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81/// Clear the terminal screen.
82pub fn clear_screen() {
83    // ANSI escape: clear entire screen and move cursor to top-left
84    print!("\x1B[2J\x1B[1;1H");
85}
86
87/// Print the watch mode status bar.
88pub fn print_watch_status(changed_count: usize) {
89    use colored::Colorize;
90
91    println!();
92    println!(
93        "  {} {}",
94        "watching".cyan().bold(),
95        format!("{} file(s) changed", changed_count).dimmed(),
96    );
97    println!(
98        "  {} {}",
99        "keys:".dimmed(),
100        "a = run all · f = run failed · q = quit · Enter = re-run".dimmed()
101    );
102    println!();
103}
104
105/// Print a separator line for watch mode re-runs.
106pub fn print_watch_separator() {
107    use colored::Colorize;
108
109    println!();
110    println!(
111        "{}",
112        "════════════════════════════════════════════════════════════"
113            .cyan()
114            .dimmed()
115    );
116    println!();
117}
118
119/// Print watch mode startup message.
120pub fn print_watch_start(root: &std::path::Path) {
121    use colored::Colorize;
122
123    println!();
124    println!(
125        "  {} {} {}",
126        "testx".bold().cyan(),
127        "watch mode".bold(),
128        format!("({})", root.display()).dimmed(),
129    );
130    println!(
131        "  {} {}",
132        "keys:".dimmed(),
133        "a = run all · f = run failed · q = quit · Enter = re-run".dimmed()
134    );
135    println!("{}", "─".repeat(60).dimmed());
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn key_to_action_quit() {
144        assert_eq!(TerminalInput::key_to_action(b'q'), WatchAction::Quit);
145        assert_eq!(TerminalInput::key_to_action(b'Q'), WatchAction::Quit);
146    }
147
148    #[test]
149    fn key_to_action_run_all() {
150        assert_eq!(TerminalInput::key_to_action(b'a'), WatchAction::RunAll);
151        assert_eq!(TerminalInput::key_to_action(b'A'), WatchAction::RunAll);
152        assert_eq!(TerminalInput::key_to_action(b'\n'), WatchAction::RunAll);
153        assert_eq!(TerminalInput::key_to_action(b'\r'), WatchAction::RunAll);
154    }
155
156    #[test]
157    fn key_to_action_run_failed() {
158        assert_eq!(TerminalInput::key_to_action(b'f'), WatchAction::RunFailed);
159        assert_eq!(TerminalInput::key_to_action(b'F'), WatchAction::RunFailed);
160    }
161
162    #[test]
163    fn key_to_action_clear() {
164        assert_eq!(TerminalInput::key_to_action(b'c'), WatchAction::ClearAndRun);
165    }
166
167    #[test]
168    fn key_to_action_unknown() {
169        assert_eq!(TerminalInput::key_to_action(b'x'), WatchAction::Continue);
170        assert_eq!(TerminalInput::key_to_action(b'z'), WatchAction::Continue);
171    }
172
173    #[test]
174    fn watch_action_equality() {
175        assert_eq!(WatchAction::Quit, WatchAction::Quit);
176        assert_ne!(WatchAction::Quit, WatchAction::RunAll);
177    }
178
179    #[test]
180    fn clear_screen_does_not_panic() {
181        // This just tests that the function doesn't crash
182        // (output goes to stdout which is fine in tests)
183        clear_screen();
184    }
185}