use crossterm::event::{Event as CrosstermEvent, KeyEvent, MouseEvent};
use futures_util::stream::Stream;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum AppEvent {
Input(InputEvent),
Tick,
Quit,
Resize(u16, u16),
Custom(String),
}
#[derive(Debug, Clone)]
pub enum InputEvent {
Key(KeyEvent),
Mouse(MouseEvent),
FocusGained,
FocusLost,
Paste(String),
}
pub struct EventHandler {
event_rx: mpsc::UnboundedReceiver<AppEvent>,
_event_tx: mpsc::UnboundedSender<AppEvent>,
}
impl EventHandler {
pub fn new() -> Self {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let tx = event_tx.clone();
tokio::spawn(async move {
let mut event_stream = crossterm::event::EventStream::new();
loop {
use futures_util::StreamExt;
if let Some(Ok(event)) = event_stream.next().await {
let app_event = match event {
CrosstermEvent::Key(key) => {
if key.code == crossterm::event::KeyCode::Char('c')
&& key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL)
{
AppEvent::Quit
} else {
AppEvent::Input(InputEvent::Key(key))
}
}
CrosstermEvent::Mouse(mouse) => AppEvent::Input(InputEvent::Mouse(mouse)),
CrosstermEvent::Resize(width, height) => AppEvent::Resize(width, height),
CrosstermEvent::FocusGained => AppEvent::Input(InputEvent::FocusGained),
CrosstermEvent::FocusLost => AppEvent::Input(InputEvent::FocusLost),
CrosstermEvent::Paste(data) => AppEvent::Input(InputEvent::Paste(data)),
};
if tx.send(app_event).is_err() {
break;
}
}
}
});
Self {
event_rx,
_event_tx: event_tx,
}
}
pub async fn next(&mut self) -> Option<AppEvent> {
self.event_rx.recv().await
}
pub fn try_next(&mut self) -> Option<AppEvent> {
self.event_rx.try_recv().ok()
}
}
impl Stream for EventHandler {
type Item = AppEvent;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.event_rx.poll_recv(cx)
}
}
#[derive(Debug, Clone)]
pub struct KeybindHandler {
bindings: std::collections::HashMap<String, String>,
leader_key: Option<String>,
leader_sequence: bool,
}
impl KeybindHandler {
pub fn new() -> Self {
Self {
bindings: std::collections::HashMap::new(),
leader_key: None,
leader_sequence: false,
}
}
pub fn set_leader_key(&mut self, key: String) {
self.leader_key = Some(key);
}
pub fn bind(&mut self, key: String, action: String) {
self.bindings.insert(key, action);
}
pub fn handle_key(&mut self, key: &KeyEvent) -> Option<String> {
let key_string = self.key_to_string(key);
if let Some(leader) = &self.leader_key {
if key_string == *leader && !self.leader_sequence {
self.leader_sequence = true;
return None;
}
}
if self.leader_sequence {
self.leader_sequence = false;
let leader_binding = format!("leader+{}", key_string);
return self.bindings.get(&leader_binding).cloned();
}
self.bindings.get(&key_string).cloned()
}
fn key_to_string(&self, key: &KeyEvent) -> String {
use crossterm::event::{KeyCode, KeyModifiers};
let mut parts = Vec::new();
if key.modifiers.contains(KeyModifiers::CONTROL) {
parts.push("ctrl");
}
if key.modifiers.contains(KeyModifiers::ALT) {
parts.push("alt");
}
if key.modifiers.contains(KeyModifiers::SHIFT) {
parts.push("shift");
}
let key_part = match key.code {
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "enter".to_string(),
KeyCode::Tab => "tab".to_string(),
KeyCode::Backspace => "backspace".to_string(),
KeyCode::Delete => "delete".to_string(),
KeyCode::Insert => "insert".to_string(),
KeyCode::Home => "home".to_string(),
KeyCode::End => "end".to_string(),
KeyCode::PageUp => "pageup".to_string(),
KeyCode::PageDown => "pagedown".to_string(),
KeyCode::Up => "up".to_string(),
KeyCode::Down => "down".to_string(),
KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(),
KeyCode::Esc => "esc".to_string(),
KeyCode::F(n) => format!("f{}", n),
_ => "unknown".to_string(),
};
parts.push(&key_part);
parts.join("+")
}
}
impl Default for KeybindHandler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MouseHandler {
last_position: (u16, u16),
drag_state: Option<DragState>,
}
#[derive(Debug, Clone)]
struct DragState {
start_position: (u16, u16),
current_position: (u16, u16),
}
impl MouseHandler {
pub fn new() -> Self {
Self {
last_position: (0, 0),
drag_state: None,
}
}
pub fn handle_mouse(&mut self, event: &MouseEvent) -> MouseAction {
use crossterm::event::{MouseButton, MouseEventKind};
match event.kind {
MouseEventKind::Down(button) => {
self.last_position = (event.column, event.row);
match button {
MouseButton::Left => {
self.drag_state = Some(DragState {
start_position: (event.column, event.row),
current_position: (event.column, event.row),
});
MouseAction::LeftClick(event.column, event.row)
}
MouseButton::Right => MouseAction::RightClick(event.column, event.row),
MouseButton::Middle => MouseAction::MiddleClick(event.column, event.row),
}
}
MouseEventKind::Up(MouseButton::Left) => {
if let Some(drag) = self.drag_state.take() {
if drag.start_position != drag.current_position {
MouseAction::DragEnd(drag.start_position, drag.current_position)
} else {
MouseAction::LeftClick(event.column, event.row)
}
} else {
MouseAction::LeftClick(event.column, event.row)
}
}
MouseEventKind::Up(_) => MouseAction::None,
MouseEventKind::Drag(MouseButton::Left) => {
if let Some(ref mut drag) = self.drag_state {
drag.current_position = (event.column, event.row);
MouseAction::Drag(drag.start_position, drag.current_position)
} else {
MouseAction::None
}
}
MouseEventKind::Moved => {
self.last_position = (event.column, event.row);
MouseAction::Move(event.column, event.row)
}
MouseEventKind::ScrollDown => MouseAction::ScrollDown(event.column, event.row),
MouseEventKind::ScrollUp => MouseAction::ScrollUp(event.column, event.row),
MouseEventKind::ScrollLeft => MouseAction::ScrollLeft(event.column, event.row),
MouseEventKind::ScrollRight => MouseAction::ScrollRight(event.column, event.row),
_ => MouseAction::None,
}
}
}
impl Default for MouseHandler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub enum MouseAction {
None,
LeftClick(u16, u16),
RightClick(u16, u16),
MiddleClick(u16, u16),
Move(u16, u16),
Drag((u16, u16), (u16, u16)), DragEnd((u16, u16), (u16, u16)), ScrollUp(u16, u16),
ScrollDown(u16, u16),
ScrollLeft(u16, u16),
ScrollRight(u16, u16),
}
pub struct EventDispatcher {
handlers: Vec<Box<dyn EventConsumer>>,
}
impl EventDispatcher {
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
pub fn add_handler(&mut self, handler: Box<dyn EventConsumer>) {
self.handlers.push(handler);
}
pub fn dispatch(&mut self, event: &AppEvent) -> bool {
for handler in &mut self.handlers {
if handler.handle_event(event) {
return true; }
}
false
}
}
impl Default for EventDispatcher {
fn default() -> Self {
Self::new()
}
}
pub trait EventConsumer {
fn handle_event(&mut self, event: &AppEvent) -> bool;
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
#[test]
fn test_key_to_string() {
let handler = KeybindHandler::new();
let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert_eq!(handler.key_to_string(&key), "a");
let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
assert_eq!(handler.key_to_string(&key), "ctrl+a");
let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(handler.key_to_string(&key), "enter");
}
#[test]
fn test_keybind_handler() {
let mut handler = KeybindHandler::new();
handler.bind("q".to_string(), "quit".to_string());
handler.bind("ctrl+c".to_string(), "interrupt".to_string());
let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
assert_eq!(handler.handle_key(&key), Some("quit".to_string()));
let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert_eq!(handler.handle_key(&key), Some("interrupt".to_string()));
}
#[test]
fn test_leader_sequence() {
let mut handler = KeybindHandler::new();
handler.set_leader_key("ctrl+x".to_string());
handler.bind("leader+s".to_string(), "save".to_string());
let leader_key = KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL);
assert_eq!(handler.handle_key(&leader_key), None);
let bound_key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
assert_eq!(handler.handle_key(&bound_key), Some("save".to_string()));
}
}