//! Every key Lumbridge claims, and the rule for which keys it may claim. //! //! A terminal multiplexer is a program whose main job is to *not* intercept //! keys. Everything it takes is taken from the shell, the editor, and the //! reader running inside it. So the bindings live in one table with a test that //! enforces the rule, rather than being scattered through the shell where the //! next one gets added without anyone checking what it costs. //! //! **The rule: no binding may be a bare control character or a bare Meta //! sequence.** Those are what a terminal application actually receives. //! //! - `ctrl-` is a C0 control byte. `ctrl-k` is kill-line, `ctrl-a` //! start-of-line, `ctrl-r` reverse search, `ctrl-c` interrupt. Binding one at //! the window level makes it unreachable in every pane, because GPUI stops //! dispatching once a binding claims the event — `finish_dispatch_key_event` //! never runs, so there is no fallthrough to the terminal. //! - `alt-` is an ESC-prefixed Meta sequence. `alt-f`/`alt-b` are word //! motion, `alt-1` is readline's digit argument, and `alt-` is word //! motion in most terminals. tmux and zellij leave these alone too. //! //! What is left is `secondary-alt-…` (ctrl+alt on Linux, cmd+alt on macOS) and //! `secondary-shift-…`, neither of which a terminal program can receive as a //! distinct sequence anyway. //! //! A leader chord — tmux's `ctrl-b` — was considered and rejected: GPUI parks a //! pending chord prefix for a full second and discards it if focus moves, so the //! second key of a chord is unreliable in a multi-pane window. use gpui::KeyBinding; use crate::{ AddPanel, DetachSelectedPanel, FocusDown, FocusLeft, FocusRight, FocusSidebar, FocusUp, JumpToAttention, OpenPalette, PasteIntoPane, RestartPane, SelectPane1, SelectPane2, SelectPane3, SelectPane4, SelectPane5, SelectPane6, TerminalNarrower, TerminalShorter, TerminalTaller, TerminalWider, TerminatePane, ToggleSidebar, }; /// The context every binding is scoped to. const CONTEXT: &str = "LumbridgeShell"; /// Every keystroke Lumbridge claims, as written for `KeyBinding::new`. /// /// Kept as strings so the test below can inspect them. A binding that is not in /// this table does not exist. pub(crate) const BINDINGS: &[&str] = &[ "secondary-shift-p", "secondary-alt-left", "secondary-alt-right", "secondary-alt-up", "secondary-alt-down", "secondary-alt-1", "secondary-alt-2", "secondary-alt-3", "secondary-alt-4", "secondary-alt-5", "secondary-alt-6", "secondary-alt-n", "secondary-alt-w", "secondary-alt-shift-up", "secondary-alt-shift-down", "secondary-alt-shift-right", "secondary-alt-shift-left", "secondary-alt-r", "secondary-alt-x", // The terminal convention, and the only safe spelling: `secondary-v` is // ctrl-v on Linux, which readline reads as quoted-insert. "secondary-shift-v", "secondary-alt-b", "secondary-alt-s", "secondary-alt-a", ]; /// Builds the bindings in the same order as [`BINDINGS`]. pub(crate) fn bindings() -> Vec { vec![ // Was `secondary-k`, which on Linux is ctrl-k — readline's kill-line, // unreachable in every pane for as long as that binding existed. KeyBinding::new(BINDINGS[0], OpenPalette, Some(CONTEXT)), // Was `alt-`: word motion in most terminals. KeyBinding::new(BINDINGS[1], FocusLeft, Some(CONTEXT)), KeyBinding::new(BINDINGS[2], FocusRight, Some(CONTEXT)), KeyBinding::new(BINDINGS[3], FocusUp, Some(CONTEXT)), KeyBinding::new(BINDINGS[4], FocusDown, Some(CONTEXT)), // Was `alt-1..6`: readline reads those as a digit argument. KeyBinding::new(BINDINGS[5], SelectPane1, Some(CONTEXT)), KeyBinding::new(BINDINGS[6], SelectPane2, Some(CONTEXT)), KeyBinding::new(BINDINGS[7], SelectPane3, Some(CONTEXT)), KeyBinding::new(BINDINGS[8], SelectPane4, Some(CONTEXT)), KeyBinding::new(BINDINGS[9], SelectPane5, Some(CONTEXT)), KeyBinding::new(BINDINGS[10], SelectPane6, Some(CONTEXT)), KeyBinding::new(BINDINGS[11], AddPanel, Some(CONTEXT)), KeyBinding::new(BINDINGS[12], DetachSelectedPanel, Some(CONTEXT)), KeyBinding::new(BINDINGS[13], TerminalTaller, Some(CONTEXT)), KeyBinding::new(BINDINGS[14], TerminalShorter, Some(CONTEXT)), KeyBinding::new(BINDINGS[15], TerminalWider, Some(CONTEXT)), KeyBinding::new(BINDINGS[16], TerminalNarrower, Some(CONTEXT)), KeyBinding::new(BINDINGS[17], RestartPane, Some(CONTEXT)), KeyBinding::new(BINDINGS[18], TerminatePane, Some(CONTEXT)), KeyBinding::new(BINDINGS[19], PasteIntoPane, Some(CONTEXT)), KeyBinding::new(BINDINGS[20], ToggleSidebar, Some(CONTEXT)), KeyBinding::new(BINDINGS[21], FocusSidebar, Some(CONTEXT)), KeyBinding::new(BINDINGS[22], JumpToAttention, Some(CONTEXT)), ] } /// Splits a binding into its modifiers and its key. #[cfg(test)] fn parts(binding: &str) -> (Vec<&str>, &str) { let mut segments: Vec<&str> = binding.split('-').collect(); // A trailing empty segment means the key itself is `-`. let key = if segments.last() == Some(&"") { segments.pop(); segments.pop(); "-" } else { segments.pop().unwrap_or("") }; (segments, key) } #[cfg(test)] mod tests { use super::{BINDINGS, bindings, parts}; #[test] fn no_binding_shadows_a_control_character() { // `secondary` is ctrl on Linux. secondary+letter with no other modifier // is a C0 control byte the terminal needs: ctrl-k kill-line, ctrl-a // start-of-line, ctrl-r reverse-search, ctrl-w kill-word, ctrl-c. for binding in BINDINGS { let (modifiers, key) = parts(binding); let bare_secondary = modifiers == ["secondary"]; assert!( !(bare_secondary && key.len() == 1 && key.chars().all(|c| c.is_ascii_alphabetic())), "{binding} shadows a control character the terminal needs" ); } } #[test] fn no_binding_shadows_a_meta_sequence() { // A bare alt+key is ESC-prefixed Meta: readline word motion, digit // arguments, and vim's own Meta bindings all arrive this way. for binding in BINDINGS { let (modifiers, _) = parts(binding); assert_ne!( modifiers, ["alt"], "{binding} shadows an ESC-prefixed Meta sequence" ); } } #[test] fn no_binding_claims_a_key_a_terminal_cannot_live_without() { // Even with modifiers, these must never be claimed at window level. for binding in BINDINGS { let (_, key) = parts(binding); assert!( !matches!(key, "escape" | "tab" | "enter" | "backspace"), "{binding} claims {key}, which every terminal application needs" ); } } #[test] fn every_binding_is_unique() { let mut seen = Vec::new(); for binding in BINDINGS { assert!(!seen.contains(binding), "{binding} is bound twice"); seen.push(binding); } } #[test] fn the_table_and_the_builder_stay_in_step() { assert_eq!( bindings().len(), BINDINGS.len(), "every entry in BINDINGS must build exactly one binding" ); } #[test] fn parts_splits_modifiers_from_the_key() { assert_eq!( parts("secondary-shift-p"), (vec!["secondary", "shift"], "p") ); assert_eq!(parts("alt-left"), (vec!["alt"], "left")); assert_eq!(parts("escape"), (Vec::new(), "escape")); } }