ex_command/ex_command.rs
1//! Ex-command (`:wq`) execution, composed entirely caller-side.
2//!
3//! A command line like vim's `:wq` is **not** a chord sequence, so it does not
4//! use `keymap-seq` (that crate is a prefix-free chord trie; a command line is an
5//! open string language — `:w`, `:wq`, `:wqa` coexist, and editing keys mutate a
6//! buffer rather than extend a sequence). The *execution* path decomposes into
7//! three stages, and the library adds no new type for them:
8//!
9//! 1. **Press `:`** — an ordinary single-key binding. `Keymap::get` returns
10//! `EnterCommandMode`, exactly like any other action.
11//! 2. **Capture `wq`** — the typed text accumulates into a caller-owned line
12//! buffer. This is caller state, the same way `keymap-seq`'s pending buffer is.
13//! 3. **Dispatch on Enter** — the buffer is resolved to an action by a
14//! `FnMut(&str) -> Option<Action>` closure. That is the *same shape* as
15//! `keymap_config::from_str`'s name resolver; we reuse the shape, not that
16//! crate's type, so the execution path remains a plain closure.
17//!
18//! **Execution vs discovery are orthogonal.** This example shows the execution
19//! path. For the *discovery* layer — front-prefix completion (`:w<Tab>`) and a
20//! full palette listing — see `examples/command_palette.rs` and
21//! [`keymap_core::cmd::CommandIndex`]. The two are independent: you can use one
22//! without the other, and combining them is a matter of wiring in the caller.
23//!
24//! Deliberate non-goals: compound splitting (`wq` → `w` + `q`), arguments
25//! (`:w file`), ranges (`:%s/a/b/g`). See `docs/ROADMAP.md`.
26//!
27//! Run with: `cargo run -p keymap-core --example ex_command`
28
29use keymap_core::{Key, KeyInput, Keymap, Modifiers};
30
31#[derive(Clone, Debug, PartialEq)]
32enum Action {
33 /// Bound to `:` in the keymap — opens the command line.
34 EnterCommandMode,
35 /// Resolved from the command name on Enter.
36 Write,
37 Quit,
38 WriteQuit,
39}
40
41/// The caller's own command-line state. The library never sees this type; the
42/// line buffer lives *inside* the `Command` variant, so a buffer can't exist
43/// while we're in `Normal` mode (illegal states are unrepresentable).
44enum Mode {
45 Normal,
46 Command(String),
47}
48
49fn plain(c: char) -> KeyInput {
50 KeyInput::new(Key::Char(c), Modifiers::NONE)
51}
52
53fn enter() -> KeyInput {
54 KeyInput::new(Key::Enter, Modifiers::NONE)
55}
56
57fn main() {
58 let mut map = Keymap::new();
59 map.bind(plain(':'), Action::EnterCommandMode);
60
61 // Stage 3's resolver: command name -> action. Same shape as
62 // `keymap_config`'s `FnMut(&str) -> Option<A>`, intentionally a plain
63 // closure rather than that crate's type.
64 let resolve = |name: &str| -> Option<Action> {
65 match name {
66 "w" => Some(Action::Write),
67 "q" => Some(Action::Quit),
68 "wq" => Some(Action::WriteQuit),
69 _ => None,
70 }
71 };
72
73 // A simulated keystroke stream: `:wq⏎`, then `:q⏎`, then an unknown `:zz⏎`.
74 let stream = [
75 plain(':'),
76 plain('w'),
77 plain('q'),
78 enter(),
79 plain(':'),
80 plain('q'),
81 enter(),
82 plain(':'),
83 plain('z'),
84 plain('z'),
85 enter(),
86 ];
87
88 let mut mode = Mode::Normal;
89 for key in stream {
90 match &mut mode {
91 Mode::Normal => {
92 if map.get(&key) == Some(&Action::EnterCommandMode) {
93 println!(": -> enter command mode");
94 mode = Mode::Command(String::new());
95 } else {
96 // Any other normal-mode key would run its own binding; this
97 // demo only cares about the `:` entry point.
98 }
99 }
100 Mode::Command(buf) => match key.key() {
101 Key::Char(c) => {
102 buf.push(c);
103 println!(" :{buf}");
104 }
105 Key::Enter => {
106 // Take the buffer out and drop back to Normal in one move.
107 let name = std::mem::take(buf);
108 match resolve(&name) {
109 Some(action) => println!(" :{name} -> fire {action:?}"),
110 None => println!(" :{name} -> unknown command, no-op"),
111 }
112 mode = Mode::Normal;
113 }
114 Key::Esc => {
115 println!(" esc -> abandon command line");
116 mode = Mode::Normal;
117 }
118 // `Key` is #[non_exhaustive]; other editing keys are ignored here.
119 _ => {}
120 },
121 }
122 }
123}