#![forbid(unsafe_code)]
use web_time::{Duration, Instant};
use crate::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
#[derive(Debug, Clone)]
pub struct KeySequenceConfig {
pub sequence_timeout: Duration,
pub detect_double_escape: bool,
}
impl Default for KeySequenceConfig {
fn default() -> Self {
Self {
sequence_timeout: Duration::from_millis(250),
detect_double_escape: true,
}
}
}
impl KeySequenceConfig {
#[must_use]
pub fn with_timeout(timeout: Duration) -> Self {
Self {
sequence_timeout: timeout,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeySequenceKind {
DoubleEscape,
}
impl KeySequenceKind {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::DoubleEscape => "Esc Esc",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KeySequenceAction {
Emit(KeyEvent),
EmitSequence {
kind: KeySequenceKind,
keys: Vec<KeyEvent>,
},
Pending,
}
impl KeySequenceAction {
#[must_use]
pub const fn is_pending(&self) -> bool {
matches!(self, Self::Pending)
}
#[must_use]
pub const fn is_sequence(&self) -> bool {
matches!(self, Self::EmitSequence { .. })
}
}
pub struct KeySequenceInterpreter {
config: KeySequenceConfig,
buffer: Vec<KeyEvent>,
buffer_start: Option<Instant>,
}
impl std::fmt::Debug for KeySequenceInterpreter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeySequenceInterpreter")
.field("buffer_len", &self.buffer.len())
.field("has_pending", &self.buffer_start.is_some())
.finish()
}
}
impl KeySequenceInterpreter {
#[must_use]
pub fn new(config: KeySequenceConfig) -> Self {
Self {
config,
buffer: Vec::with_capacity(4),
buffer_start: None,
}
}
#[must_use]
pub fn with_defaults() -> Self {
Self::new(KeySequenceConfig::default())
}
pub fn feed(&mut self, event: &KeyEvent, now: Instant) -> KeySequenceAction {
if event.kind != KeyEventKind::Press {
return KeySequenceAction::Emit(*event);
}
match self.try_sequence(event, now) {
SequenceResult::Complete(kind) => {
self.buffer.push(*event);
let keys = std::mem::take(&mut self.buffer);
self.buffer_start = None;
KeySequenceAction::EmitSequence { kind, keys }
}
SequenceResult::Continue => {
if self.buffer.is_empty() {
self.buffer_start = Some(now);
}
self.buffer.push(*event);
KeySequenceAction::Pending
}
SequenceResult::NoMatch => {
if !self.buffer.is_empty() {
self.buffer.clear();
self.buffer_start = None;
}
KeySequenceAction::Emit(*event)
}
}
}
pub fn check_timeout(&mut self, now: Instant) -> Option<Vec<KeySequenceAction>> {
if let Some(start) = self.buffer_start {
if now.saturating_duration_since(start) >= self.config.sequence_timeout {
let actions: Vec<_> = self.buffer.drain(..).map(KeySequenceAction::Emit).collect();
self.buffer_start = None;
if actions.is_empty() {
None
} else {
Some(actions)
}
} else {
None
}
} else {
None
}
}
#[must_use]
pub fn has_pending(&self) -> bool {
self.buffer_start.is_some()
}
#[must_use]
pub fn time_until_timeout(&self, now: Instant) -> Option<Duration> {
self.buffer_start.map(|start| {
let elapsed = now.saturating_duration_since(start);
self.config.sequence_timeout.saturating_sub(elapsed)
})
}
pub fn reset(&mut self) {
self.buffer.clear();
self.buffer_start = None;
}
pub fn flush(&mut self) -> Vec<KeySequenceAction> {
let actions: Vec<_> = self.buffer.drain(..).map(KeySequenceAction::Emit).collect();
self.buffer_start = None;
actions
}
#[must_use]
pub fn config(&self) -> &KeySequenceConfig {
&self.config
}
pub fn set_config(&mut self, config: KeySequenceConfig) {
self.config = config;
}
}
#[derive(Debug, Clone, Copy)]
enum SequenceResult {
Complete(KeySequenceKind),
Continue,
NoMatch,
}
impl KeySequenceInterpreter {
fn try_sequence(&self, event: &KeyEvent, _now: Instant) -> SequenceResult {
if self.config.detect_double_escape
&& event.code == KeyCode::Escape
&& event.modifiers == Modifiers::NONE
{
if self.buffer.len() == 1
&& self.buffer[0].code == KeyCode::Escape
&& self.buffer[0].modifiers == Modifiers::NONE
{
return SequenceResult::Complete(KeySequenceKind::DoubleEscape);
}
if self.buffer.is_empty() {
return SequenceResult::Continue;
}
}
SequenceResult::NoMatch
}
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> Instant {
Instant::now()
}
fn esc() -> KeyEvent {
KeyEvent {
code: KeyCode::Escape,
modifiers: Modifiers::NONE,
kind: KeyEventKind::Press,
}
}
fn key(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: Modifiers::NONE,
kind: KeyEventKind::Press,
}
}
fn key_release(c: char) -> KeyEvent {
KeyEvent {
code: KeyCode::Char(c),
modifiers: Modifiers::NONE,
kind: KeyEventKind::Release,
}
}
const MS_50: Duration = Duration::from_millis(50);
const MS_100: Duration = Duration::from_millis(100);
const MS_300: Duration = Duration::from_millis(300);
#[test]
fn double_escape_within_timeout() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&esc(), t);
assert!(matches!(action, KeySequenceAction::Pending));
assert!(interp.has_pending());
let action = interp.feed(&esc(), t + MS_100);
assert!(matches!(
action,
KeySequenceAction::EmitSequence {
kind: KeySequenceKind::DoubleEscape,
..
}
));
assert!(!interp.has_pending());
}
#[test]
fn single_escape_timeout() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&esc(), t);
assert!(matches!(action, KeySequenceAction::Pending));
let actions = interp.check_timeout(t + MS_300);
assert!(actions.is_some());
let actions = actions.unwrap();
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], KeySequenceAction::Emit(_)));
}
#[test]
fn escape_then_different_key() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&esc(), t);
assert!(matches!(action, KeySequenceAction::Pending));
let action = interp.feed(&key('a'), t + MS_50);
assert!(matches!(action, KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn non_escape_key_passes_through() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&key('x'), t);
assert!(matches!(action, KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn key_release_passes_through() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&key_release('x'), t);
assert!(matches!(action, KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn modified_escape_passes_through() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let ctrl_esc = KeyEvent {
code: KeyCode::Escape,
modifiers: Modifiers::CTRL,
kind: KeyEventKind::Press,
};
let action = interp.feed(&ctrl_esc, t);
assert!(matches!(action, KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn custom_timeout() {
let config = KeySequenceConfig::with_timeout(Duration::from_millis(100));
let mut interp = KeySequenceInterpreter::new(config);
let t = now();
interp.feed(&esc(), t);
assert!(interp.has_pending());
assert!(interp.check_timeout(t + MS_50).is_none());
let actions = interp.check_timeout(t + Duration::from_millis(150));
assert!(actions.is_some());
}
#[test]
fn disabled_double_escape() {
let config = KeySequenceConfig {
detect_double_escape: false,
..Default::default()
};
let mut interp = KeySequenceInterpreter::new(config);
let t = now();
let action = interp.feed(&esc(), t);
assert!(matches!(action, KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn time_until_timeout() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
assert!(interp.time_until_timeout(t).is_none());
interp.feed(&esc(), t);
let remaining = interp.time_until_timeout(t + MS_100);
assert!(remaining.is_some());
let remaining = remaining.unwrap();
assert!(remaining >= Duration::from_millis(140));
assert!(remaining <= Duration::from_millis(160));
}
#[test]
fn reset_clears_state() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
interp.feed(&esc(), t);
assert!(interp.has_pending());
interp.reset();
assert!(!interp.has_pending());
}
#[test]
fn flush_returns_pending_keys() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
interp.feed(&esc(), t);
assert!(interp.has_pending());
let actions = interp.flush();
assert_eq!(actions.len(), 1);
assert!(matches!(actions[0], KeySequenceAction::Emit(_)));
assert!(!interp.has_pending());
}
#[test]
fn flush_on_empty_returns_empty() {
let mut interp = KeySequenceInterpreter::with_defaults();
let actions = interp.flush();
assert!(actions.is_empty());
}
#[test]
fn config_getter_and_setter() {
let mut interp = KeySequenceInterpreter::with_defaults();
assert_eq!(interp.config().sequence_timeout, Duration::from_millis(250));
let new_config = KeySequenceConfig::with_timeout(Duration::from_millis(500));
interp.set_config(new_config);
assert_eq!(interp.config().sequence_timeout, Duration::from_millis(500));
}
#[test]
fn debug_format() {
let interp = KeySequenceInterpreter::with_defaults();
let dbg = format!("{:?}", interp);
assert!(dbg.contains("KeySequenceInterpreter"));
}
#[test]
fn action_is_pending() {
assert!(KeySequenceAction::Pending.is_pending());
assert!(!KeySequenceAction::Emit(esc()).is_pending());
assert!(
!KeySequenceAction::EmitSequence {
kind: KeySequenceKind::DoubleEscape,
keys: vec![],
}
.is_pending()
);
}
#[test]
fn action_is_sequence() {
assert!(!KeySequenceAction::Pending.is_sequence());
assert!(!KeySequenceAction::Emit(esc()).is_sequence());
assert!(
KeySequenceAction::EmitSequence {
kind: KeySequenceKind::DoubleEscape,
keys: vec![],
}
.is_sequence()
);
}
#[test]
fn sequence_kind_name() {
assert_eq!(KeySequenceKind::DoubleEscape.name(), "Esc Esc");
}
#[test]
fn default_config_values() {
let config = KeySequenceConfig::default();
assert_eq!(config.sequence_timeout, Duration::from_millis(250));
assert!(config.detect_double_escape);
}
#[test]
fn triple_escape_produces_sequence_then_pending() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
let action = interp.feed(&esc(), t);
assert!(matches!(action, KeySequenceAction::Pending));
let action = interp.feed(&esc(), t + MS_50);
assert!(matches!(
action,
KeySequenceAction::EmitSequence {
kind: KeySequenceKind::DoubleEscape,
..
}
));
let action = interp.feed(&esc(), t + MS_100);
assert!(matches!(action, KeySequenceAction::Pending));
}
#[test]
fn sequence_keys_are_captured() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
interp.feed(&esc(), t);
let action = interp.feed(&esc(), t + MS_50);
if let KeySequenceAction::EmitSequence { keys, kind } = action {
assert_eq!(keys.len(), 2, "Sequence should capture both keys");
assert_eq!(keys[0].code, KeyCode::Escape);
assert_eq!(keys[1].code, KeyCode::Escape);
assert_eq!(kind, KeySequenceKind::DoubleEscape);
} else {
panic!("Expected EmitSequence");
}
}
#[test]
fn rapid_non_escape_keys() {
let mut interp = KeySequenceInterpreter::with_defaults();
let t = now();
for (i, c) in "hello".chars().enumerate() {
let action = interp.feed(&key(c), t + Duration::from_millis(i as u64 * 10));
assert!(
matches!(action, KeySequenceAction::Emit(_)),
"Key '{}' should pass through",
c
);
}
assert!(!interp.has_pending());
}
}