use crossbeam::channel::Receiver;
pub use crossterm::event::{Event, KeyCode};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
#[derive(Clone)]
pub struct TerminalFocus(Arc<AtomicBool>);
impl Default for TerminalFocus {
fn default() -> Self {
Self(Arc::new(AtomicBool::new(true)))
}
}
impl TerminalFocus {
pub fn is_focussed(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
pub fn observe(&self, event: &Event) {
let value = match event {
Event::FocusGained => true,
Event::FocusLost => false,
_ => return,
};
self.0.store(value, Ordering::Relaxed);
}
}
fn forward_event(
sender: &crossbeam::channel::Sender<Event>,
focus: &TerminalFocus,
event: Event,
) -> bool {
focus.observe(&event);
if matches!(event, Event::FocusGained | Event::FocusLost) {
return true;
}
match sender.try_send(event) {
Ok(()) | Err(crossbeam::channel::TrySendError::Full(_)) => true,
Err(crossbeam::channel::TrySendError::Disconnected(_)) => false,
}
}
pub fn input_channel(focus: TerminalFocus) -> Receiver<Event> {
let (key_send, key_receive) = crossbeam::channel::bounded(32);
std::thread::spawn(move || -> Result<(), std::io::Error> {
loop {
let event = loop {
match crossterm::event::read() {
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
result => break result?,
}
};
if !forward_event(&key_send, &focus, event) {
break;
}
}
Ok(())
});
key_receive
}
pub fn input_channel_from_keys(input: &str) -> Result<Receiver<Event>, String> {
let (key_send, key_receive) = crossbeam::channel::unbounded();
let events = if input.is_empty() {
Vec::new()
} else {
match input.parse::<dua::KeyBinding>() {
Ok(binding) => vec![Event::Key(binding.to_event())],
Err(err) if input.contains('+') => return Err(err),
Err(_) => input
.chars()
.map(|character| Event::Key(KeyCode::Char(character).into()))
.collect(),
}
};
for event in events {
if key_send.send(event).is_err() {
break;
}
}
Ok(key_receive)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cloned_focus_tracker_observes_changes_immediately() {
let app_focus = TerminalFocus::default();
let input_focus = app_focus.clone();
input_focus.observe(&Event::FocusLost);
assert!(!app_focus.is_focussed());
input_focus.observe(&Event::FocusGained);
assert!(app_focus.is_focussed());
}
#[test]
fn focus_changes_bypass_a_full_bounded_event_queue() {
let focus = TerminalFocus::default();
let (sender, receiver) = crossbeam::channel::bounded(1);
assert!(forward_event(
&sender,
&focus,
Event::Key(KeyCode::Char('a').into()),
));
assert!(forward_event(&sender, &focus, Event::FocusLost));
assert!(forward_event(&sender, &focus, Event::FocusGained));
assert!(focus.is_focussed());
assert!(matches!(receiver.try_recv(), Ok(Event::Key(_))));
assert!(receiver.try_recv().is_err());
}
#[test]
fn once_input_accepts_configured_key_syntax_and_legacy_sequences() {
use crossterm::event::{KeyEvent, KeyModifiers};
let modified = input_channel_from_keys("ctrl+r").unwrap();
assert_eq!(
modified.recv().unwrap(),
Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL))
);
let named = input_channel_from_keys("page-down").unwrap();
assert_eq!(named.recv().unwrap(), Event::Key(KeyCode::PageDown.into()));
let legacy = input_channel_from_keys("jk").unwrap();
assert_eq!(
legacy.recv().unwrap(),
Event::Key(KeyCode::Char('j').into())
);
assert_eq!(
legacy.recv().unwrap(),
Event::Key(KeyCode::Char('k').into())
);
}
}