ex_command/ex_command.rs
1//! Ex-command (`:wq`) input, 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). It decomposes into three stages, and
7//! the library adds *no new type* for any of 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, deliberately
16//! not that crate's type, so the command line stays free of a new public API.
17//!
18//! Deliberate non-goals (until demand is proven — see `docs/ROADMAP.md`):
19//! compound splitting (`wq` → `w` + `q`), arguments (`:w file`), ranges
20//! (`:%s/a/b/g`), and `:`-completion.
21//!
22//! Run with: `cargo run -p keymap-core --example ex_command`
23
24use keymap_core::{Key, KeyInput, Keymap, Modifiers};
25
26#[derive(Clone, Debug, PartialEq)]
27enum Action {
28 /// Bound to `:` in the keymap — opens the command line.
29 EnterCommandMode,
30 /// Resolved from the command name on Enter.
31 Write,
32 Quit,
33 WriteQuit,
34}
35
36/// The caller's own command-line state. The library never sees this type; the
37/// line buffer lives *inside* the `Command` variant, so a buffer can't exist
38/// while we're in `Normal` mode (illegal states are unrepresentable).
39enum Mode {
40 Normal,
41 Command(String),
42}
43
44fn plain(c: char) -> KeyInput {
45 KeyInput::new(Key::Char(c), Modifiers::NONE)
46}
47
48fn enter() -> KeyInput {
49 KeyInput::new(Key::Enter, Modifiers::NONE)
50}
51
52fn main() {
53 let mut map = Keymap::new();
54 map.bind(plain(':'), Action::EnterCommandMode);
55
56 // Stage 3's resolver: command name -> action. Same shape as
57 // `keymap_config`'s `FnMut(&str) -> Option<A>`, intentionally a plain
58 // closure rather than that crate's type.
59 let resolve = |name: &str| -> Option<Action> {
60 match name {
61 "w" => Some(Action::Write),
62 "q" => Some(Action::Quit),
63 "wq" => Some(Action::WriteQuit),
64 _ => None,
65 }
66 };
67
68 // A simulated keystroke stream: `:wq⏎`, then `:q⏎`, then an unknown `:zz⏎`.
69 let stream = [
70 plain(':'),
71 plain('w'),
72 plain('q'),
73 enter(),
74 plain(':'),
75 plain('q'),
76 enter(),
77 plain(':'),
78 plain('z'),
79 plain('z'),
80 enter(),
81 ];
82
83 let mut mode = Mode::Normal;
84 for key in stream {
85 match &mut mode {
86 Mode::Normal => {
87 if map.get(&key) == Some(&Action::EnterCommandMode) {
88 println!(": -> enter command mode");
89 mode = Mode::Command(String::new());
90 } else {
91 // Any other normal-mode key would run its own binding; this
92 // demo only cares about the `:` entry point.
93 }
94 }
95 Mode::Command(buf) => match key.key() {
96 Key::Char(c) => {
97 buf.push(c);
98 println!(" :{buf}");
99 }
100 Key::Enter => {
101 // Take the buffer out and drop back to Normal in one move.
102 let name = std::mem::take(buf);
103 match resolve(&name) {
104 Some(action) => println!(" :{name} -> fire {action:?}"),
105 None => println!(" :{name} -> unknown command, no-op"),
106 }
107 mode = Mode::Normal;
108 }
109 Key::Esc => {
110 println!(" esc -> abandon command line");
111 mode = Mode::Normal;
112 }
113 // `Key` is #[non_exhaustive]; other editing keys are ignored here.
114 _ => {}
115 },
116 }
117 }
118}