Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Fixed
- macOS `herdr --remote` clients now keep the accepted bridge socket blocking, preventing an immediate disconnect after the protocol handshake. (#2478, thanks @mathijshenquet)
- Prefix keybindings now preserve Shift in WezTerm Kitty keyboard mode, so commands such as config reload no longer trigger their unshifted action. (#2435)
- Stable direct installs, self-updates, and remote helper downloads now require and verify the SHA-256 digest published for each GitHub release asset.
- Configs containing the retired Herdr-written `ui.agent_panel_scope` setting no longer report it as an unknown key after upgrades. (#2292)
- Claude Code confirmation prompts using `Enter to confirm · Esc to cancel` now report `blocked` instead of `idle`. (#2268)
Expand Down
36 changes: 35 additions & 1 deletion src/app/input/navigate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,12 @@ mod tests {
use super::super::{state_with_workspaces, unique_temp_path};
use super::*;
use crate::{
app::App, config::Config, input::TerminalKey, terminal::TerminalState, workspace::Workspace,
app::App,
config::Config,
input::TerminalKey,
raw_input::{parse_raw_input_bytes_sync, RawInputEvent},
terminal::TerminalState,
workspace::Workspace,
};

fn mark_worktree_space_member(state: &mut AppState, ws_idx: usize, key: &str) {
Expand Down Expand Up @@ -2840,6 +2845,35 @@ command = "printf literal > '{}'"
assert_eq!(app.state.mode, Mode::RenameWorkspace);
}

#[tokio::test]
async fn kitty_shifted_alternate_without_modifier_prefers_reload_over_resize() {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
let mut app = App::new(
&Config::default(),
true,
None,
api_rx,
crate::api::EventHub::default(),
);
app.state.workspaces = vec![Workspace::test_new("test")];
app.state.active = Some(0);
app.state.selected = 0;
app.state.mode = Mode::Prefix;

let mut events = parse_raw_input_bytes_sync(b"\x1b[114:82;1u");
assert_eq!(events.len(), 1);
let RawInputEvent::Key(key) = events.remove(0) else {
panic!("expected key event");
};
assert_eq!(
action_for_key(&app.state, key.clone(), BindingDispatch::Prefix),
Some(NavigateAction::ReloadConfig)
);
app.handle_prefix_key(key);

assert_eq!(app.state.mode, Mode::Terminal);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[tokio::test]
async fn legacy_uppercase_prefers_shifted_reload_binding_over_unshifted() {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
Expand Down
35 changes: 34 additions & 1 deletion src/input/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,17 @@ fn parse_kitty_key_sequence(data: &str) -> Option<TerminalKey> {

let code = kitty_codepoint_to_keycode(codepoint)?;
let kind = parse_kitty_event_type(event_type)?;
let mut modifiers = key_modifiers_from_u8(modifier);
// Kitty permits the shifted alternate only while Shift is active. Normalize
// contradictory reports here so they cannot dispatch an unshifted command.
if matches!(code, KeyCode::Char(_))
&& shifted_codepoint
.is_some_and(|shifted| shifted != codepoint && char::from_u32(shifted).is_some())
{
modifiers |= KeyModifiers::SHIFT;
}

let mut key = TerminalKey::new(code, key_modifiers_from_u8(modifier)).with_kind(kind);
let mut key = TerminalKey::new(code, modifiers).with_kind(kind);
if let Some(shifted_codepoint) = shifted_codepoint {
key = key.with_shifted_codepoint(shifted_codepoint);
}
Expand Down Expand Up @@ -598,6 +607,30 @@ mod tests {
assert_eq!(key.shifted_codepoint, Some('L' as u32));
}

#[test]
fn parse_kitty_sequence_recovers_omitted_shift_modifier() {
for (sequence, kind) in [
("\x1b[114:82;1u", crossterm::event::KeyEventKind::Press),
("\x1b[114:82;1:2u", crossterm::event::KeyEventKind::Repeat),
("\x1b[114:82;1:3u", crossterm::event::KeyEventKind::Release),
] {
let key = parse_terminal_key_sequence(sequence).unwrap();
assert_eq!(key.code, KeyCode::Char('r'));
assert_eq!(key.modifiers, KeyModifiers::SHIFT);
assert_eq!(key.kind, kind);
assert_eq!(key.shifted_codepoint, Some('R' as u32));
}
}

#[test]
fn parse_kitty_sequence_does_not_infer_shift_without_distinct_shifted_alternate() {
for sequence in ["\x1b[114;1u", "\x1b[114:114;1u", "\x1b[114::113;1u"] {
let key = parse_terminal_key_sequence(sequence).unwrap();
assert_eq!(key.code, KeyCode::Char('r'));
assert_eq!(key.modifiers, KeyModifiers::empty());
}
}

#[test]
fn parse_kitty_sequence_preserves_non_us_shift_pairs() {
for (sequence, base, shifted) in [
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/keyboard_protocol_corpus.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ modify_other_keys_ctrl_shift_l 1b5b32373b363b3130387e char:l control+shift press
kitty_alt_backspace 1b5b3132373b3375 backspace alt press
kitty_shift_letter 1b5b3130383a37363b323a3175 char:l shift press 76
kitty_shift_symbol 1b5b34393a33333b323a3175 char:1 shift press 33
kitty_omitted_shift_modifier 1b5b3131343a38323b3175 char:r shift press 82
kitty_release_letter 1b5b3130383a37363b323a3375 char:l shift release 76
kitty_keypad_0 1b5b35373339393b3175 char:0 - press
kitty_keypad_1 1b5b35373430303b3175 char:1 - press
Expand Down
Loading