fix: send shifted punctuation as text in kitty mode

refs #1105
This commit is contained in:
Ogulcan Celik 2026-07-07 16:24:26 +03:00
parent 39ba6b7a02
commit d190c1f55e
3 changed files with 110 additions and 0 deletions

View File

@ -18,6 +18,7 @@
- Bumped the client/server protocol version to 15 for socket API placement mutation event and response compatibility.
### Fixed
- Windows clients now send shifted punctuation such as `!`, `?`, and `:` as literal text to Kitty-keyboard-mode pane apps, fixing Kiro CLI TUI prompts while preserving modified key chords. (#1105)
- `herdr --remote` now prints clean remote attach failures and SSH authentication guidance instead of Rust Debug-formatted I/O errors when SSH authentication is denied. (#1034)
- `herdr server stop` now stops Windows named-pipe servers instead of failing with `named pipes do not support I/O timeouts`. (#1113)
- `herdr server stop` now waits until both server sockets are unreachable before returning, avoiding an immediate first-start failure when restarting right after replacing the binary.

View File

@ -2012,6 +2012,35 @@ mod tests {
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn api_pane_send_keys_sends_shifted_punctuation_as_text_in_kitty_mode() {
let (mut app, pane_id) = app_with_test_workspace();
let internal_pane_id = app.state.workspaces[0].tabs[0].root_pane;
let (runtime, mut rx) =
crate::terminal::TerminalRuntime::test_with_channel_and_scrollback_bytes(
80,
24,
0,
b"\x1b[>7u",
1,
);
app.state.insert_test_runtime(internal_pane_id, runtime);
let response = app.handle_api_request(crate::api::schema::Request {
id: "req".into(),
method: crate::api::schema::Method::PaneSendKeys(PaneSendKeysParams {
pane_id,
keys: vec!["shift+?".into()],
}),
});
let success: SuccessResponse = serde_json::from_str(&response).unwrap();
assert_eq!(success.id, "req");
assert_eq!(success.result, ResponseResult::Ok {});
assert_eq!(rx.try_recv().unwrap(), bytes::Bytes::from_static(b"?"));
assert!(rx.try_recv().is_err());
}
#[tokio::test]
async fn api_pane_send_input_keys_accept_key_combo_chords() {
let (mut app, pane_id, mut rx) = app_with_send_key_runtime(1);

View File

@ -379,9 +379,39 @@ fn shifted_text_char(key: &TerminalKey, ch: char) -> Option<char> {
return Some(ch.to_ascii_uppercase());
}
if is_shifted_ascii_punctuation(ch) {
return Some(ch);
}
None
}
fn is_shifted_ascii_punctuation(ch: char) -> bool {
matches!(
ch,
'!' | '@'
| '#'
| '$'
| '%'
| '^'
| '&'
| '*'
| '('
| ')'
| '_'
| '+'
| '{'
| '}'
| '|'
| ':'
| '"'
| '<'
| '>'
| '?'
| '~'
)
}
fn canonical_kitty_char(ch: char, mods: KeyModifiers) -> char {
if mods.contains(KeyModifiers::SHIFT) && ch.is_ascii_uppercase() {
ch.to_ascii_lowercase()
@ -848,6 +878,56 @@ mod tests {
assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 7 }), b"");
}
#[test]
fn kitty_shifted_punctuation_literals_send_text() {
for ch in "!@#$%^&*()_+{}|:\"<>?~".chars() {
let key = TerminalKey::new(KeyCode::Char(ch), KeyModifiers::SHIFT);
let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
assert_eq!(encoded, ch.to_string().into_bytes(), "ch={ch}");
}
}
#[test]
fn kitty_shifted_punctuation_release_does_not_emit_text() {
let key = TerminalKey::new(KeyCode::Char('?'), KeyModifiers::SHIFT)
.with_kind(crossterm::event::KeyEventKind::Release);
assert_eq!(
encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
b""
);
}
#[test]
fn kitty_shifted_punctuation_does_not_infer_layout() {
let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT);
assert_eq!(
encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }),
b"\x1b[49;2:1u"
);
}
#[test]
fn kitty_modified_shifted_punctuation_stays_modified_key() {
for (modifiers, expected) in [
(
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
b"\x1b[33;6:1u".as_slice(),
),
(
KeyModifiers::ALT | KeyModifiers::SHIFT,
b"\x1b[33;4:1u".as_slice(),
),
(
KeyModifiers::SUPER | KeyModifiers::SHIFT,
b"\x1b[33;10:1u".as_slice(),
),
] {
let key = TerminalKey::new(KeyCode::Char('!'), modifiers);
let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 });
assert_eq!(encoded, expected, "modifiers={modifiers:?}");
}
}
#[test]
fn release_bytes_gated_on_report_event_types() {
for code in [KeyCode::Enter, KeyCode::Backspace] {