use std::collections::HashSet;
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crossbeam_channel::{Receiver, RecvError, RecvTimeoutError, TryRecvError};
use crate::{Error, Event, EventKind, Key, Tap};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Chord {
keys: HashSet<Key>,
}
impl Chord {
pub fn of<I: IntoIterator<Item = Key>>(keys: I) -> Self {
Self {
keys: keys.into_iter().collect(),
}
}
pub fn keys(&self) -> impl Iterator<Item = &Key> {
self.keys.iter()
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
fn matches(&self, held: &HashSet<Key>) -> bool {
!self.keys.is_empty() && self.keys.is_subset(held)
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum ChordMode {
#[default]
Momentary,
Toggle,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ChordEvent<Id: Clone + Debug> {
Start { id: Id, time: Instant },
End { id: Id, time: Instant },
}
#[derive(Debug)]
pub struct ChordMatcher<Id: Clone + Debug + Eq + Hash + Send + 'static> {
rx: Receiver<ChordEvent<Id>>,
worker: Option<WorkerHandle>,
}
#[derive(Debug)]
struct WorkerHandle {
thread: Option<JoinHandle<()>>,
running: Arc<AtomicBool>,
}
impl<Id: Clone + Debug + Eq + Hash + Send + 'static> ChordMatcher<Id> {
pub fn builder() -> ChordMatcherBuilder<Id> {
ChordMatcherBuilder {
registered: Vec::new(),
tap: None,
}
}
pub fn recv(&self) -> Result<ChordEvent<Id>, RecvError> {
self.rx.recv()
}
pub fn try_recv(&self) -> Result<ChordEvent<Id>, TryRecvError> {
self.rx.try_recv()
}
pub fn recv_timeout(&self, d: Duration) -> Result<ChordEvent<Id>, RecvTimeoutError> {
self.rx.recv_timeout(d)
}
}
impl<Id: Clone + Debug + Eq + Hash + Send + 'static> Drop for ChordMatcher<Id> {
fn drop(&mut self) {
if let Some(mut handle) = self.worker.take() {
handle.running.store(false, Ordering::Relaxed);
if let Some(thread) = handle.thread.take() {
let _ = thread.join();
}
}
}
}
#[derive(Debug)]
pub struct ChordMatcherBuilder<Id: Clone + Debug + Eq + Hash + Send + 'static> {
registered: Vec<(Id, Chord, ChordMode)>,
tap: Option<Tap>,
}
impl<Id: Clone + Debug + Eq + Hash + Send + 'static> ChordMatcherBuilder<Id> {
pub fn add(self, id: Id, chord: Chord) -> Self {
self.add_with_mode(id, chord, ChordMode::Momentary)
}
pub fn add_toggle(self, id: Id, chord: Chord) -> Self {
self.add_with_mode(id, chord, ChordMode::Toggle)
}
pub fn add_with_mode(mut self, id: Id, chord: Chord, mode: ChordMode) -> Self {
self.registered.push((id, chord, mode));
self
}
pub fn with_tap(mut self, tap: Tap) -> Self {
self.tap = Some(tap);
self
}
pub fn build(mut self) -> Result<ChordMatcher<Id>, Error> {
let tap = match self.tap.take() {
Some(t) => t,
None => Tap::new()?,
};
let (tx, rx) = crossbeam_channel::unbounded();
let registered = std::mem::take(&mut self.registered);
let running = Arc::new(AtomicBool::new(true));
let running_worker = running.clone();
let thread = thread::Builder::new()
.name("keytap-chord".into())
.spawn(move || {
let mut state = MatcherState::new(registered);
while running_worker.load(Ordering::Relaxed) {
match tap.recv_timeout(Duration::from_millis(50)) {
Ok(event) => {
state.process(event, |ev| {
let _ = tx.send(ev);
});
}
Err(RecvTimeoutError::Timeout) => continue,
Err(RecvTimeoutError::Disconnected) => break,
}
}
})
.expect("failed to spawn chord worker thread");
Ok(ChordMatcher {
rx,
worker: Some(WorkerHandle {
thread: Some(thread),
running,
}),
})
}
}
#[derive(Debug)]
pub(crate) struct MatcherState<Id: Clone + Debug + Eq + Hash> {
registered: Vec<(Id, Chord, ChordMode)>,
held: HashSet<Key>,
active: Option<(Id, ChordMode)>,
satisfied_prev: HashSet<usize>,
}
impl<Id: Clone + Debug + Eq + Hash> MatcherState<Id> {
pub(crate) fn new(registered: Vec<(Id, Chord, ChordMode)>) -> Self {
Self {
registered,
held: HashSet::new(),
active: None,
satisfied_prev: HashSet::new(),
}
}
pub(crate) fn process<F: FnMut(ChordEvent<Id>)>(&mut self, event: Event, mut emit: F) {
match event.kind {
EventKind::KeyDown(k) => {
self.held.insert(k);
}
EventKind::KeyUp(k) => {
self.held.remove(&k);
}
EventKind::KeyRepeat(_) => return, }
let satisfied_now = self.satisfied_indices();
let rising: Vec<usize> = satisfied_now
.difference(&self.satisfied_prev)
.copied()
.collect();
self.satisfied_prev = satisfied_now;
if let Some((active_id, ChordMode::Toggle)) = self.active.clone() {
for &idx in &rising {
if self.registered[idx].0 == active_id {
emit(ChordEvent::End {
id: active_id,
time: event.time,
});
self.active = None;
return;
}
}
return;
}
let new_match = self.longest_match();
let same_id = match (&new_match, &self.active) {
(Some((a, _)), Some((b, _))) => a == b,
(None, None) => true,
_ => false,
};
if !same_id {
if let Some((prev_id, _)) = self.active.take() {
emit(ChordEvent::End {
id: prev_id,
time: event.time,
});
}
if let Some((next_id, _)) = &new_match {
emit(ChordEvent::Start {
id: next_id.clone(),
time: event.time,
});
}
self.active = new_match;
}
}
fn satisfied_indices(&self) -> HashSet<usize> {
self.registered
.iter()
.enumerate()
.filter(|(_, (_, c, _))| c.matches(&self.held))
.map(|(i, _)| i)
.collect()
}
fn longest_match(&self) -> Option<(Id, ChordMode)> {
let max_len = self
.registered
.iter()
.filter(|(_, c, _)| c.matches(&self.held))
.map(|(_, c, _)| c.len())
.max()?;
self.registered
.iter()
.find(|(_, c, _)| c.len() == max_len && c.matches(&self.held))
.map(|(id, _, mode)| (id.clone(), *mode))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn down(k: Key) -> Event {
Event {
time: Instant::now(),
kind: EventKind::KeyDown(k),
}
}
fn up(k: Key) -> Event {
Event {
time: Instant::now(),
kind: EventKind::KeyUp(k),
}
}
fn repeat(k: Key) -> Event {
Event {
time: Instant::now(),
kind: EventKind::KeyRepeat(k),
}
}
fn run<Id: Clone + Debug + Eq + Hash>(
registered: Vec<(Id, Chord)>,
events: Vec<Event>,
) -> Vec<ChordEvent<Id>> {
let tagged = registered
.into_iter()
.map(|(id, c)| (id, c, ChordMode::Momentary))
.collect();
run_with_modes(tagged, events)
}
fn run_with_modes<Id: Clone + Debug + Eq + Hash>(
registered: Vec<(Id, Chord, ChordMode)>,
events: Vec<Event>,
) -> Vec<ChordEvent<Id>> {
let mut state = MatcherState::new(registered);
let mut out = Vec::new();
for e in events {
state.process(e, |ce| out.push(ce));
}
out
}
fn ids<Id: Clone + Debug>(evs: &[ChordEvent<Id>]) -> Vec<String> {
evs.iter()
.map(|e| match e {
ChordEvent::Start { id, .. } => format!("start({id:?})"),
ChordEvent::End { id, .. } => format!("end({id:?})"),
})
.collect()
}
#[test]
fn single_key_chord_emits_start_and_end() {
let out = run(
vec![("esc", Chord::of([Key::Escape]))],
vec![down(Key::Escape), up(Key::Escape)],
);
assert_eq!(ids(&out), vec!["start(\"esc\")", "end(\"esc\")"]);
}
#[test]
fn two_key_chord_activates_on_second_key() {
let out = run(
vec![("ptt", Chord::of([Key::MetaRight, Key::AltRight]))],
vec![
down(Key::MetaRight), down(Key::AltRight), up(Key::AltRight), up(Key::MetaRight),
],
);
assert_eq!(ids(&out), vec!["start(\"ptt\")", "end(\"ptt\")"]);
}
#[test]
fn end_fires_when_any_chord_key_released() {
let out = run(
vec![("ptt", Chord::of([Key::MetaRight, Key::AltRight]))],
vec![
down(Key::MetaRight),
down(Key::AltRight),
up(Key::MetaRight), up(Key::AltRight),
],
);
assert_eq!(ids(&out), vec!["start(\"ptt\")", "end(\"ptt\")"]);
}
#[test]
fn left_right_modifiers_are_distinct() {
let out = run::<&str>(
vec![("ptt", Chord::of([Key::MetaRight, Key::AltRight]))],
vec![
down(Key::MetaLeft),
down(Key::AltLeft),
up(Key::AltLeft),
up(Key::MetaLeft),
],
);
assert!(out.is_empty(), "unexpected: {out:?}");
}
#[test]
fn longest_match_wins() {
let out = run(
vec![
("short", Chord::of([Key::A])),
("long", Chord::of([Key::A, Key::B])),
],
vec![
down(Key::A), down(Key::B), up(Key::B), up(Key::A),
],
);
assert_eq!(
ids(&out),
vec![
"start(\"short\")",
"end(\"short\")",
"start(\"long\")",
"end(\"long\")",
"start(\"short\")",
"end(\"short\")",
]
);
}
#[test]
fn transitioning_between_chords_emits_end_then_start() {
let out = run(
vec![("a", Chord::of([Key::A])), ("b", Chord::of([Key::B]))],
vec![
down(Key::A),
down(Key::B), ],
);
assert_eq!(ids(&out), vec!["start(\"a\")"]);
}
#[test]
fn key_repeat_does_not_affect_state() {
let out = run(
vec![("esc", Chord::of([Key::Escape]))],
vec![
down(Key::Escape),
repeat(Key::Escape),
repeat(Key::Escape),
repeat(Key::Escape),
up(Key::Escape),
],
);
assert_eq!(ids(&out), vec!["start(\"esc\")", "end(\"esc\")"]);
}
#[test]
fn non_chord_keys_do_not_trigger() {
let out = run::<&str>(
vec![("ptt", Chord::of([Key::MetaRight]))],
vec![down(Key::A), up(Key::A), down(Key::B), up(Key::B)],
);
assert!(out.is_empty());
}
#[test]
fn every_start_has_matching_end() {
let events = vec![
down(Key::A),
down(Key::B),
up(Key::A),
down(Key::A),
up(Key::B),
up(Key::A),
];
let out = run(
vec![
("a", Chord::of([Key::A])),
("b", Chord::of([Key::B])),
("ab", Chord::of([Key::A, Key::B])),
],
events,
);
let starts = out
.iter()
.filter(|e| matches!(e, ChordEvent::Start { .. }))
.count();
let ends = out
.iter()
.filter(|e| matches!(e, ChordEvent::End { .. }))
.count();
assert_eq!(starts, ends);
}
#[test]
fn empty_chord_never_matches() {
let out = run::<&str>(
vec![("empty", Chord::of(std::iter::empty()))],
vec![down(Key::A), up(Key::A)],
);
assert!(out.is_empty());
}
#[test]
fn toggle_stays_active_through_key_release() {
let out = run_with_modes(
vec![("t", Chord::of([Key::A]), ChordMode::Toggle)],
vec![down(Key::A), up(Key::A)],
);
assert_eq!(ids(&out), vec!["start(\"t\")"]);
}
#[test]
fn toggle_ends_on_second_complete_press() {
let out = run_with_modes(
vec![("t", Chord::of([Key::A]), ChordMode::Toggle)],
vec![
down(Key::A),
up(Key::A),
down(Key::A), up(Key::A),
],
);
assert_eq!(ids(&out), vec!["start(\"t\")", "end(\"t\")"]);
}
#[test]
fn toggle_multi_key_requires_all_keys_for_repress() {
let out = run_with_modes(
vec![(
"t",
Chord::of([Key::MetaRight, Key::AltRight]),
ChordMode::Toggle,
)],
vec![
down(Key::MetaRight),
down(Key::AltRight), up(Key::AltRight), down(Key::AltRight), up(Key::AltRight),
up(Key::MetaRight),
],
);
assert_eq!(ids(&out), vec!["start(\"t\")", "end(\"t\")"]);
}
#[test]
fn toggle_suppresses_other_chords_while_active() {
let out = run_with_modes(
vec![
("hands_free", Chord::of([Key::Space]), ChordMode::Toggle),
("cancel", Chord::of([Key::Escape]), ChordMode::Momentary),
],
vec![
down(Key::Space), up(Key::Space),
down(Key::Escape), up(Key::Escape),
down(Key::Space), up(Key::Space),
],
);
assert_eq!(
ids(&out),
vec!["start(\"hands_free\")", "end(\"hands_free\")"]
);
}
#[test]
fn ptt_upgrades_to_toggle_by_longest_match() {
let out = run_with_modes(
vec![
(
"ptt",
Chord::of([Key::MetaRight, Key::AltRight]),
ChordMode::Momentary,
),
(
"hands_free",
Chord::of([Key::MetaRight, Key::AltRight, Key::Space]),
ChordMode::Toggle,
),
],
vec![
down(Key::MetaRight),
down(Key::AltRight), down(Key::Space), up(Key::Space), up(Key::AltRight),
up(Key::MetaRight), ],
);
assert_eq!(
ids(&out),
vec!["start(\"ptt\")", "end(\"ptt\")", "start(\"hands_free\")",]
);
}
#[test]
fn toggle_end_while_ptt_held_reenters_ptt() {
let out = run_with_modes(
vec![
(
"ptt",
Chord::of([Key::MetaRight, Key::AltRight]),
ChordMode::Momentary,
),
(
"hands_free",
Chord::of([Key::MetaRight, Key::AltRight, Key::Space]),
ChordMode::Toggle,
),
],
vec![
down(Key::MetaRight),
down(Key::AltRight), down(Key::Space), up(Key::Space), down(Key::Space), up(Key::Space), up(Key::AltRight), up(Key::MetaRight),
],
);
assert_eq!(
ids(&out),
vec![
"start(\"ptt\")",
"end(\"ptt\")",
"start(\"hands_free\")",
"end(\"hands_free\")",
"start(\"ptt\")",
"end(\"ptt\")",
]
);
}
#[test]
fn toggle_start_requires_rising_edge() {
let out = run_with_modes(
vec![("t", Chord::of([Key::A]), ChordMode::Toggle)],
vec![down(Key::A), up(Key::A), down(Key::A), up(Key::A)],
);
assert_eq!(ids(&out), vec!["start(\"t\")", "end(\"t\")"]);
}
#[test]
fn toggle_ignores_key_repeat_events() {
let out = run_with_modes(
vec![("t", Chord::of([Key::A]), ChordMode::Toggle)],
vec![
down(Key::A),
repeat(Key::A),
repeat(Key::A),
up(Key::A),
],
);
assert_eq!(ids(&out), vec!["start(\"t\")"]);
}
#[test]
fn momentary_and_toggle_coexist_independently_when_no_active() {
let out = run_with_modes(
vec![
("m", Chord::of([Key::A]), ChordMode::Momentary),
("t", Chord::of([Key::B]), ChordMode::Toggle),
],
vec![
down(Key::A), up(Key::A), down(Key::B), up(Key::B), down(Key::B), up(Key::B),
],
);
assert_eq!(
ids(&out),
vec!["start(\"m\")", "end(\"m\")", "start(\"t\")", "end(\"t\")"]
);
}
}