use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
type EventHandler = dyn Fn() -> bool + Send + Sync + 'static;
static GLOBAL_EVENT_HANDLERS: Lazy<Mutex<HashMap<KeyCode, Vec<Arc<EventHandler>>>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn on_global_event<F>(key: KeyCode, handler: F)
where
F: Fn() -> bool + Send + Sync + 'static,
{
let mut handlers = GLOBAL_EVENT_HANDLERS.lock();
let handlers_for_key = handlers.entry(key).or_default();
handlers_for_key.push(Arc::new(handler));
}
pub fn process_global_event(event: &KeyEvent) -> bool {
if event.kind != KeyEventKind::Press {
return false;
}
let handlers = GLOBAL_EVENT_HANDLERS.lock();
if let Some(handlers_for_key) = handlers.get(&event.code) {
for handler in handlers_for_key {
if handler() {
return true; }
}
}
false
}
pub fn clear_global_handlers() {
GLOBAL_EVENT_HANDLERS.lock().clear();
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyEventState, KeyModifiers};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
pub(super) static TEST_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn key_press(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
fn key_release(code: KeyCode) -> KeyEvent {
KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Release,
state: KeyEventState::NONE,
}
}
#[test]
fn test_handler_registration_and_invocation() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
on_global_event(KeyCode::Char('a'), move || {
called_clone.store(true, Ordering::SeqCst);
true
});
let event = key_press(KeyCode::Char('a'));
let result = process_global_event(&event);
assert!(called.load(Ordering::SeqCst), "Handler should be called");
assert!(result, "Event should be marked as handled");
}
#[test]
fn test_propagation_stops_on_true() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let first_called = Arc::new(AtomicBool::new(false));
let second_called = Arc::new(AtomicBool::new(false));
let first = first_called.clone();
on_global_event(KeyCode::Char('b'), move || {
first.store(true, Ordering::SeqCst);
true });
let second = second_called.clone();
on_global_event(KeyCode::Char('b'), move || {
second.store(true, Ordering::SeqCst);
true
});
let event = key_press(KeyCode::Char('b'));
let result = process_global_event(&event);
assert!(
first_called.load(Ordering::SeqCst),
"First handler should be called"
);
assert!(
!second_called.load(Ordering::SeqCst),
"Second handler should NOT be called"
);
assert!(result, "Event should be marked as handled");
}
#[test]
fn test_propagation_continues_on_false() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let call_count = Arc::new(AtomicUsize::new(0));
let count1 = call_count.clone();
on_global_event(KeyCode::Char('c'), move || {
count1.fetch_add(1, Ordering::SeqCst);
false });
let count2 = call_count.clone();
on_global_event(KeyCode::Char('c'), move || {
count2.fetch_add(1, Ordering::SeqCst);
false });
let event = key_press(KeyCode::Char('c'));
let result = process_global_event(&event);
assert_eq!(
call_count.load(Ordering::SeqCst),
2,
"Both handlers should be called"
);
assert!(
!result,
"Event should NOT be marked as handled when all return false"
);
}
#[test]
fn test_no_handlers_for_key() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let event = key_press(KeyCode::Char('x'));
let result = process_global_event(&event);
assert!(!result, "Should return false when no handlers registered");
}
#[test]
fn test_only_press_events_processed() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
on_global_event(KeyCode::Char('d'), move || {
called_clone.store(true, Ordering::SeqCst);
true
});
let release_event = key_release(KeyCode::Char('d'));
let result = process_global_event(&release_event);
assert!(
!called.load(Ordering::SeqCst),
"Handler should NOT be called for release"
);
assert!(!result, "Release event should not be handled");
let press_event = key_press(KeyCode::Char('d'));
let result = process_global_event(&press_event);
assert!(
called.load(Ordering::SeqCst),
"Handler should be called for press"
);
assert!(result, "Press event should be handled");
}
#[test]
fn test_clear_handlers() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
on_global_event(KeyCode::Char('e'), move || {
called_clone.store(true, Ordering::SeqCst);
true
});
clear_global_handlers();
let event = key_press(KeyCode::Char('e'));
let result = process_global_event(&event);
assert!(
!called.load(Ordering::SeqCst),
"Handler should NOT be called after clear"
);
assert!(!result, "Should return false after handlers cleared");
}
#[test]
fn test_handlers_called_in_registration_order() {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let order = Arc::new(Mutex::new(Vec::new()));
let order1 = order.clone();
on_global_event(KeyCode::Char('f'), move || {
order1.lock().push(1);
false
});
let order2 = order.clone();
on_global_event(KeyCode::Char('f'), move || {
order2.lock().push(2);
false
});
let order3 = order.clone();
on_global_event(KeyCode::Char('f'), move || {
order3.lock().push(3);
true });
let event = key_press(KeyCode::Char('f'));
process_global_event(&event);
let recorded_order = order.lock().clone();
assert_eq!(
recorded_order,
vec![1, 2, 3],
"Handlers should be called in registration order"
);
}
}
#[cfg(test)]
mod property_tests {
use super::tests::TEST_MUTEX;
use super::*;
use crossterm::event::{KeyEventKind, KeyEventState, KeyModifiers};
use proptest::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn key_code_strategy() -> impl Strategy<Value = KeyCode> {
prop_oneof![
any::<char>().prop_filter_map("printable char", |c| {
if c.is_ascii_alphanumeric() || c.is_ascii_punctuation() {
Some(KeyCode::Char(c))
} else {
None
}
}),
(1u8..=12).prop_map(KeyCode::F),
Just(KeyCode::Enter),
Just(KeyCode::Tab),
Just(KeyCode::Backspace),
Just(KeyCode::Esc),
Just(KeyCode::Left),
Just(KeyCode::Right),
Just(KeyCode::Up),
Just(KeyCode::Down),
Just(KeyCode::Home),
Just(KeyCode::End),
Just(KeyCode::PageUp),
Just(KeyCode::PageDown),
Just(KeyCode::Delete),
Just(KeyCode::Insert),
]
}
fn key_press_event_strategy() -> impl Strategy<Value = KeyEvent> {
key_code_strategy().prop_map(|code| KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_handler_propagation(
key_event in key_press_event_strategy(),
handler_count in 1usize..=10,
) {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let call_order = Arc::new(Mutex::new(Vec::new()));
for i in 0..handler_count {
let order = call_order.clone();
on_global_event(key_event.code, move || {
order.lock().push(i);
false });
}
process_global_event(&key_event);
let recorded = call_order.lock().clone();
let expected: Vec<usize> = (0..handler_count).collect();
prop_assert_eq!(recorded, expected, "Handlers should be called in registration order");
}
#[test]
fn prop_propagation_stops_on_true(
key_event in key_press_event_strategy(),
stop_at in 0usize..10,
total_handlers in 1usize..=10,
) {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let stop_at = stop_at % total_handlers;
let called = Arc::new(Mutex::new(Vec::new()));
for i in 0..total_handlers {
let called_clone = called.clone();
let should_stop = i == stop_at;
on_global_event(key_event.code, move || {
called_clone.lock().push(i);
should_stop });
}
let result = process_global_event(&key_event);
let called_handlers = called.lock().clone();
let expected: Vec<usize> = (0..=stop_at).collect();
prop_assert_eq!(called_handlers, expected, "Only handlers up to stop_at should be called");
prop_assert!(result, "Event should be marked as handled");
}
#[test]
fn prop_all_handlers_called_when_none_stop(
key_event in key_press_event_strategy(),
handler_count in 1usize..=10,
) {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let call_count = Arc::new(AtomicUsize::new(0));
for _ in 0..handler_count {
let count = call_count.clone();
on_global_event(key_event.code, move || {
count.fetch_add(1, Ordering::SeqCst);
false });
}
let result = process_global_event(&key_event);
prop_assert_eq!(
call_count.load(Ordering::SeqCst),
handler_count,
"All handlers should be called when none return true"
);
prop_assert!(!result, "Event should not be marked as handled when all return false");
}
#[test]
fn prop_only_press_events_processed(
code in key_code_strategy(),
) {
let _lock = TEST_MUTEX.lock();
clear_global_handlers();
let called = Arc::new(AtomicUsize::new(0));
let called_clone = called.clone();
on_global_event(code, move || {
called_clone.fetch_add(1, Ordering::SeqCst);
true
});
let release_event = KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Release,
state: KeyEventState::NONE,
};
let release_result = process_global_event(&release_event);
prop_assert!(!release_result, "Release event should not be handled");
prop_assert_eq!(called.load(Ordering::SeqCst), 0, "Handler should not be called for release");
let repeat_event = KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Repeat,
state: KeyEventState::NONE,
};
let repeat_result = process_global_event(&repeat_event);
prop_assert!(!repeat_result, "Repeat event should not be handled");
prop_assert_eq!(called.load(Ordering::SeqCst), 0, "Handler should not be called for repeat");
let press_event = KeyEvent {
code,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
};
let press_result = process_global_event(&press_event);
prop_assert!(press_result, "Press event should be handled");
prop_assert_eq!(called.load(Ordering::SeqCst), 1, "Handler should be called for press");
}
}
}