ferridriver_test/
interactive.rs1use std::io::Write;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16#[derive(Debug, Clone)]
18pub enum WatchCommand {
19 RunAll,
21 RunFailed,
23 Rerun,
25 FilterByName(String),
27 Quit,
29}
30
31pub struct KeyHandler {
33 rx: async_channel::Receiver<WatchCommand>,
34 active: Arc<AtomicBool>,
35 _handle: std::thread::JoinHandle<()>,
36}
37
38impl KeyHandler {
39 pub fn new() -> ferridriver::error::Result<Self> {
45 use ferridriver::FerriError;
46 crossterm::terminal::enable_raw_mode()
48 .map_err(|e| FerriError::unsupported(format!("raw mode not supported: {e}")))?;
49 let _ = crossterm::terminal::disable_raw_mode();
50
51 let (tx, rx) = async_channel::bounded(16);
52 let active = Arc::new(AtomicBool::new(false));
53 let active_clone = Arc::clone(&active);
54
55 let handle = std::thread::Builder::new()
56 .name("ferridriver-keyhandler".into())
57 .spawn(move || key_poll_loop(&tx, &active_clone))
58 .map_err(|e| FerriError::backend(format!("spawn key handler: {e}")))?;
59
60 Ok(Self {
61 rx,
62 active,
63 _handle: handle,
64 })
65 }
66
67 pub async fn recv(&self) -> Option<WatchCommand> {
69 self.rx.recv().await.ok()
70 }
71
72 pub fn enter_interactive(&self) {
75 let _ = crossterm::terminal::enable_raw_mode();
76 self.active.store(true, Ordering::Release);
77 }
78
79 pub fn leave_interactive(&self) {
82 self.active.store(false, Ordering::Release);
83 let _ = crossterm::terminal::disable_raw_mode();
84 }
85}
86
87impl Drop for KeyHandler {
88 fn drop(&mut self) {
89 self.active.store(false, Ordering::Release);
90 let _ = crossterm::terminal::disable_raw_mode();
91 }
92}
93
94pub fn print_watch_hint() {
96 let mut stderr = std::io::stderr();
97 let _ = writeln!(stderr);
98 let _ = writeln!(stderr, "\x1b[2mWatching for changes...\x1b[0m");
99 let _ = writeln!(
100 stderr,
101 "\x1b[2mPress \x1b[0m\x1b[1ma\x1b[0m\x1b[2m to run all, \
102 \x1b[0m\x1b[1mf\x1b[0m\x1b[2m to run failed, \
103 \x1b[0m\x1b[1mp\x1b[0m\x1b[2m to filter, \
104 \x1b[0m\x1b[1mq\x1b[0m\x1b[2m to quit.\x1b[0m"
105 );
106 let _ = stderr.flush();
107}
108
109fn key_poll_loop(tx: &async_channel::Sender<WatchCommand>, active: &AtomicBool) {
111 use crossterm::event::{self, Event, KeyCode, KeyModifiers};
112
113 loop {
114 if tx.is_closed() {
115 break;
116 }
117
118 if !active.load(Ordering::Acquire) {
121 std::thread::sleep(std::time::Duration::from_millis(50));
122 continue;
123 }
124
125 if !event::poll(std::time::Duration::from_millis(100)).unwrap_or(false) {
127 continue;
128 }
129
130 let Ok(Event::Key(key)) = event::read() else {
131 continue;
132 };
133
134 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
136 let _ = tx.try_send(WatchCommand::Quit);
137 break;
138 }
139
140 let cmd = match key.code {
141 KeyCode::Char('a') => Some(WatchCommand::RunAll),
142 KeyCode::Char('f') => Some(WatchCommand::RunFailed),
143 KeyCode::Char('q') => Some(WatchCommand::Quit),
144 KeyCode::Enter => Some(WatchCommand::Rerun),
145 KeyCode::Char('p') => {
146 let _ = crossterm::terminal::disable_raw_mode();
148 let pattern = read_filter_pattern();
149 let _ = crossterm::terminal::enable_raw_mode();
150 if pattern.is_empty() {
151 None
152 } else {
153 Some(WatchCommand::FilterByName(pattern))
154 }
155 },
156 _ => None,
157 };
158
159 if let Some(cmd) = cmd {
160 let is_quit = matches!(cmd, WatchCommand::Quit);
161 let _ = tx.try_send(cmd);
162 if is_quit {
163 break;
164 }
165 }
166 }
167}
168
169fn read_filter_pattern() -> String {
171 let mut stderr = std::io::stderr();
172 let _ = write!(stderr, "\r\n\x1b[1mFilter pattern:\x1b[0m ");
173 let _ = stderr.flush();
174
175 let mut input = String::new();
176 if std::io::stdin().read_line(&mut input).is_ok() {
177 input.trim().to_string()
178 } else {
179 String::new()
180 }
181}