mirror of
https://github.com/herdrdev/herdr.git
synced 2026-11-04 08:06:09 +00:00
@@ -10,6 +10,19 @@ fn is_retained_selection_copy_key(key: &crate::input::TerminalKey) -> bool {
|
||||
&& matches!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
}
|
||||
|
||||
/// True for Ctrl+[, the terminal-level equivalent of Esc.
|
||||
///
|
||||
/// A legacy terminal sends Ctrl+[ as 0x1b, the same byte as Esc, so it already
|
||||
/// arrives as `KeyCode::Esc`. Under the kitty keyboard protocol the modified key
|
||||
/// is reported on its own and reaches navigate mode as `Char('[')` with CONTROL.
|
||||
/// Ctrl+Shift+[ stays distinct because it carries SHIFT.
|
||||
///
|
||||
/// Navigate mode checks this only after keybinding dispatch, so a configured
|
||||
/// Ctrl+[ binding keeps working and this stays a fallback cancel.
|
||||
fn is_ctrl_bracket_key(key: &crate::input::TerminalKey) -> bool {
|
||||
key.code == KeyCode::Char('[') && key.modifiers == KeyModifiers::CONTROL
|
||||
}
|
||||
|
||||
pub(super) fn is_modal_paste_shortcut_for_platform(
|
||||
key: &crate::input::TerminalKey,
|
||||
macos: bool,
|
||||
@@ -650,9 +663,7 @@ impl ClientShellState {
|
||||
|
||||
self.pending_workspace_highlight = None;
|
||||
if key.code == KeyCode::Esc || self.config.keybinds.matches_prefix(key) {
|
||||
self.mode = self.copy_or_terminal_mode();
|
||||
self.navigate_workspace_id = None;
|
||||
outcome.repaint = true;
|
||||
self.cancel_navigate(outcome);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -687,6 +698,12 @@ impl ClientShellState {
|
||||
return;
|
||||
}
|
||||
if self.workspace_preview_action_blocked() {
|
||||
// Bindings cannot run against a foreign preview, so Ctrl+[ has
|
||||
// nothing to lose to and cancels here the same way Esc does.
|
||||
if is_ctrl_bracket_key(key) {
|
||||
self.cancel_navigate(outcome);
|
||||
return;
|
||||
}
|
||||
self.push_endpoint_notice(
|
||||
ClientEndpointNoticeKind::Rejected,
|
||||
"navigate_endpoint_inactive",
|
||||
@@ -816,9 +833,17 @@ impl ClientShellState {
|
||||
});
|
||||
if let Some(binding) = binding {
|
||||
self.record_navigate_binding(binding, false, outcome);
|
||||
} else if is_ctrl_bracket_key(key) {
|
||||
self.cancel_navigate(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_navigate(&mut self, outcome: &mut ClientShellInput) {
|
||||
self.mode = self.copy_or_terminal_mode();
|
||||
self.navigate_workspace_id = None;
|
||||
outcome.repaint = true;
|
||||
}
|
||||
|
||||
fn record_navigate_binding(
|
||||
&mut self,
|
||||
binding: crate::input::KeybindMatch,
|
||||
|
||||
@@ -968,3 +968,61 @@ fn navigation_highlight_ends_for_noop_focus_and_focused_creation() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kitty keyboard protocol (CSI u) encodings of `[` (codepoint 91).
|
||||
const KITTY_CTRL_BRACKET: &[u8] = b"\x1b[91;5u";
|
||||
const KITTY_CTRL_SHIFT_BRACKET: &[u8] = b"\x1b[91;6u";
|
||||
|
||||
#[test]
|
||||
fn kitty_ctrl_bracket_leaves_navigation_like_esc() {
|
||||
let mut state = local_navigation_state(false);
|
||||
enter_navigation(&mut state);
|
||||
preview_key(&mut state, b"\x1b[B");
|
||||
|
||||
preview_key(&mut state, KITTY_CTRL_BRACKET);
|
||||
|
||||
assert_eq!(state.mode, ClientShellMode::Terminal);
|
||||
assert!(state.navigate_workspace_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_ctrl_bracket_navigate_binding_wins_over_cancel() {
|
||||
let config: Config = toml::from_str("[keys]\nnavigate_workspace_down = \"ctrl+[\"\n").unwrap();
|
||||
let mut state = ClientShellState::new(ClientShellConfig::from_config(&config));
|
||||
state.set_snapshot(Box::new(workspaces(3)));
|
||||
state.set_pane_surface(surface());
|
||||
state.compose(100, 28).unwrap();
|
||||
enter_navigation(&mut state);
|
||||
assert_selected(&state, &ClientEndpointId::Local, "ws_1");
|
||||
|
||||
preview_key(&mut state, KITTY_CTRL_BRACKET);
|
||||
|
||||
assert_eq!(state.mode, ClientShellMode::Navigate);
|
||||
assert_selected(&state, &ClientEndpointId::Local, "ws_2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_ctrl_shift_bracket_keeps_navigation_open() {
|
||||
let mut state = local_navigation_state(false);
|
||||
enter_navigation(&mut state);
|
||||
|
||||
state.handle_input_bytes(KITTY_CTRL_SHIFT_BRACKET);
|
||||
|
||||
assert_eq!(state.mode, ClientShellMode::Navigate);
|
||||
assert_selected(&state, &ClientEndpointId::Local, "ws_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_ctrl_bracket_cancels_a_blocked_foreign_preview() {
|
||||
let (mut state, remote) = state_with_remote();
|
||||
state.compose(100, 28).unwrap();
|
||||
enter_navigation(&mut state);
|
||||
preview_key(&mut state, b"\x1b[B");
|
||||
assert_selected(&state, &remote, "ws_1");
|
||||
assert!(state.workspace_preview_action_blocked());
|
||||
|
||||
preview_key(&mut state, KITTY_CTRL_BRACKET);
|
||||
|
||||
assert_ne!(state.mode, ClientShellMode::Navigate);
|
||||
assert!(state.navigate_workspace_id.is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user