use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyHint {
pub key: String,
pub description: String,
}
impl KeyHint {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
}
}
pub fn arrow(direction: &str, description: impl Into<String>) -> Self {
let arrow = match direction {
"up" | "↑" => "↑",
"down" | "↓" => "↓",
"left" | "←" => "←",
"right" | "→" => "→",
_ => direction,
};
Self::new(arrow, description)
}
pub fn format(&self) -> String {
format!("{} → {}", self.key, self.description)
}
}
pub trait OverlayInputHandler {
fn handle_key(&mut self, key: &KeyEvent) -> bool;
fn key_hints(&self) -> Vec<KeyHint>;
fn captures_all_input(&self) -> bool {
false
}
fn on_open(&mut self) {}
fn on_close(&mut self) {}
}
pub mod key_matchers {
use super::*;
pub fn is_tab(key: &KeyEvent) -> bool {
key.code == KeyCode::Tab && key.modifiers == KeyModifiers::NONE
}
pub fn is_backtab(key: &KeyEvent) -> bool {
key.code == KeyCode::BackTab
|| (key.code == KeyCode::Tab && key.modifiers.contains(KeyModifiers::SHIFT))
}
pub fn is_escape(key: &KeyEvent) -> bool {
key.code == KeyCode::Esc
}
pub fn is_enter(key: &KeyEvent) -> bool {
matches!(key.code, KeyCode::Enter | KeyCode::Char('\n'))
}
pub fn is_arrow(key: &KeyEvent) -> Option<Direction> {
match key.code {
KeyCode::Up => Some(Direction::Up),
KeyCode::Down => Some(Direction::Down),
KeyCode::Left => Some(Direction::Left),
KeyCode::Right => Some(Direction::Right),
_ => None,
}
}
pub fn is_char(key: &KeyEvent, c: char, modifiers: KeyModifiers) -> bool {
key.code == KeyCode::Char(c) && key.modifiers == modifiers
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
}
pub struct CompositeHandler {
handlers: Vec<Box<dyn OverlayInputHandler>>,
}
impl CompositeHandler {
pub fn new() -> Self {
Self {
handlers: Vec::new(),
}
}
pub fn add<H: OverlayInputHandler + 'static>(&mut self, handler: H) {
self.handlers.push(Box::new(handler));
}
}
impl Default for CompositeHandler {
fn default() -> Self {
Self::new()
}
}
impl OverlayInputHandler for CompositeHandler {
fn handle_key(&mut self, key: &KeyEvent) -> bool {
for handler in &mut self.handlers {
if handler.handle_key(key) {
return true;
}
}
false
}
fn key_hints(&self) -> Vec<KeyHint> {
self.handlers.iter().flat_map(|h| h.key_hints()).collect()
}
}
pub struct SimpleCloseHandler;
impl OverlayInputHandler for SimpleCloseHandler {
fn handle_key(&mut self, key: &KeyEvent) -> bool {
key_matchers::is_escape(key)
}
fn key_hints(&self) -> Vec<KeyHint> {
vec![KeyHint::new("Esc", "Close")]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
fn make_key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}
}
#[test]
fn test_key_hint_creation() {
let hint = KeyHint::new("Tab", "Cycle category");
assert_eq!(hint.key, "Tab");
assert_eq!(hint.description, "Cycle category");
assert_eq!(hint.format(), "Tab → Cycle category");
}
#[test]
fn test_key_hint_arrow() {
let hint = KeyHint::arrow("up", "Increase value");
assert_eq!(hint.key, "↑");
let hint2 = KeyHint::arrow("→", "Next item");
assert_eq!(hint2.key, "→");
}
#[test]
fn test_key_matchers() {
let tab = make_key(KeyCode::Tab, KeyModifiers::NONE);
assert!(key_matchers::is_tab(&tab));
let shift_tab = make_key(KeyCode::Tab, KeyModifiers::SHIFT);
assert!(key_matchers::is_backtab(&shift_tab));
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert!(key_matchers::is_escape(&esc));
let enter = make_key(KeyCode::Enter, KeyModifiers::NONE);
assert!(key_matchers::is_enter(&enter));
let up = make_key(KeyCode::Up, KeyModifiers::NONE);
assert_eq!(
key_matchers::is_arrow(&up),
Some(key_matchers::Direction::Up)
);
}
struct TestHandler {
last_key: Option<String>,
}
impl OverlayInputHandler for TestHandler {
fn handle_key(&mut self, key: &KeyEvent) -> bool {
match key.code {
KeyCode::Char(c) => {
self.last_key = Some(c.to_string());
true
}
_ => false,
}
}
fn key_hints(&self) -> Vec<KeyHint> {
vec![KeyHint::new("a", "Test action")]
}
}
#[test]
fn test_handler_trait() {
let mut handler = TestHandler { last_key: None };
let key = make_key(KeyCode::Char('a'), KeyModifiers::NONE);
assert!(handler.handle_key(&key));
assert_eq!(handler.last_key, Some("a".to_string()));
let key2 = make_key(KeyCode::Tab, KeyModifiers::NONE);
assert!(!handler.handle_key(&key2));
let hints = handler.key_hints();
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].key, "a");
}
#[test]
fn test_composite_handler() {
let mut composite = CompositeHandler::new();
struct HandlerA;
impl OverlayInputHandler for HandlerA {
fn handle_key(&mut self, key: &KeyEvent) -> bool {
key.code == KeyCode::Char('a')
}
fn key_hints(&self) -> Vec<KeyHint> {
vec![KeyHint::new("a", "Action A")]
}
}
struct HandlerB;
impl OverlayInputHandler for HandlerB {
fn handle_key(&mut self, key: &KeyEvent) -> bool {
key.code == KeyCode::Char('b')
}
fn key_hints(&self) -> Vec<KeyHint> {
vec![KeyHint::new("b", "Action B")]
}
}
composite.add(HandlerA);
composite.add(HandlerB);
assert!(composite.handle_key(&make_key(KeyCode::Char('a'), KeyModifiers::NONE)));
assert!(composite.handle_key(&make_key(KeyCode::Char('b'), KeyModifiers::NONE)));
assert!(!composite.handle_key(&make_key(KeyCode::Char('c'), KeyModifiers::NONE)));
let hints = composite.key_hints();
assert_eq!(hints.len(), 2);
}
#[test]
fn test_simple_close_handler() {
let mut handler = SimpleCloseHandler;
assert!(handler.handle_key(&make_key(KeyCode::Esc, KeyModifiers::NONE)));
assert!(!handler.handle_key(&make_key(KeyCode::Char('a'), KeyModifiers::NONE)));
let hints = handler.key_hints();
assert_eq!(hints.len(), 1);
assert_eq!(hints[0].key, "Esc");
}
}