use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use std::time::Duration;
use crossterm::event::{self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers};
use super::app::{App, AppState};
#[derive(Debug, Clone)]
pub enum Event {
Key(KeyEvent),
Resize(u16, u16),
Tick,
LintComplete,
Error(String),
}
pub struct EventHandler {
rx: Receiver<Event>,
tx: Sender<Event>,
}
impl EventHandler {
pub fn new(tick_rate: Duration) -> Self {
let (tx, rx) = mpsc::channel();
let event_tx = tx.clone();
thread::spawn(move || {
loop {
if event::poll(tick_rate).unwrap_or(false) {
if let Ok(evt) = event::read() {
let event = match evt {
CrosstermEvent::Key(key) => Event::Key(key),
CrosstermEvent::Resize(w, h) => Event::Resize(w, h),
_ => continue,
};
if event_tx.send(event).is_err() {
break;
}
}
} else {
if event_tx.send(Event::Tick).is_err() {
break;
}
}
}
});
Self { rx, tx }
}
pub fn next(&self) -> Result<Event, mpsc::RecvError> {
self.rx.recv()
}
pub fn try_next(&self) -> Option<Event> {
self.rx.try_recv().ok()
}
pub fn sender(&self) -> Sender<Event> {
self.tx.clone()
}
}
pub fn handle_key_event(app: &mut App, key: KeyEvent) {
if key.code == KeyCode::Char('q')
|| (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
{
app.quit();
return;
}
match app.state {
AppState::ShowingHelp => {
app.toggle_help();
}
AppState::Running => {
handle_running_key(app, key);
}
AppState::Quitting => {}
}
}
fn handle_running_key(app: &mut App, key: KeyEvent) {
match key.code {
KeyCode::Up | KeyCode::Char('k') => app.move_up(),
KeyCode::Down | KeyCode::Char('j') => app.move_down(),
KeyCode::PageUp => {
for _ in 0..10 {
app.move_up();
}
}
KeyCode::PageDown => {
for _ in 0..10 {
app.move_down();
}
}
KeyCode::Home | KeyCode::Char('g') => {
app.issue_index = 0;
app.issue_scroll = 0;
}
KeyCode::End | KeyCode::Char('G') => {
let count = app.watch_state.issues().len();
if count > 0 {
app.issue_index = count - 1;
}
}
KeyCode::Tab => app.switch_focus(),
KeyCode::Enter => {
if app.open_in_editor().is_some() {
app.set_status("Opened in editor");
}
}
KeyCode::Char('r') => app.request_rerun(),
KeyCode::Char('c') => app.clear(),
KeyCode::Char('?') => app.toggle_help(),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::watch::WatchConfig;
fn make_key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::empty())
}
fn make_key_with_mod(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, modifiers)
}
#[test]
fn test_quit_with_q() {
let config = WatchConfig::default();
let mut app = App::new(config);
handle_key_event(&mut app, make_key(KeyCode::Char('q')));
assert_eq!(app.state, AppState::Quitting);
}
#[test]
fn test_quit_with_ctrl_c() {
let config = WatchConfig::default();
let mut app = App::new(config);
handle_key_event(
&mut app,
make_key_with_mod(KeyCode::Char('c'), KeyModifiers::CONTROL),
);
assert_eq!(app.state, AppState::Quitting);
}
#[test]
fn test_help_toggle() {
let config = WatchConfig::default();
let mut app = App::new(config);
handle_key_event(&mut app, make_key(KeyCode::Char('?')));
assert_eq!(app.state, AppState::ShowingHelp);
handle_key_event(&mut app, make_key(KeyCode::Char('a')));
assert_eq!(app.state, AppState::Running);
}
#[test]
fn test_navigation_keys() {
let config = WatchConfig::default();
let mut app = App::new(config);
let mut result = crate::utils::types::RunResult::new();
for i in 0..5 {
result.issues.push(crate::utils::types::LintIssue {
file_path: std::path::PathBuf::from("test.rs"),
line: i + 1,
column: None,
message: format!("Issue {}", i),
code: Some(format!("E00{}", i)),
severity: crate::Severity::Error,
source: Some("test".to_string()),
suggestion: None,
language: None,
code_line: None,
context_before: Vec::new(),
context_after: Vec::new(),
});
}
app.update_results(&result);
handle_key_event(&mut app, make_key(KeyCode::Char('j')));
assert_eq!(app.issue_index, 1);
handle_key_event(&mut app, make_key(KeyCode::Char('k')));
assert_eq!(app.issue_index, 0);
handle_key_event(&mut app, make_key(KeyCode::Down));
assert_eq!(app.issue_index, 1);
handle_key_event(&mut app, make_key(KeyCode::Up));
assert_eq!(app.issue_index, 0);
}
#[test]
fn test_tab_switches_focus() {
let config = WatchConfig::default();
let mut app = App::new(config);
use super::super::app::FocusedPanel;
assert_eq!(app.focused_panel, FocusedPanel::Issues);
handle_key_event(&mut app, make_key(KeyCode::Tab));
assert_eq!(app.focused_panel, FocusedPanel::Files);
handle_key_event(&mut app, make_key(KeyCode::Tab));
assert_eq!(app.focused_panel, FocusedPanel::Issues);
}
#[test]
fn test_rerun_request() {
let config = WatchConfig::default();
let mut app = App::new(config);
handle_key_event(&mut app, make_key(KeyCode::Char('r')));
assert!(app.force_rerun);
}
}