Skip to main content

scarab_plugin_api/key_tables/
stack.rs

1//! Key Table Stack
2//!
3//! Manages a stack of active key tables for modal keyboard configurations.
4
5use super::{ActivateKeyTableMode, KeyAction, KeyCombo, KeyTable};
6use serde::{Deserialize, Serialize};
7use std::time::{Duration, Instant};
8
9/// A stack of active key tables
10#[derive(Clone, Debug)]
11pub struct KeyTableStack {
12    /// Stack of active table activations (top = most recent)
13    stack: Vec<KeyTableActivation>,
14    /// Default key table (always at bottom of stack)
15    default_table: KeyTable,
16}
17
18impl KeyTableStack {
19    /// Create a new key table stack with a default table
20    pub fn new(default_table: KeyTable) -> Self {
21        Self {
22            stack: Vec::new(),
23            default_table,
24        }
25    }
26
27    /// Push a new key table activation onto the stack
28    pub fn push(&mut self, activation: KeyTableActivation) {
29        self.stack.push(activation);
30    }
31
32    /// Pop the top key table from the stack
33    pub fn pop(&mut self) -> Option<KeyTableActivation> {
34        self.stack.pop()
35    }
36
37    /// Get the current (top) key table activation
38    pub fn current(&self) -> Option<&KeyTableActivation> {
39        self.stack.last()
40    }
41
42    /// Get the name of the current key table, or "default" if stack is empty
43    pub fn current_name(&self) -> &str {
44        self.current().map(|a| a.name.as_str()).unwrap_or("default")
45    }
46
47    /// Clear the entire stack
48    pub fn clear(&mut self) {
49        self.stack.clear();
50    }
51
52    /// Check if the stack is empty
53    pub fn is_empty(&self) -> bool {
54        self.stack.is_empty()
55    }
56
57    /// Get the number of tables on the stack (not including default)
58    pub fn len(&self) -> usize {
59        self.stack.len()
60    }
61
62    /// Resolve a key combination by searching the stack from top to bottom
63    ///
64    /// Returns the action if found, None otherwise
65    pub fn resolve(&self, combo: &KeyCombo) -> Option<&KeyAction> {
66        // Search from top of stack downward
67        for activation in self.stack.iter().rev() {
68            if let Some(action) = activation.table.get(combo) {
69                return Some(action);
70            }
71        }
72
73        // Fall through to default table
74        self.default_table.get(combo)
75    }
76
77    /// Handle a key press, resolving it and managing one-shot/timeout behavior
78    ///
79    /// Returns the action if found, None otherwise
80    pub fn handle_key(&mut self, combo: KeyCombo, now: Instant) -> Option<KeyAction> {
81        // First, expire any timed-out tables
82        self.expire_timeouts(now);
83
84        // Try to resolve the key
85        let action = self.resolve(&combo).cloned();
86
87        // Handle one-shot mode: pop after any keypress
88        if let Some(top) = self.stack.last() {
89            if matches!(top.mode, ActivateKeyTableMode::OneShot) {
90                self.stack.pop();
91            }
92        }
93
94        action
95    }
96
97    /// Remove expired tables from the stack
98    fn expire_timeouts(&mut self, now: Instant) {
99        self.stack.retain(|activation| {
100            activation
101                .timeout
102                .map(|timeout| now < timeout)
103                .unwrap_or(true)
104        });
105    }
106
107    /// Check if any tables have timeouts and return the earliest timeout
108    pub fn next_timeout(&self) -> Option<Instant> {
109        self.stack.iter().filter_map(|a| a.timeout).min()
110    }
111
112    /// Get a reference to the default table
113    pub fn default_table(&self) -> &KeyTable {
114        &self.default_table
115    }
116
117    /// Get a mutable reference to the default table
118    pub fn default_table_mut(&mut self) -> &mut KeyTable {
119        &mut self.default_table
120    }
121}
122
123impl Default for KeyTableStack {
124    fn default() -> Self {
125        Self::new(KeyTable::new("default"))
126    }
127}
128
129/// An activation of a key table on the stack
130#[derive(Clone, Debug, Serialize, Deserialize)]
131pub struct KeyTableActivation {
132    /// Name of the activated table
133    pub name: String,
134    /// The key table itself
135    pub table: KeyTable,
136    /// Activation mode
137    pub mode: ActivateKeyTableMode,
138    /// Optional timeout (absolute time when this activation expires)
139    #[serde(skip)]
140    pub timeout: Option<Instant>,
141    /// Whether this activation replaces the previous one
142    pub replace_current: bool,
143}
144
145impl KeyTableActivation {
146    /// Create a new persistent activation
147    pub fn persistent(name: String, table: KeyTable) -> Self {
148        Self {
149            name,
150            table,
151            mode: ActivateKeyTableMode::Persistent,
152            timeout: None,
153            replace_current: false,
154        }
155    }
156
157    /// Create a new one-shot activation
158    pub fn one_shot(name: String, table: KeyTable) -> Self {
159        Self {
160            name,
161            table,
162            mode: ActivateKeyTableMode::OneShot,
163            timeout: None,
164            replace_current: false,
165        }
166    }
167
168    /// Create a new timed activation
169    pub fn timed(name: String, table: KeyTable, duration: Duration, now: Instant) -> Self {
170        Self {
171            name,
172            table,
173            mode: ActivateKeyTableMode::Timeout(duration),
174            timeout: Some(now + duration),
175            replace_current: false,
176        }
177    }
178
179    /// Set whether this activation should replace the current one
180    pub fn with_replace(mut self, replace: bool) -> Self {
181        self.replace_current = replace;
182        self
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::key_tables::{KeyCode, KeyModifiers};
190
191    fn create_test_table(name: &str) -> KeyTable {
192        let mut table = KeyTable::new(name);
193        table.bind(
194            KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
195            KeyAction::Noop,
196        );
197        table
198    }
199
200    #[test]
201    fn test_stack_push_pop() {
202        let mut stack = KeyTableStack::default();
203        assert_eq!(stack.len(), 0);
204        assert!(stack.is_empty());
205
206        let activation = KeyTableActivation::persistent("test".into(), create_test_table("test"));
207        stack.push(activation);
208
209        assert_eq!(stack.len(), 1);
210        assert!(!stack.is_empty());
211        assert_eq!(stack.current_name(), "test");
212
213        stack.pop();
214        assert_eq!(stack.len(), 0);
215        assert!(stack.is_empty());
216    }
217
218    #[test]
219    fn test_key_resolution_stack() {
220        let mut stack = KeyTableStack::default();
221
222        // Add a table with H bound
223        let table1 = create_test_table("test1");
224        stack.push(KeyTableActivation::persistent("test1".into(), table1));
225
226        // H should resolve to the table's action
227        let combo = KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE);
228        assert!(stack.resolve(&combo).is_some());
229
230        // J should not resolve (not in any table)
231        let combo = KeyCombo::new(KeyCode::KeyJ, KeyModifiers::NONE);
232        assert!(stack.resolve(&combo).is_none());
233    }
234
235    #[test]
236    fn test_one_shot_pop() {
237        let mut stack = KeyTableStack::default();
238        let now = Instant::now();
239
240        // Push a one-shot table
241        let table = create_test_table("oneshot");
242        stack.push(KeyTableActivation::one_shot("oneshot".into(), table));
243
244        assert_eq!(stack.len(), 1);
245
246        // Any keypress should pop the table
247        let combo = KeyCombo::new(KeyCode::KeyX, KeyModifiers::NONE);
248        stack.handle_key(combo, now);
249
250        assert_eq!(stack.len(), 0);
251    }
252
253    #[test]
254    fn test_timeout_expiration() {
255        let mut stack = KeyTableStack::default();
256        let now = Instant::now();
257
258        // Push a table with 100ms timeout
259        let table = create_test_table("timed");
260        stack.push(KeyTableActivation::timed(
261            "timed".into(),
262            table,
263            Duration::from_millis(100),
264            now,
265        ));
266
267        assert_eq!(stack.len(), 1);
268
269        // Should still be there immediately
270        stack.expire_timeouts(now);
271        assert_eq!(stack.len(), 1);
272
273        // Should be gone after timeout
274        let future = now + Duration::from_millis(101);
275        stack.expire_timeouts(future);
276        assert_eq!(stack.len(), 0);
277    }
278
279    #[test]
280    fn test_multiple_tables_resolution() {
281        let mut stack = KeyTableStack::default();
282
283        // Bottom table: H -> Noop
284        let mut table1 = KeyTable::new("bottom");
285        table1.bind(
286            KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
287            KeyAction::Noop,
288        );
289        stack.push(KeyTableActivation::persistent("bottom".into(), table1));
290
291        // Top table: H -> PopKeyTable (overrides bottom)
292        let mut table2 = KeyTable::new("top");
293        table2.bind(
294            KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE),
295            KeyAction::PopKeyTable,
296        );
297        stack.push(KeyTableActivation::persistent("top".into(), table2));
298
299        // H should resolve to PopKeyTable (from top table)
300        let combo = KeyCombo::new(KeyCode::KeyH, KeyModifiers::NONE);
301        let action = stack.resolve(&combo);
302        assert_eq!(action, Some(&KeyAction::PopKeyTable));
303    }
304
305    #[test]
306    fn test_clear_stack() {
307        let mut stack = KeyTableStack::default();
308
309        stack.push(KeyTableActivation::persistent(
310            "test1".into(),
311            create_test_table("test1"),
312        ));
313        stack.push(KeyTableActivation::persistent(
314            "test2".into(),
315            create_test_table("test2"),
316        ));
317
318        assert_eq!(stack.len(), 2);
319
320        stack.clear();
321        assert_eq!(stack.len(), 0);
322        assert!(stack.is_empty());
323    }
324
325    #[test]
326    fn test_next_timeout() {
327        let mut stack = KeyTableStack::default();
328        let now = Instant::now();
329
330        // No timeouts initially
331        assert!(stack.next_timeout().is_none());
332
333        // Add table with timeout
334        let table = create_test_table("timed");
335        stack.push(KeyTableActivation::timed(
336            "timed".into(),
337            table,
338            Duration::from_millis(100),
339            now,
340        ));
341
342        let next = stack.next_timeout();
343        assert!(next.is_some());
344        assert!(next.unwrap() > now);
345    }
346
347    #[test]
348    fn test_default_table_access() {
349        let mut stack = KeyTableStack::default();
350
351        // Modify default table
352        stack.default_table_mut().bind(
353            KeyCombo::new(KeyCode::KeyA, KeyModifiers::CTRL),
354            KeyAction::Noop,
355        );
356
357        // Should be able to resolve from default table
358        let combo = KeyCombo::new(KeyCode::KeyA, KeyModifiers::CTRL);
359        assert!(stack.resolve(&combo).is_some());
360    }
361}