use std::io::Write;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Clone)]
pub enum WatchCommand {
RunAll,
RunFailed,
Rerun,
FilterByName(String),
Quit,
}
pub struct KeyHandler {
rx: async_channel::Receiver<WatchCommand>,
active: Arc<AtomicBool>,
_handle: std::thread::JoinHandle<()>,
}
impl KeyHandler {
pub fn new() -> ferridriver::error::Result<Self> {
use ferridriver::FerriError;
crossterm::terminal::enable_raw_mode()
.map_err(|e| FerriError::unsupported(format!("raw mode not supported: {e}")))?;
let _ = crossterm::terminal::disable_raw_mode();
let (tx, rx) = async_channel::bounded(16);
let active = Arc::new(AtomicBool::new(false));
let active_clone = Arc::clone(&active);
let handle = std::thread::Builder::new()
.name("ferridriver-keyhandler".into())
.spawn(move || key_poll_loop(&tx, &active_clone))
.map_err(|e| FerriError::backend(format!("spawn key handler: {e}")))?;
Ok(Self {
rx,
active,
_handle: handle,
})
}
pub async fn recv(&self) -> Option<WatchCommand> {
self.rx.recv().await.ok()
}
pub fn enter_interactive(&self) {
let _ = crossterm::terminal::enable_raw_mode();
self.active.store(true, Ordering::Release);
}
pub fn leave_interactive(&self) {
self.active.store(false, Ordering::Release);
let _ = crossterm::terminal::disable_raw_mode();
}
}
impl Drop for KeyHandler {
fn drop(&mut self) {
self.active.store(false, Ordering::Release);
let _ = crossterm::terminal::disable_raw_mode();
}
}
pub fn print_watch_hint() {
let mut stderr = std::io::stderr();
let _ = writeln!(stderr);
let _ = writeln!(stderr, "\x1b[2mWatching for changes...\x1b[0m");
let _ = writeln!(
stderr,
"\x1b[2mPress \x1b[0m\x1b[1ma\x1b[0m\x1b[2m to run all, \
\x1b[0m\x1b[1mf\x1b[0m\x1b[2m to run failed, \
\x1b[0m\x1b[1mp\x1b[0m\x1b[2m to filter, \
\x1b[0m\x1b[1mq\x1b[0m\x1b[2m to quit.\x1b[0m"
);
let _ = stderr.flush();
}
fn key_poll_loop(tx: &async_channel::Sender<WatchCommand>, active: &AtomicBool) {
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
loop {
if tx.is_closed() {
break;
}
if !active.load(Ordering::Acquire) {
std::thread::sleep(std::time::Duration::from_millis(50));
continue;
}
if !event::poll(std::time::Duration::from_millis(100)).unwrap_or(false) {
continue;
}
let Ok(Event::Key(key)) = event::read() else {
continue;
};
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
let _ = tx.try_send(WatchCommand::Quit);
break;
}
let cmd = match key.code {
KeyCode::Char('a') => Some(WatchCommand::RunAll),
KeyCode::Char('f') => Some(WatchCommand::RunFailed),
KeyCode::Char('q') => Some(WatchCommand::Quit),
KeyCode::Enter => Some(WatchCommand::Rerun),
KeyCode::Char('p') => {
let _ = crossterm::terminal::disable_raw_mode();
let pattern = read_filter_pattern();
let _ = crossterm::terminal::enable_raw_mode();
if pattern.is_empty() {
None
} else {
Some(WatchCommand::FilterByName(pattern))
}
},
_ => None,
};
if let Some(cmd) = cmd {
let is_quit = matches!(cmd, WatchCommand::Quit);
let _ = tx.try_send(cmd);
if is_quit {
break;
}
}
}
}
fn read_filter_pattern() -> String {
let mut stderr = std::io::stderr();
let _ = write!(stderr, "\r\n\x1b[1mFilter pattern:\x1b[0m ");
let _ = stderr.flush();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
input.trim().to_string()
} else {
String::new()
}
}