onedrop_gui/
keymap.rs

1//! Keyboard → action mapping for the OneDrop GUI.
2//!
3//! The GUI's `handle_keyboard` dispatches every key event through
4//! [`Keymap::lookup`]. Defaults match the MD2 keybind table (see
5//! `objectives_to_reach.md` §7); the user can override any binding by
6//! editing `[keymap.bindings]` in `~/.config/onedrop/settings.toml` —
7//! the override is applied at startup via [`Keymap::from_settings`].
8
9use std::collections::BTreeMap;
10
11use winit::keyboard::KeyCode;
12
13use crate::settings::KeymapSettings;
14
15/// Every action the GUI can dispatch from a keybind. Adding an action
16/// here requires (a) a default binding in [`Keymap::default`] and
17/// (b) a match arm in `App::dispatch_action`. The string form returned
18/// by [`Action::as_str`] is the stable key used in `settings.toml`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub enum Action {
21    // ----- Transport ---------------------------------------------------
22    /// Next preset (mode-aware: random or sequential), with crossfade.
23    NextPreset,
24    /// Next preset, instant cut (no crossfade).
25    NextPresetInstant,
26    /// History-backed previous preset.
27    PreviousPreset,
28    /// Toggle Random ↔ Sequential selection mode.
29    ToggleRandomMode,
30    /// Toggle preset lock (Scroll Lock).
31    TogglePresetLock,
32    /// Engine reset (re-evaluate init).
33    EngineReset,
34    /// Load-preset menu (placeholder until §11).
35    LoadMenu,
36
37    // ----- Rating ------------------------------------------------------
38    /// Nudge current preset rating +1 (clamped to 5).
39    RateUp,
40    /// Nudge current preset rating -1 (clamped to 0).
41    RateDown,
42
43    // ----- Edit / save / monitor (placeholders) ------------------------
44    EditMenu,
45    SavePreset,
46    ToggleMonitor,
47    MashUp,
48    CycleShaderLock,
49
50    // ----- Overlay info / help ----------------------------------------
51    ShowHelp,
52    ShowSongInfo,
53    ShowFps,
54    ShowPresetName,
55    ShowRating,
56    RereadIni,
57    ToggleAnaglyph3D,
58
59    // ----- Beat detection / fullscreen / window ----------------------
60    CycleBeatDetection,
61    ToggleFullscreen,
62    ToggleOptions,
63    Quit,
64
65    // ----- Sprites / messages (existing, kept in the new dispatch) ----
66    SpriteCycle,
67    SpriteRandom,
68    SpritePopRecent,
69    MessageCycle,
70    ClearAllOverlays,
71    ClearMessages,
72
73    // ----- Quick-edit warp / rotation / zoom (placeholders) -----------
74    WarpAmountDown,
75    WarpAmountUp,
76    RotationCcw,
77    RotationCw,
78    ZoomOut,
79    ZoomIn,
80    WarpSpeedDown,
81    WarpSpeedUp,
82
83    // ----- Waveform (placeholders) ------------------------------------
84    CycleWaveform,
85    WaveScaleDown,
86    WaveScaleUp,
87    WaveOpacityDown,
88    WaveOpacityUp,
89}
90
91impl Action {
92    /// Stable name used as the key in `settings.toml`. Snake_case so it
93    /// reads naturally in TOML and survives Action enum rename refactors
94    /// independently of the Rust identifier.
95    #[allow(dead_code)] // Symmetric to `from_str`; used by tests and
96    // exposed as part of the public API so future settings-write
97    // paths (Options panel editor) can round-trip bindings.
98    pub fn as_str(self) -> &'static str {
99        match self {
100            Action::NextPreset => "next_preset",
101            Action::NextPresetInstant => "next_preset_instant",
102            Action::PreviousPreset => "previous_preset",
103            Action::ToggleRandomMode => "toggle_random_mode",
104            Action::TogglePresetLock => "toggle_preset_lock",
105            Action::EngineReset => "engine_reset",
106            Action::LoadMenu => "load_menu",
107            Action::RateUp => "rate_up",
108            Action::RateDown => "rate_down",
109            Action::EditMenu => "edit_menu",
110            Action::SavePreset => "save_preset",
111            Action::ToggleMonitor => "toggle_monitor",
112            Action::MashUp => "mash_up",
113            Action::CycleShaderLock => "cycle_shader_lock",
114            Action::ShowHelp => "show_help",
115            Action::ShowSongInfo => "show_song_info",
116            Action::ShowFps => "show_fps",
117            Action::ShowPresetName => "show_preset_name",
118            Action::ShowRating => "show_rating",
119            Action::RereadIni => "reread_ini",
120            Action::ToggleAnaglyph3D => "toggle_anaglyph_3d",
121            Action::CycleBeatDetection => "cycle_beat_detection",
122            Action::ToggleFullscreen => "toggle_fullscreen",
123            Action::ToggleOptions => "toggle_options",
124            Action::Quit => "quit",
125            Action::SpriteCycle => "sprite_cycle",
126            Action::SpriteRandom => "sprite_random",
127            Action::SpritePopRecent => "sprite_pop_recent",
128            Action::MessageCycle => "message_cycle",
129            Action::ClearAllOverlays => "clear_all_overlays",
130            Action::ClearMessages => "clear_messages",
131            Action::WarpAmountDown => "warp_amount_down",
132            Action::WarpAmountUp => "warp_amount_up",
133            Action::RotationCcw => "rotation_ccw",
134            Action::RotationCw => "rotation_cw",
135            Action::ZoomOut => "zoom_out",
136            Action::ZoomIn => "zoom_in",
137            Action::WarpSpeedDown => "warp_speed_down",
138            Action::WarpSpeedUp => "warp_speed_up",
139            Action::CycleWaveform => "cycle_waveform",
140            Action::WaveScaleDown => "wave_scale_down",
141            Action::WaveScaleUp => "wave_scale_up",
142            Action::WaveOpacityDown => "wave_opacity_down",
143            Action::WaveOpacityUp => "wave_opacity_up",
144        }
145    }
146
147    fn from_str(s: &str) -> Option<Action> {
148        Some(match s {
149            "next_preset" => Action::NextPreset,
150            "next_preset_instant" => Action::NextPresetInstant,
151            "previous_preset" => Action::PreviousPreset,
152            "toggle_random_mode" => Action::ToggleRandomMode,
153            "toggle_preset_lock" => Action::TogglePresetLock,
154            "engine_reset" => Action::EngineReset,
155            "load_menu" => Action::LoadMenu,
156            "rate_up" => Action::RateUp,
157            "rate_down" => Action::RateDown,
158            "edit_menu" => Action::EditMenu,
159            "save_preset" => Action::SavePreset,
160            "toggle_monitor" => Action::ToggleMonitor,
161            "mash_up" => Action::MashUp,
162            "cycle_shader_lock" => Action::CycleShaderLock,
163            "show_help" => Action::ShowHelp,
164            "show_song_info" => Action::ShowSongInfo,
165            "show_fps" => Action::ShowFps,
166            "show_preset_name" => Action::ShowPresetName,
167            "show_rating" => Action::ShowRating,
168            "reread_ini" => Action::RereadIni,
169            "toggle_anaglyph_3d" => Action::ToggleAnaglyph3D,
170            "cycle_beat_detection" => Action::CycleBeatDetection,
171            "toggle_fullscreen" => Action::ToggleFullscreen,
172            "toggle_options" => Action::ToggleOptions,
173            "quit" => Action::Quit,
174            "sprite_cycle" => Action::SpriteCycle,
175            "sprite_random" => Action::SpriteRandom,
176            "sprite_pop_recent" => Action::SpritePopRecent,
177            "message_cycle" => Action::MessageCycle,
178            "clear_all_overlays" => Action::ClearAllOverlays,
179            "clear_messages" => Action::ClearMessages,
180            "warp_amount_down" => Action::WarpAmountDown,
181            "warp_amount_up" => Action::WarpAmountUp,
182            "rotation_ccw" => Action::RotationCcw,
183            "rotation_cw" => Action::RotationCw,
184            "zoom_out" => Action::ZoomOut,
185            "zoom_in" => Action::ZoomIn,
186            "warp_speed_down" => Action::WarpSpeedDown,
187            "warp_speed_up" => Action::WarpSpeedUp,
188            "cycle_waveform" => Action::CycleWaveform,
189            "wave_scale_down" => Action::WaveScaleDown,
190            "wave_scale_up" => Action::WaveScaleUp,
191            "wave_opacity_down" => Action::WaveOpacityDown,
192            "wave_opacity_up" => Action::WaveOpacityUp,
193            _ => return None,
194        })
195    }
196}
197
198/// Modifier-state bits we care about for dispatch. Stored as a small
199/// bitset so `Binding` stays trivially `Copy`/`Hash`.
200#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
201pub struct Modifiers {
202    pub ctrl: bool,
203    pub shift: bool,
204    pub alt: bool,
205}
206
207impl Modifiers {
208    pub const NONE: Modifiers = Modifiers {
209        ctrl: false,
210        shift: false,
211        alt: false,
212    };
213    pub const CTRL: Modifiers = Modifiers {
214        ctrl: true,
215        shift: false,
216        alt: false,
217    };
218    pub const SHIFT: Modifiers = Modifiers {
219        ctrl: false,
220        shift: true,
221        alt: false,
222    };
223
224    pub fn from_winit(m: winit::keyboard::ModifiersState) -> Self {
225        Self {
226            ctrl: m.control_key(),
227            shift: m.shift_key(),
228            alt: m.alt_key(),
229        }
230    }
231}
232
233/// A single key chord — a physical key plus the modifier flags that
234/// must be held while it's pressed.
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
236pub struct Binding {
237    pub key: KeyCode,
238    pub mods: Modifiers,
239}
240
241impl Binding {
242    pub const fn new(key: KeyCode, mods: Modifiers) -> Self {
243        Self { key, mods }
244    }
245
246    /// Bare key (no modifiers held). Most keybinds use this.
247    pub const fn bare(key: KeyCode) -> Self {
248        Self {
249            key,
250            mods: Modifiers::NONE,
251        }
252    }
253}
254
255/// Mutable, runtime keybind table. Built from the GUI's MD2-style
256/// defaults plus optional user overrides from
257/// [`crate::settings::KeymapSettings`].
258#[derive(Debug, Clone)]
259pub struct Keymap {
260    table: BTreeMap<Binding, Action>,
261}
262
263impl Keymap {
264    /// MD2-style default table. Mirrors §7's roadmap table — bindings
265    /// that don't have a physical-key equivalent (e.g. `+` / `-` on
266    /// keyboards with separate layouts) use the most common US-ANSI key.
267    pub fn default_table() -> Self {
268        let mut t = BTreeMap::new();
269        let mut bind = |b: Binding, a: Action| {
270            t.insert(b, a);
271        };
272
273        // Transport.
274        bind(Binding::bare(KeyCode::Space), Action::NextPreset);
275        bind(Binding::bare(KeyCode::ArrowRight), Action::NextPreset);
276        bind(Binding::bare(KeyCode::KeyH), Action::NextPresetInstant);
277        bind(Binding::bare(KeyCode::Backspace), Action::PreviousPreset);
278        bind(Binding::bare(KeyCode::ArrowLeft), Action::PreviousPreset);
279        bind(Binding::bare(KeyCode::KeyR), Action::ToggleRandomMode);
280        bind(Binding::bare(KeyCode::ScrollLock), Action::TogglePresetLock);
281        bind(Binding::bare(KeyCode::F5), Action::EngineReset);
282        bind(Binding::bare(KeyCode::KeyL), Action::LoadMenu);
283
284        // Ratings: Equal is the unshifted "=" / "+" key on US-ANSI;
285        // Minus is the dash key. Both unshifted to match MD2.
286        bind(Binding::bare(KeyCode::Equal), Action::RateUp);
287        bind(Binding::bare(KeyCode::NumpadAdd), Action::RateUp);
288        bind(Binding::bare(KeyCode::Minus), Action::RateDown);
289        bind(Binding::bare(KeyCode::NumpadSubtract), Action::RateDown);
290
291        // Edit / save / monitor (mostly stubs in v1.0).
292        bind(Binding::bare(KeyCode::KeyM), Action::EditMenu);
293        bind(Binding::bare(KeyCode::KeyS), Action::SavePreset);
294        bind(Binding::bare(KeyCode::KeyN), Action::ToggleMonitor);
295        bind(Binding::bare(KeyCode::KeyA), Action::MashUp);
296        bind(Binding::bare(KeyCode::KeyD), Action::CycleShaderLock);
297
298        // Info overlays.
299        bind(Binding::bare(KeyCode::F1), Action::ShowHelp);
300        bind(Binding::bare(KeyCode::F2), Action::ShowSongInfo);
301        bind(Binding::bare(KeyCode::F3), Action::ShowFps);
302        bind(Binding::bare(KeyCode::F4), Action::ShowPresetName);
303        bind(Binding::bare(KeyCode::F6), Action::ShowRating);
304        bind(Binding::bare(KeyCode::F7), Action::RereadIni);
305        bind(Binding::bare(KeyCode::F9), Action::ToggleAnaglyph3D);
306
307        // Beat / window.
308        bind(Binding::bare(KeyCode::F8), Action::CycleBeatDetection);
309        bind(Binding::bare(KeyCode::F11), Action::ToggleFullscreen);
310        bind(Binding::bare(KeyCode::Tab), Action::ToggleOptions);
311        bind(Binding::bare(KeyCode::Escape), Action::Quit);
312        bind(Binding::bare(KeyCode::KeyQ), Action::Quit);
313
314        // Sprites / messages.
315        bind(Binding::bare(KeyCode::KeyK), Action::SpriteCycle);
316        bind(
317            Binding::new(KeyCode::KeyK, Modifiers::SHIFT),
318            Action::SpriteRandom,
319        );
320        bind(Binding::bare(KeyCode::Delete), Action::SpritePopRecent);
321        bind(Binding::bare(KeyCode::KeyT), Action::MessageCycle);
322        bind(
323            Binding::new(KeyCode::KeyT, Modifiers::CTRL),
324            Action::ClearAllOverlays,
325        );
326        bind(
327            Binding::new(KeyCode::KeyY, Modifiers::CTRL),
328            Action::ClearMessages,
329        );
330
331        // Quick-edit warp / rotation / zoom / waveform — placeholders
332        // wired to the action enum but stubbed by the dispatch in v1.0.
333        bind(Binding::bare(KeyCode::BracketLeft), Action::WarpAmountDown);
334        bind(Binding::bare(KeyCode::BracketRight), Action::WarpAmountUp);
335        bind(Binding::bare(KeyCode::Comma), Action::RotationCcw);
336        bind(Binding::bare(KeyCode::Period), Action::RotationCw);
337        bind(Binding::bare(KeyCode::KeyO), Action::ZoomOut);
338        bind(
339            Binding::new(KeyCode::KeyO, Modifiers::SHIFT),
340            Action::ZoomIn,
341        );
342        bind(Binding::bare(KeyCode::KeyI), Action::WarpSpeedDown);
343        bind(
344            Binding::new(KeyCode::KeyI, Modifiers::SHIFT),
345            Action::WarpSpeedUp,
346        );
347        bind(Binding::bare(KeyCode::KeyW), Action::CycleWaveform);
348        bind(Binding::bare(KeyCode::KeyJ), Action::WaveScaleDown);
349        bind(
350            Binding::new(KeyCode::KeyJ, Modifiers::SHIFT),
351            Action::WaveScaleUp,
352        );
353        bind(Binding::bare(KeyCode::KeyE), Action::WaveOpacityDown);
354        bind(
355            Binding::new(KeyCode::KeyE, Modifiers::SHIFT),
356            Action::WaveOpacityUp,
357        );
358
359        Self { table: t }
360    }
361
362    /// Build the runtime keymap by starting from [`default_table`] and
363    /// applying user overrides. Each override removes any existing
364    /// bindings pointing at the overridden action, then installs the
365    /// new chord. Parse failures are logged and skipped — the rest of
366    /// the table still applies.
367    pub fn from_settings(s: &KeymapSettings) -> Self {
368        let mut km = Self::default_table();
369        for (action_name, chord) in &s.bindings {
370            let Some(action) = Action::from_str(action_name) else {
371                log::warn!(
372                    "keymap: unknown action {:?} in settings — ignored",
373                    action_name
374                );
375                continue;
376            };
377            let Some(binding) = parse_chord(chord) else {
378                log::warn!(
379                    "keymap: failed to parse chord {:?} for action {:?} — ignored",
380                    chord,
381                    action_name
382                );
383                continue;
384            };
385            km.table.retain(|_, &mut a| a != action);
386            km.table.insert(binding, action);
387        }
388        km
389    }
390
391    /// Look up the action bound to a key chord, if any.
392    pub fn lookup(&self, binding: Binding) -> Option<Action> {
393        self.table.get(&binding).copied()
394    }
395}
396
397impl Default for Keymap {
398    fn default() -> Self {
399        Self::default_table()
400    }
401}
402
403/// Parse a human-readable chord like `"Ctrl+Shift+K"` or `"F5"` into a
404/// [`Binding`]. Modifier names are case-insensitive (`ctrl`, `Ctrl`,
405/// `CTRL`); the key name uses winit's `KeyCode` Debug spelling minus
406/// the `Key` prefix for letters (`K` → `KeyK`).
407fn parse_chord(s: &str) -> Option<Binding> {
408    let mut mods = Modifiers::NONE;
409    let mut last: Option<&str> = None;
410    for part in s.split('+').map(str::trim).filter(|p| !p.is_empty()) {
411        match part.to_ascii_lowercase().as_str() {
412            "ctrl" | "control" => mods.ctrl = true,
413            "shift" => mods.shift = true,
414            "alt" | "option" => mods.alt = true,
415            _ => {
416                if last.is_some() {
417                    return None; // two non-modifier tokens
418                }
419                last = Some(part);
420            }
421        }
422    }
423    let key = parse_keycode(last?)?;
424    Some(Binding { key, mods })
425}
426
427fn parse_keycode(s: &str) -> Option<KeyCode> {
428    // Cover the names we use in the default table. Anything else falls
429    // through to `None` and the override is skipped with a warning.
430    let s_upper = s.to_ascii_uppercase();
431    Some(match s_upper.as_str() {
432        "SPACE" => KeyCode::Space,
433        "ENTER" | "RETURN" => KeyCode::Enter,
434        "TAB" => KeyCode::Tab,
435        "ESCAPE" | "ESC" => KeyCode::Escape,
436        "BACKSPACE" => KeyCode::Backspace,
437        "DELETE" | "DEL" => KeyCode::Delete,
438        "INSERT" | "INS" => KeyCode::Insert,
439        "SCROLLLOCK" | "SCROLL_LOCK" | "SCRLK" => KeyCode::ScrollLock,
440        "PAUSE" => KeyCode::Pause,
441        "LEFT" | "ARROWLEFT" => KeyCode::ArrowLeft,
442        "RIGHT" | "ARROWRIGHT" => KeyCode::ArrowRight,
443        "UP" | "ARROWUP" => KeyCode::ArrowUp,
444        "DOWN" | "ARROWDOWN" => KeyCode::ArrowDown,
445        "EQUAL" | "EQUALS" | "PLUS" | "=" => KeyCode::Equal,
446        "MINUS" | "-" => KeyCode::Minus,
447        "BRACKETLEFT" | "LBRACKET" | "[" => KeyCode::BracketLeft,
448        "BRACKETRIGHT" | "RBRACKET" | "]" => KeyCode::BracketRight,
449        "COMMA" | "," => KeyCode::Comma,
450        "PERIOD" | "." => KeyCode::Period,
451        "SEMICOLON" | ";" => KeyCode::Semicolon,
452        "SLASH" | "/" => KeyCode::Slash,
453        "BACKSLASH" | "\\" => KeyCode::Backslash,
454        "F1" => KeyCode::F1,
455        "F2" => KeyCode::F2,
456        "F3" => KeyCode::F3,
457        "F4" => KeyCode::F4,
458        "F5" => KeyCode::F5,
459        "F6" => KeyCode::F6,
460        "F7" => KeyCode::F7,
461        "F8" => KeyCode::F8,
462        "F9" => KeyCode::F9,
463        "F10" => KeyCode::F10,
464        "F11" => KeyCode::F11,
465        "F12" => KeyCode::F12,
466        other => {
467            // Single-letter `A`..`Z` → `KeyA`..`KeyZ`.
468            if other.len() == 1 {
469                let c = other.chars().next().unwrap();
470                if c.is_ascii_alphabetic() {
471                    return letter_to_keycode(c.to_ascii_uppercase());
472                }
473                if c.is_ascii_digit() {
474                    return digit_to_keycode(c);
475                }
476            }
477            return None;
478        }
479    })
480}
481
482fn letter_to_keycode(c: char) -> Option<KeyCode> {
483    Some(match c {
484        'A' => KeyCode::KeyA,
485        'B' => KeyCode::KeyB,
486        'C' => KeyCode::KeyC,
487        'D' => KeyCode::KeyD,
488        'E' => KeyCode::KeyE,
489        'F' => KeyCode::KeyF,
490        'G' => KeyCode::KeyG,
491        'H' => KeyCode::KeyH,
492        'I' => KeyCode::KeyI,
493        'J' => KeyCode::KeyJ,
494        'K' => KeyCode::KeyK,
495        'L' => KeyCode::KeyL,
496        'M' => KeyCode::KeyM,
497        'N' => KeyCode::KeyN,
498        'O' => KeyCode::KeyO,
499        'P' => KeyCode::KeyP,
500        'Q' => KeyCode::KeyQ,
501        'R' => KeyCode::KeyR,
502        'S' => KeyCode::KeyS,
503        'T' => KeyCode::KeyT,
504        'U' => KeyCode::KeyU,
505        'V' => KeyCode::KeyV,
506        'W' => KeyCode::KeyW,
507        'X' => KeyCode::KeyX,
508        'Y' => KeyCode::KeyY,
509        'Z' => KeyCode::KeyZ,
510        _ => return None,
511    })
512}
513
514fn digit_to_keycode(c: char) -> Option<KeyCode> {
515    Some(match c {
516        '0' => KeyCode::Digit0,
517        '1' => KeyCode::Digit1,
518        '2' => KeyCode::Digit2,
519        '3' => KeyCode::Digit3,
520        '4' => KeyCode::Digit4,
521        '5' => KeyCode::Digit5,
522        '6' => KeyCode::Digit6,
523        '7' => KeyCode::Digit7,
524        '8' => KeyCode::Digit8,
525        '9' => KeyCode::Digit9,
526        _ => return None,
527    })
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    #[test]
535    fn default_table_covers_md2_essentials() {
536        let km = Keymap::default_table();
537        assert_eq!(
538            km.lookup(Binding::bare(KeyCode::Space)),
539            Some(Action::NextPreset)
540        );
541        assert_eq!(
542            km.lookup(Binding::bare(KeyCode::Backspace)),
543            Some(Action::PreviousPreset)
544        );
545        assert_eq!(
546            km.lookup(Binding::bare(KeyCode::ScrollLock)),
547            Some(Action::TogglePresetLock)
548        );
549        assert_eq!(
550            km.lookup(Binding::new(KeyCode::KeyT, Modifiers::CTRL)),
551            Some(Action::ClearAllOverlays)
552        );
553        assert_eq!(
554            km.lookup(Binding::bare(KeyCode::Equal)),
555            Some(Action::RateUp)
556        );
557    }
558
559    #[test]
560    fn shift_variant_distinct_from_bare() {
561        let km = Keymap::default_table();
562        assert_eq!(
563            km.lookup(Binding::bare(KeyCode::KeyK)),
564            Some(Action::SpriteCycle)
565        );
566        assert_eq!(
567            km.lookup(Binding::new(KeyCode::KeyK, Modifiers::SHIFT)),
568            Some(Action::SpriteRandom)
569        );
570    }
571
572    #[test]
573    fn parse_chord_handles_modifiers_and_letters() {
574        let b = parse_chord("Ctrl+Shift+K").unwrap();
575        assert_eq!(b.key, KeyCode::KeyK);
576        assert!(b.mods.ctrl && b.mods.shift && !b.mods.alt);
577        let b = parse_chord("F5").unwrap();
578        assert_eq!(b.key, KeyCode::F5);
579        assert_eq!(b.mods, Modifiers::NONE);
580        assert!(parse_chord("Bogus+Bogus+K").is_none());
581        assert!(parse_chord("Ctrl+Shift").is_none());
582    }
583
584    #[test]
585    fn settings_override_replaces_default_binding() {
586        let mut s = KeymapSettings::default();
587        s.bindings.insert(
588            Action::TogglePresetLock.as_str().to_string(),
589            "Ctrl+L".into(),
590        );
591        let km = Keymap::from_settings(&s);
592        // ScrollLock should no longer point at TogglePresetLock (the
593        // override stripped any prior binding for that action).
594        assert_ne!(
595            km.lookup(Binding::bare(KeyCode::ScrollLock)),
596            Some(Action::TogglePresetLock)
597        );
598        assert_eq!(
599            km.lookup(Binding::new(KeyCode::KeyL, Modifiers::CTRL)),
600            Some(Action::TogglePresetLock)
601        );
602    }
603}