Skip to main content

typ_core/
keymap.rs

1//! Chord string → `Action`, as data rather than control flow.
2//!
3//! Bindings live in a table because three things need to read them: the input
4//! loop, help text, and — once the vim layer lands — a second table swapped in
5//! wholesale. A `match` on `KeyCode` can be read by exactly one of those.
6
7use std::collections::BTreeMap;
8
9use anyhow::{Context, Result, anyhow};
10
11use crate::{Action, Direction, KeyChord, Motion};
12
13#[derive(Debug, Clone)]
14pub struct Keymap {
15    /// Canonical chord string → action. `BTreeMap` so `bindings_for` and any
16    /// help listing come out in a stable order rather than a hash order that
17    /// changes between runs.
18    bindings: BTreeMap<String, Action>,
19}
20
21/// The non-modal defaults, shaped like what someone arriving from a GUI editor
22/// already has in their fingers.
23const DEFAULTS: &[(&str, Action)] = &[
24    (
25        "left",
26        Action::Move {
27            motion: Motion::Left,
28            extend: false,
29        },
30    ),
31    (
32        "shift+left",
33        Action::Move {
34            motion: Motion::Left,
35            extend: true,
36        },
37    ),
38    (
39        "right",
40        Action::Move {
41            motion: Motion::Right,
42            extend: false,
43        },
44    ),
45    (
46        "shift+right",
47        Action::Move {
48            motion: Motion::Right,
49            extend: true,
50        },
51    ),
52    (
53        "up",
54        Action::Move {
55            motion: Motion::Up,
56            extend: false,
57        },
58    ),
59    (
60        "shift+up",
61        Action::Move {
62            motion: Motion::Up,
63            extend: true,
64        },
65    ),
66    (
67        "down",
68        Action::Move {
69            motion: Motion::Down,
70            extend: false,
71        },
72    ),
73    (
74        "shift+down",
75        Action::Move {
76            motion: Motion::Down,
77            extend: true,
78        },
79    ),
80    (
81        "ctrl+left",
82        Action::Move {
83            motion: Motion::WordLeft,
84            extend: false,
85        },
86    ),
87    (
88        "ctrl+shift+left",
89        Action::Move {
90            motion: Motion::WordLeft,
91            extend: true,
92        },
93    ),
94    (
95        "ctrl+right",
96        Action::Move {
97            motion: Motion::WordRight,
98            extend: false,
99        },
100    ),
101    (
102        "ctrl+shift+right",
103        Action::Move {
104            motion: Motion::WordRight,
105            extend: true,
106        },
107    ),
108    (
109        "home",
110        Action::Move {
111            motion: Motion::LineStart,
112            extend: false,
113        },
114    ),
115    (
116        "shift+home",
117        Action::Move {
118            motion: Motion::LineStart,
119            extend: true,
120        },
121    ),
122    (
123        "end",
124        Action::Move {
125            motion: Motion::LineEnd,
126            extend: false,
127        },
128    ),
129    (
130        "shift+end",
131        Action::Move {
132            motion: Motion::LineEnd,
133            extend: true,
134        },
135    ),
136    (
137        "pageup",
138        Action::Move {
139            motion: Motion::PageUp,
140            extend: false,
141        },
142    ),
143    (
144        "shift+pageup",
145        Action::Move {
146            motion: Motion::PageUp,
147            extend: true,
148        },
149    ),
150    (
151        "pagedown",
152        Action::Move {
153            motion: Motion::PageDown,
154            extend: false,
155        },
156    ),
157    (
158        "shift+pagedown",
159        Action::Move {
160            motion: Motion::PageDown,
161            extend: true,
162        },
163    ),
164    (
165        "ctrl+home",
166        Action::Move {
167            motion: Motion::DocumentStart,
168            extend: false,
169        },
170    ),
171    (
172        "ctrl+shift+home",
173        Action::Move {
174            motion: Motion::DocumentStart,
175            extend: true,
176        },
177    ),
178    (
179        "ctrl+end",
180        Action::Move {
181            motion: Motion::DocumentEnd,
182            extend: false,
183        },
184    ),
185    (
186        "ctrl+shift+end",
187        Action::Move {
188            motion: Motion::DocumentEnd,
189            extend: true,
190        },
191    ),
192    (
193        "backspace",
194        Action::Delete {
195            direction: Direction::Backward,
196            by_word: false,
197        },
198    ),
199    (
200        "ctrl+backspace",
201        Action::Delete {
202            direction: Direction::Backward,
203            by_word: true,
204        },
205    ),
206    (
207        "delete",
208        Action::Delete {
209            direction: Direction::Forward,
210            by_word: false,
211        },
212    ),
213    (
214        "ctrl+delete",
215        Action::Delete {
216            direction: Direction::Forward,
217            by_word: true,
218        },
219    ),
220    ("enter", Action::InsertNewline),
221    ("ctrl+z", Action::Undo),
222    ("ctrl+y", Action::Redo),
223    ("ctrl+a", Action::SelectAll),
224    ("ctrl+l", Action::SelectLine),
225    ("esc", Action::CollapseSelections),
226    ("ctrl+alt+up", Action::AddCursor(Direction::Backward)),
227    ("ctrl+alt+down", Action::AddCursor(Direction::Forward)),
228    ("ctrl+s", Action::Save),
229    ("ctrl+q", Action::Quit),
230    ("tab", Action::FocusNext),
231    ("ctrl+f", Action::SearchOpen),
232    ("f3", Action::SearchNext),
233    ("shift+f3", Action::SearchPrevious),
234    ("ctrl+h", Action::ReplaceOpen),
235];
236
237impl Keymap {
238    pub fn default_bindings() -> Self {
239        Self {
240            bindings: DEFAULTS
241                .iter()
242                .map(|(chord, action)| ((*chord).to_string(), *action))
243                .collect(),
244        }
245    }
246
247    pub fn lookup(&self, chord: &KeyChord) -> Option<Action> {
248        self.bindings.get(&chord.canonical).copied()
249    }
250
251    /// Chords bound to an action, for help text and the future palette.
252    pub fn bindings_for(&self, action: Action) -> Vec<&str> {
253        self.bindings
254            .iter()
255            .filter(|(_, a)| **a == action)
256            .map(|(chord, _)| chord.as_str())
257            .collect()
258    }
259
260    /// Apply a user config over the current bindings.
261    ///
262    /// Parsed into a staging list first, so a config with one bad line changes
263    /// nothing. A half-applied keymap is worse than a rejected one: the user
264    /// cannot tell which half took effect.
265    pub fn merge_toml(&mut self, src: &str) -> Result<()> {
266        let table: BTreeMap<String, String> =
267            toml::from_str(src).context("parsing the keybinding table")?;
268
269        let mut staged: Vec<(String, Option<Action>)> = Vec::new();
270        for (chord, action_name) in table {
271            if action_name.is_empty() {
272                // An empty action unbinds, which a user needs in order to free
273                // a chord their terminal or window manager wants for itself.
274                staged.push((chord, None));
275                continue;
276            }
277            let action = Action::from_name(&action_name)
278                .ok_or_else(|| anyhow!("{chord} is bound to an unknown action: {action_name}"))?;
279            staged.push((chord, Some(action)));
280        }
281
282        for (chord, action) in staged {
283            match action {
284                Some(action) => {
285                    self.bindings.insert(chord, action);
286                }
287                None => {
288                    self.bindings.remove(&chord);
289                }
290            }
291        }
292        Ok(())
293    }
294}
295
296impl Default for Keymap {
297    fn default() -> Self {
298        Self::default_bindings()
299    }
300}