use std::time::Instant;
use serde::{Deserialize, Serialize};
use crate::action::Action;
use crate::hotkey::Hotkey;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyChord {
pub leader: Hotkey,
pub follower: Hotkey,
pub timeout_ms: u32,
pub action: Action,
}
#[derive(Debug)]
pub enum ChordState {
Idle,
Pending {
leader: Hotkey,
started: Instant,
timeout_ms: u32,
},
}
impl Default for ChordState {
fn default() -> Self {
Self::Idle
}
}
impl ChordState {
#[must_use]
pub fn is_pending(&self) -> bool {
matches!(self, Self::Pending { .. })
}
#[must_use]
pub fn is_timed_out(&self) -> bool {
match self {
Self::Idle => false,
Self::Pending {
started,
timeout_ms,
..
} => started.elapsed().as_millis() >= u128::from(*timeout_ms),
}
}
pub fn begin(&mut self, leader: Hotkey, timeout_ms: u32) {
*self = Self::Pending {
leader,
started: Instant::now(),
timeout_ms,
};
}
pub fn reset(&mut self) {
*self = Self::Idle;
}
#[must_use]
pub fn pending_leader(&self) -> Option<&Hotkey> {
match self {
Self::Idle => None,
Self::Pending { leader, .. } => Some(leader),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hotkey::{Key, Modifiers};
fn leader() -> Hotkey {
Hotkey::new(Modifiers::CTRL, Key::A)
}
fn follower() -> Hotkey {
Hotkey::new(Modifiers::NONE, Key::C)
}
#[test]
fn chord_state_default_idle() {
let state = ChordState::default();
assert!(!state.is_pending());
assert!(state.pending_leader().is_none());
}
#[test]
fn chord_state_begin_pending() {
let mut state = ChordState::default();
state.begin(leader(), 1000);
assert!(state.is_pending());
assert_eq!(state.pending_leader(), Some(&leader()));
}
#[test]
fn chord_state_reset() {
let mut state = ChordState::default();
state.begin(leader(), 1000);
state.reset();
assert!(!state.is_pending());
}
#[test]
fn chord_state_not_timed_out_initially() {
let mut state = ChordState::default();
state.begin(leader(), 5000); assert!(!state.is_timed_out());
}
#[test]
fn chord_state_times_out() {
let mut state = ChordState::default();
state.begin(leader(), 0); std::thread::sleep(std::time::Duration::from_millis(1));
assert!(state.is_timed_out());
}
#[test]
fn idle_never_timed_out() {
let state = ChordState::default();
assert!(!state.is_timed_out());
}
#[test]
fn key_chord_serde() {
let chord = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("new_window"),
};
let json = serde_json::to_string(&chord).unwrap();
let deserialized: KeyChord = serde_json::from_str(&json).unwrap();
assert_eq!(chord, deserialized);
}
#[test]
fn chord_state_pending_leader_returns_correct_hotkey() {
let mut state = ChordState::default();
let ldr = Hotkey::new(Modifiers::CMD, Key::Space);
state.begin(ldr, 500);
assert_eq!(state.pending_leader(), Some(&ldr));
}
#[test]
fn chord_state_idle_pending_leader_is_none() {
let state = ChordState::Idle;
assert!(state.pending_leader().is_none());
}
#[test]
fn chord_state_begin_overwrites_previous() {
let mut state = ChordState::default();
let first = Hotkey::new(Modifiers::CTRL, Key::A);
let second = Hotkey::new(Modifiers::CMD, Key::B);
state.begin(first, 1000);
assert_eq!(state.pending_leader(), Some(&first));
state.begin(second, 500);
assert_eq!(state.pending_leader(), Some(&second));
}
#[test]
fn chord_state_reset_after_timeout_is_idle() {
let mut state = ChordState::default();
state.begin(leader(), 0);
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(state.is_timed_out());
state.reset();
assert!(!state.is_pending());
assert!(!state.is_timed_out());
}
#[test]
fn key_chord_equality() {
let chord1 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("a"),
};
let chord2 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("a"),
};
assert_eq!(chord1, chord2);
}
#[test]
fn key_chord_inequality_different_action() {
let chord1 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("a"),
};
let chord2 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("b"),
};
assert_ne!(chord1, chord2);
}
#[test]
fn key_chord_inequality_different_timeout() {
let chord1 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 1000,
action: Action::command("a"),
};
let chord2 = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 2000,
action: Action::command("a"),
};
assert_ne!(chord1, chord2);
}
#[test]
fn key_chord_with_chain_action_serde() {
let chord = KeyChord {
leader: leader(),
follower: follower(),
timeout_ms: 500,
action: Action::chain(vec![
Action::command("focus_window"),
Action::mode_switch("default"),
]),
};
let json = serde_json::to_string(&chord).unwrap();
let deserialized: KeyChord = serde_json::from_str(&json).unwrap();
assert_eq!(chord, deserialized);
}
#[test]
fn chord_state_large_timeout_not_timed_out() {
let mut state = ChordState::default();
state.begin(leader(), u32::MAX); assert!(!state.is_timed_out());
}
}