From 7f89b11a11160874ba89f3be7c64b29960bd2df3 Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Mon, 28 Sep 2026 11:58:09 +0200 Subject: feat: support multiple prefix keys (#4653) * feat: support multiple prefix keys keys.prefix accepts an array in addition to a single string so one config can define several prefix keys. Every configured prefix enters the same prefix mode, and the help panel lists them all. Published keybinding profiles keep the primary prefix as a scalar and carry the rest in an optional extra_prefixes field so older clients keep working with the primary prefix. * fix: reject an empty prefix list on reload An explicitly empty keys.prefix array now stays empty through config parsing so validation rejects it and a reload keeps the current keybindings instead of silently falling back to ctrl+b. --- src/client/shell/tests/copy.rs | 4 ++-- src/client/shell/tests/input.rs | 25 +++++++++++++++++++++++++ src/client/shell/tests/keybindings_settings.rs | 4 ++-- 3 files changed, 29 insertions(+), 4 deletions(-) (limited to 'src/client/shell/tests') diff --git a/src/client/shell/tests/copy.rs b/src/client/shell/tests/copy.rs index c0d528e1..317950cf 100644 --- a/src/client/shell/tests/copy.rs +++ b/src/client/shell/tests/copy.rs @@ -1856,7 +1856,7 @@ fn copy_mode_survives_mouse_motion_and_parks_across_focus_changes() { .as_ref() .is_some_and(|copy_mode| copy_mode.selection.is_some())); - let (prefix_key, prefix_modifiers) = state.config.keybinds.prefix; + let (prefix_key, prefix_modifiers) = state.config.keybinds.prefix[0]; state.handle_raw_events(vec![RawInputEvent::Key(crate::input::TerminalKey::new( prefix_key, prefix_modifiers, @@ -2038,7 +2038,7 @@ fn queued_copy_keys_preserve_prefix_order() { let origin = state.copy_mode.as_ref().expect("copy mode").cursor; let motion = state.handle_input_bytes(b"w"); state.handle_input_bytes(b"l"); - let (prefix_key, prefix_modifiers) = state.config.keybinds.prefix; + let (prefix_key, prefix_modifiers) = state.config.keybinds.prefix[0]; state.handle_raw_events(vec![RawInputEvent::Key(crate::input::TerminalKey::new( prefix_key, prefix_modifiers, diff --git a/src/client/shell/tests/input.rs b/src/client/shell/tests/input.rs index b54548f2..145c7ae6 100644 --- a/src/client/shell/tests/input.rs +++ b/src/client/shell/tests/input.rs @@ -650,3 +650,28 @@ fn styled_client_composition_preserves_pane_hyperlinks() { let link = frame.cells[index].hyperlink.expect("linked cell") as usize; assert_eq!(frame.hyperlinks[link], "https://example.test"); } + +#[test] +fn every_configured_prefix_enters_prefix_mode() { + let mut config = Config::default(); + config.keys.prefix = + crate::config::BindingConfig::Many(vec!["ctrl+space".to_owned(), "ctrl+s".to_owned()]); + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + state.set_snapshot(Box::new(snapshot())); + + for combo in [ + (KeyCode::Char(' '), KeyModifiers::CONTROL), + (KeyCode::Char('s'), KeyModifiers::CONTROL), + ] { + let _ = state.handle_raw_events(vec![RawInputEvent::Key(crate::input::TerminalKey::new( + combo.0, combo.1, + ))]); + assert_eq!(state.mode, ClientShellMode::Prefix); + + let _ = state.handle_raw_events(vec![RawInputEvent::Key(crate::input::TerminalKey::new( + KeyCode::Esc, + KeyModifiers::empty(), + ))]); + assert_eq!(state.mode, ClientShellMode::Terminal); + } +} diff --git a/src/client/shell/tests/keybindings_settings.rs b/src/client/shell/tests/keybindings_settings.rs index e1b43a01..dd60fe11 100644 --- a/src/client/shell/tests/keybindings_settings.rs +++ b/src/client/shell/tests/keybindings_settings.rs @@ -282,7 +282,7 @@ command = "local-only" .unwrap(); let remote_local = ClientShellConfig::from_config(&local) .with_keybinding_source(ClientShellKeybindingSource::RemoteLocal); - assert_eq!(remote_local.keybinds.prefix.0, KeyCode::Char('a')); + assert_eq!(remote_local.keybinds.prefix[0].0, KeyCode::Char('a')); assert!(remote_local.keybinds.keybinds.custom_commands.is_empty()); assert_eq!( remote_local.keybinds.keybinds.new_tab.label().as_deref(), @@ -377,7 +377,7 @@ new_tab = "prefix+n" }); state.set_snapshot(Box::new(projection)); - assert_eq!(state.config.keybinds.prefix.0, KeyCode::Char('x')); + assert_eq!(state.config.keybinds.prefix[0].0, KeyCode::Char('x')); assert_eq!( state.config.keybinds.keybinds.new_tab.label().as_deref(), Some("prefix+n") -- cgit v1.2.3