use super::{ActivateKeyTableMode, KeyAction, KeyCombo, KeyTable};
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct KeyTableStack {
stack: Vec<KeyTableActivation>,
default_table: KeyTable,
}
impl KeyTableStack {
pub fn new(default_table: KeyTable) -> Self {
Self {
stack: Vec::new(),
default_table,
}
}
pub fn push(&mut self, activation: KeyTableActivation) {
self.stack.push(activation);
}
pub fn pop(&mut self) -> Option<KeyTableActivation> {
self.stack.pop()
}
pub fn current(&self) -> Option<&KeyTableActivation> {
self.stack.last()
}
pub fn current_name(&self) -> &str {
self.current().map(|a| a.name.as_str()).unwrap_or("default")
}
pub fn clear(&mut self) {
self.stack.clear();
}
pub fn is_empty(&self) -> bool {
self.stack.is_empty()
}
pub fn len(&self) -> usize {
self.stack.len()
}
pub fn resolve(&self, combo: &KeyCombo) -> Option<&KeyAction> {
for activation in self.stack.iter().rev() {
if let Some(action) = activation.table.get(combo) {
return Some(action);
}
}
self.default_table.get(combo)
}
pub fn handle_key(&mut self, combo: KeyCombo, now: Instant) -> Option<KeyAction> {
self.expire_timeouts(now);
let action = self.resolve(&combo).cloned();
if let Some(top) = self.stack.last() {
if matches!(top.mode, ActivateKeyTableMode::OneShot) {
self.stack.pop();
}
}
action
}
fn expire_timeouts(&mut self, now: Instant) {
self.stack.retain(|activation| {
activation
.timeout
.map(|timeout| now < timeout)
.unwrap_or(true)
});
}
pub fn next_timeout(&self) -> Option<Instant> {
self.stack.iter().filter_map(|a| a.timeout).min()
}
pub fn default_table(&self) -> &KeyTable {
&self.default_table
}
pub fn default_table_mut(&mut self) -> &mut KeyTable {
&mut self.default_table
}
}
impl Default for KeyTableStack {
fn default() -> Self {
Self::new(KeyTable::new("default"))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyTableActivation {
pub name: String,
pub table: KeyTable,
pub mode: ActivateKeyTableMode,
#[serde(skip)]
pub timeout: Option<Instant>,
pub replace_current: bool,
}
impl KeyTableActivation {
pub fn persistent(name: String, table: KeyTable) -> Self {
Self {
name,
table,
mode: ActivateKeyTableMode::Persistent,
timeout: None,
replace_current: false,
}
}
pub fn one_shot(name: String, table: KeyTable) -> Self {
Self {
name,
table,
mode: ActivateKeyTableMode::OneShot,
timeout: None,
replace_current: false,
}
}
pub fn timed(name: String, table: KeyTable, duration: Duration, now: Instant) -> Self {
Self {
name,
table,
mode: ActivateKeyTableMode::Timeout(duration),
timeout: Some(now + duration),
replace_current: false,
}
}
pub fn with_replace(mut self, replace: bool) -> Self {
self.replace_current = replace;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key_tables::{KeyCode, KeyModifiers};
fn create_test_table(name: &str) -> KeyTable {
let mut table = KeyTable::new(name);
table.bind(
KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
KeyAction::Noop,
);
table
}
#[test]
fn test_stack_push_pop() {
let mut stack = KeyTableStack::default();
assert_eq!(stack.len(), 0);
assert!(stack.is_empty());
let activation = KeyTableActivation::persistent("test".into(), create_test_table("test"));
stack.push(activation);
assert_eq!(stack.len(), 1);
assert!(!stack.is_empty());
assert_eq!(stack.current_name(), "test");
stack.pop();
assert_eq!(stack.len(), 0);
assert!(stack.is_empty());
}
#[test]
fn test_key_resolution_stack() {
let mut stack = KeyTableStack::default();
let table1 = create_test_table("test1");
stack.push(KeyTableActivation::persistent("test1".into(), table1));
let combo = KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE);
assert!(stack.resolve(&combo).is_some());
let combo = KeyCombo::new(KeyCode::KeyJ, KeyModifiers::NONE);
assert!(stack.resolve(&combo).is_none());
}
#[test]
fn test_one_shot_pop() {
let mut stack = KeyTableStack::default();
let now = Instant::now();
let table = create_test_table("oneshot");
stack.push(KeyTableActivation::one_shot("oneshot".into(), table));
assert_eq!(stack.len(), 1);
let combo = KeyCombo::new(KeyCode::KeyX, KeyModifiers::NONE);
stack.handle_key(combo, now);
assert_eq!(stack.len(), 0);
}
#[test]
fn test_timeout_expiration() {
let mut stack = KeyTableStack::default();
let now = Instant::now();
let table = create_test_table("timed");
stack.push(KeyTableActivation::timed(
"timed".into(),
table,
Duration::from_millis(100),
now,
));
assert_eq!(stack.len(), 1);
stack.expire_timeouts(now);
assert_eq!(stack.len(), 1);
let future = now + Duration::from_millis(101);
stack.expire_timeouts(future);
assert_eq!(stack.len(), 0);
}
#[test]
fn test_multiple_tables_resolution() {
let mut stack = KeyTableStack::default();
let mut table1 = KeyTable::new("bottom");
table1.bind(
KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
KeyAction::Noop,
);
stack.push(KeyTableActivation::persistent("bottom".into(), table1));
let mut table2 = KeyTable::new("top");
table2.bind(
KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
KeyAction::PopKeyTable,
);
stack.push(KeyTableActivation::persistent("top".into(), table2));
let combo = KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE);
let action = stack.resolve(&combo);
assert_eq!(action, Some(&KeyAction::PopKeyTable));
}
#[test]
fn test_clear_stack() {
let mut stack = KeyTableStack::default();
stack.push(KeyTableActivation::persistent(
"test1".into(),
create_test_table("test1"),
));
stack.push(KeyTableActivation::persistent(
"test2".into(),
create_test_table("test2"),
));
assert_eq!(stack.len(), 2);
stack.clear();
assert_eq!(stack.len(), 0);
assert!(stack.is_empty());
}
#[test]
fn test_next_timeout() {
let mut stack = KeyTableStack::default();
let now = Instant::now();
assert!(stack.next_timeout().is_none());
let table = create_test_table("timed");
stack.push(KeyTableActivation::timed(
"timed".into(),
table,
Duration::from_millis(100),
now,
));
let next = stack.next_timeout();
assert!(next.is_some());
assert!(next.unwrap() > now);
}
#[test]
fn test_default_table_access() {
let mut stack = KeyTableStack::default();
stack.default_table_mut().bind(
KeyCombo::new(KeyCode::KeyA, KeyModifiers::CTRL),
KeyAction::Noop,
);
let combo = KeyCombo::new(KeyCode::KeyA, KeyModifiers::CTRL);
assert!(stack.resolve(&combo).is_some());
}
}