aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-24 02:47:07 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-24 02:47:07 +0300
commit520d0b8b9c1b9d621274411d5fc26e35df9d60d2 (patch)
treef40e2a70743388073290a7c157f35bab4eb9bcde /src/pane
parent74fa90cecc6f6dfd93dc64893659771001ad2d82 (diff)
fix: prevent release key fallback from doubling input
refs #769
Diffstat (limited to 'src/pane')
-rw-r--r--src/pane/terminal.rs77
1 files changed, 76 insertions, 1 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index ed5d1061..e573f958 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -1019,7 +1019,11 @@ impl GhosttyPaneTerminal {
return crate::input::encode_terminal_key(key, protocol);
};
match encoder.encode(&event) {
- Ok(bytes) if !bytes.is_empty() => bytes,
+ Ok(bytes)
+ if !bytes.is_empty() && encoded_key_preserves_event_kind(&bytes, key, protocol) =>
+ {
+ bytes
+ }
Ok(_) | Err(_) => crate::input::encode_terminal_key(key, protocol),
}
}
@@ -1291,6 +1295,23 @@ impl GhosttyPaneTerminal {
}
}
+fn encoded_key_preserves_event_kind(
+ bytes: &[u8],
+ key: crate::input::TerminalKey,
+ protocol: crate::input::KeyboardProtocol,
+) -> bool {
+ if !protocol.reports_event_types() || key.kind == crossterm::event::KeyEventKind::Press {
+ return true;
+ }
+
+ std::str::from_utf8(bytes)
+ .ok()
+ .and_then(crate::input::parse_terminal_key_sequence)
+ .is_some_and(|parsed| {
+ parsed.code == key.code && parsed.modifiers == key.modifiers && parsed.kind == key.kind
+ })
+}
+
fn cursor_position_settle_pending(core: &GhosttyPaneCore) -> bool {
core.cursor_settle_state.pending()
}
@@ -2433,6 +2454,60 @@ mod tests {
}
#[test]
+ fn ghostty_enter_backspace_release_in_legacy_pane_emits_nothing() {
+ let (tx, _rx) = mpsc::channel(4);
+ let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ for code in [
+ crossterm::event::KeyCode::Enter,
+ crossterm::event::KeyCode::Backspace,
+ ] {
+ let press = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty()),
+ crate::input::KeyboardProtocol::Legacy,
+ );
+ let release = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty())
+ .with_kind(crossterm::event::KeyEventKind::Release),
+ crate::input::KeyboardProtocol::Legacy,
+ );
+ assert!(!press.is_empty(), "{code:?} press should emit bytes");
+ assert!(
+ release.is_empty(),
+ "{code:?} release should emit nothing in a legacy pane, got {release:?}"
+ );
+ }
+ }
+
+ #[test]
+ fn ghostty_release_still_encoded_for_report_event_pane() {
+ let (tx, _rx) = mpsc::channel(4);
+ // Push kitty flags including REPORT_EVENT_TYPES (0b10) + DISAMBIGUATE (0b1).
+ let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ terminal.write(b"\x1b[>3u");
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ let release = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(
+ crossterm::event::KeyCode::Enter,
+ crossterm::event::KeyModifiers::empty(),
+ )
+ .with_kind(crossterm::event::KeyEventKind::Release),
+ pane.keyboard_protocol().unwrap(),
+ );
+ let parsed =
+ crate::input::parse_terminal_key_sequence(std::str::from_utf8(&release).unwrap())
+ .unwrap();
+ assert_eq!(parsed.code, crossterm::event::KeyCode::Enter);
+ assert_eq!(
+ parsed.kind,
+ crossterm::event::KeyEventKind::Release,
+ "report-event pane should encode a release, got {release:?}"
+ );
+ }
+
+ #[test]
fn ghostty_char_keys_still_use_herdr_encoding() {
let (tx, _rx) = mpsc::channel(4);
let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();