leader_sequence/leader_sequence.rs
1//! A leader / prefix-chord keymap driven by a simulated key stream.
2//!
3//! Shows the intended split: `keymap-seq` answers "exact / prefix / miss" for the
4//! keys so far, and the *caller* owns the pending buffer and decides when to fire
5//! or reset. The second demo (`timed`) adds the time dimension a `jj`-style
6//! binding needs — still entirely caller-side, because the library holds no clock.
7//! Run with `cargo run -p keymap-seq --example leader_sequence`.
8
9use std::time::Duration;
10
11use keymap_core::{Key, KeyInput, Modifiers};
12use keymap_seq::{Match, PendingSequence, SequenceKeymap, Step};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum Action {
16 Save,
17 Quit,
18 GotoTop,
19 NormalMode,
20}
21
22fn ctrl(c: char) -> KeyInput {
23 KeyInput::new(Key::Char(c), Modifiers::CTRL)
24}
25
26fn plain(c: char) -> KeyInput {
27 KeyInput::new(Key::Char(c), Modifiers::NONE)
28}
29
30fn main() {
31 untimed();
32 println!();
33 timed();
34}
35
36/// Resolution without a clock: the buffer resets only on `Exact`/`NoMatch`.
37fn untimed() {
38 println!("== untimed ==");
39 let mut map = SequenceKeymap::new();
40 map.bind([ctrl('x'), ctrl('s')], Action::Save).unwrap();
41 map.bind([ctrl('x'), ctrl('c')], Action::Quit).unwrap();
42 map.bind([plain('g'), plain('g')], Action::GotoTop).unwrap();
43
44 // A stream a terminal might deliver: a completed save, an abandoned prefix
45 // (`ctrl+x` then an unrelated key), then `g g`.
46 let stream = [
47 ctrl('x'),
48 ctrl('s'),
49 ctrl('x'),
50 plain('z'),
51 plain('g'),
52 plain('g'),
53 ];
54
55 let mut pending: Vec<KeyInput> = Vec::new();
56 for key in stream {
57 pending.push(key);
58 match map.lookup(&pending) {
59 Match::Exact(action) => {
60 println!("{} -> fire {action:?}", render(&pending));
61 pending.clear();
62 }
63 Match::Prefix => {
64 println!("{} -> prefix, waiting", render(&pending));
65 }
66 Match::NoMatch => {
67 println!("{} -> no binding, passing through", render(&pending));
68 pending.clear();
69 }
70 }
71 }
72}
73
74/// `jj`-style time window, driven through [`PendingSequence`]. The window —
75/// "abandon a pending prefix if the next key is too slow" — is the *caller's*
76/// policy, not the table's: `keymap-seq` has no clock, so the caller measures
77/// inter-key time and decides when a prefix is stale. The helper owns the buffer
78/// bookkeeping; the caller owns only the timing decision and the one `flush` call
79/// it triggers.
80///
81/// Note what this demo does *not* do: real vim `jj` also binds `j` on its own (a
82/// literal cursor move), so a lone `j` must fire while a quick `j j` escapes. That
83/// needs both `j` *and* `[j, j]` bound — which the prefix-free invariant forbids
84/// (`bind` would reject it with `PrefixShadow`). Resolving "is this `j` a literal
85/// or the first half of `jj`?" is therefore caller timing policy layered on top
86/// of `lookup`, not a trie outcome. Here `j` alone is unbound, so the timeout is
87/// the *only* caller policy needed.
88///
89/// Unlike the raw-loop version this once was, the idle flush is *exercised*, not
90/// just described: a too-slow key abandons the held prefix (mid-stream), and the
91/// stream ends mid-prefix so the trailing `flush` drains the dangling `j` as a
92/// literal — the case a real caller's idle timer fires on when no further key
93/// arrives.
94fn timed() {
95 const WINDOW: Duration = Duration::from_millis(500);
96
97 println!("== timed (jj) ==");
98 let mut map = SequenceKeymap::new();
99 map.bind([plain('j'), plain('j')], Action::NormalMode)
100 .unwrap();
101
102 // `(key, timestamp-since-start)`: a deterministic stand-in for an event
103 // loop's clock (no real `Instant`/`sleep`, so the demo can't be flaky). We
104 // compare the *inter-key* gap to the window — the gap since the last key the
105 // pending prefix accepted, which is what "pressed twice quickly" means.
106 let stream = [
107 (plain('j'), Duration::from_millis(0)),
108 (plain('j'), Duration::from_millis(120)), // quick: completes `jj`
109 (plain('j'), Duration::from_millis(900)),
110 (plain('j'), Duration::from_millis(1700)), // 800ms gap: too slow
111 ];
112
113 let mut pending = PendingSequence::new();
114 let mut last: Option<Duration> = None;
115 for (key, now) in stream {
116 // Timeout check lives here, in the caller, before the new key is judged:
117 // a too-slow key means the held prefix was abandoned, so flush it (pass
118 // its keys through as literals) and let this key start fresh.
119 if let Some(prev) = last {
120 if now.saturating_sub(prev) > WINDOW && !pending.is_empty() {
121 let dropped = pending.flush();
122 println!(
123 "{} @ {now:?} -> idle ({:?} gap > {WINDOW:?}), flushed as literals",
124 render(&dropped),
125 now.saturating_sub(prev),
126 );
127 }
128 }
129
130 // `last` tracks the time of the key that last extended a live prefix, so
131 // it is set solely by this resolution: only `Pending` leaves a prefix
132 // waiting on the clock.
133 last = match pending.feed(&map, key) {
134 Step::Fired(action) => {
135 println!("{key} @ {now:?} -> fire {action:?}");
136 None
137 }
138 Step::Pending => {
139 println!("{key} @ {now:?} -> prefix, waiting (window {WINDOW:?})");
140 Some(now)
141 }
142 Step::PassThrough(keys) => {
143 println!("{} @ {now:?} -> no binding, passing through", render(&keys));
144 None
145 }
146 };
147 }
148
149 // The stream ended mid-prefix. A real caller's idle timer would fire after the
150 // window with no further key; here we flush that dangling `j` as a literal —
151 // the step the old version of this example could only describe in a comment.
152 let dangling = pending.flush();
153 if !dangling.is_empty() {
154 println!(
155 "{} -> still pending at end; idle timer flushes it as a literal",
156 render(&dangling)
157 );
158 }
159}
160
161fn render(keys: &[KeyInput]) -> String {
162 keys.iter()
163 .map(ToString::to_string)
164 .collect::<Vec<_>>()
165 .join(" ")
166}