refactor: read live keyboard modes only from libghostty

This commit is contained in:
Ogulcan Celik
2026-10-06 17:21:09 +03:00
parent 53cfd06831
commit fc8ae00289
4 changed files with 44 additions and 37 deletions
+3 -1
View File
@@ -225,8 +225,10 @@ impl Oracle {
}
}
/// Panes that negotiated nothing. Like Ghostty, modifyOtherKeys level 1 counts
/// as nothing.
fn is_legacy_pane(pane_mode: &[u8]) -> bool {
matches!(pane_mode, b"" | b"\x1b[?1h")
matches!(pane_mode, b"" | b"\x1b[?1h" | b"\x1b[>4;1m")
}
/// Agreed exception table: panes that negotiated no keyboard protocol keep the
+5 -5
View File
@@ -1,3 +1,7 @@
/// Handoff-only record of the keyboard modes a pane's app requested, so an
/// update handoff can replay the full Kitty keyboard stack into the new server.
/// It is never consulted for live input: libghostty owns keyboard state and
/// encoding. Remove once handoff restores panes from libghostty snapshots.
#[derive(Debug, Clone, Default)]
pub(crate) struct KittyKeyboardTracker {
pending: Vec<u8>,
@@ -67,15 +71,11 @@ impl KittyKeyboardTracker {
}
}
#[cfg(test)]
pub(crate) fn modify_other_keys_level(&self) -> u8 {
self.modify_other_keys_level
}
#[cfg(windows)]
pub(crate) fn modify_other_keys_enabled(&self) -> bool {
self.modify_other_keys_level > 0
}
fn observe_modify_other_keys(&mut self, params: &[u8]) {
let Some(params) = params.strip_prefix(b">") else {
return;
+33 -29
View File
@@ -1855,10 +1855,16 @@ impl GhosttyPaneTerminal {
.is_ok_and(|core| core.terminal.mouse_tracking_enabled().unwrap_or(false))
}
/// libghostty tracks only level 2; like Ghostty, level 1 behaves as if the
/// app negotiated nothing.
pub fn modify_other_keys_level(&self) -> u8 {
self.core
.lock()
.map_or(0, |core| core.kitty_keyboard.modify_other_keys_level())
self.core.lock().map_or(0, |core| {
if core.terminal.modify_other_keys_enabled().unwrap_or(false) {
2
} else {
0
}
})
}
pub fn sgr_pixel_mouse_enabled(&self) -> bool {
@@ -2024,20 +2030,15 @@ impl GhosttyPaneTerminal {
pub fn encode_terminal_key(
&self,
key: crate::input::TerminalKey,
// Unused: libghostty reads the live modes. Removed with the shadow tracker.
// Unused: libghostty reads the live modes.
_protocol: crate::input::KeyboardProtocol,
) -> Vec<u8> {
#[cfg(windows)]
if self.core.lock().is_ok_and(|core| {
core.terminal
.kitty_keyboard_flags()
.is_ok_and(|flags| flags == 0)
&& !core.kitty_keyboard.modify_other_keys_enabled()
&& core
.terminal
.modify_other_keys_enabled()
.is_ok_and(|enabled| !enabled)
}) {
if self
.core
.lock()
.is_ok_and(|core| keyboard_negotiated_nothing(&core.terminal))
{
if let Some(bytes) = crate::platform::encode_windows_conpty_fallback(&key) {
return bytes;
}
@@ -2062,15 +2063,7 @@ impl GhosttyPaneTerminal {
let Ok(core) = self.core.lock() else {
return Vec::new();
};
let negotiated_nothing = core
.terminal
.kitty_keyboard_flags()
.is_ok_and(|flags| flags == 0)
&& core.kitty_keyboard.modify_other_keys_level() == 0
&& core
.terminal
.modify_other_keys_enabled()
.is_ok_and(|enabled| !enabled);
let negotiated_nothing = keyboard_negotiated_nothing(&core.terminal);
let key = if negotiated_nothing {
legacy_shell_key(key)
} else {
@@ -2507,6 +2500,17 @@ impl GhosttyPaneTerminal {
}
}
/// True when the pane's app asked for no enhanced keyboard protocol (no Kitty
/// flags, no modifyOtherKeys 2), read from libghostty's live state.
fn keyboard_negotiated_nothing(terminal: &crate::ghostty::Terminal) -> bool {
terminal
.kitty_keyboard_flags()
.is_ok_and(|flags| flags == 0)
&& terminal
.modify_other_keys_enabled()
.is_ok_and(|enabled| !enabled)
}
fn effective_cursor_state(
core: &mut GhosttyPaneCore,
current: Option<TerminalCursorState>,
@@ -4830,8 +4834,8 @@ mod tests {
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap();
let pane_id = PaneId::from_raw(1);
// Like Ghostty, modifyOtherKeys level 1 behaves as if nothing was negotiated.
let legacy = ["\r", "\r", "\r", "\x1b\r"];
let mode_one = ["\x1b[27;2;13~", "\x1b[27;5;13~", "\x1b[27;9;13~", "\x1b\r"];
let mode_two = [
"\x1b[27;2;13~",
"\x1b[27;5;13~",
@@ -4842,7 +4846,7 @@ mod tests {
for (sequence, expected) in [
("", legacy),
("\x1b[>4;1m", mode_one),
("\x1b[>4;1m", legacy),
("\x1b[>4;2m", mode_two),
("\x1b[>4n", legacy),
("\x1b[>4;2m", mode_two),
@@ -4852,7 +4856,7 @@ mod tests {
("\x1b[>4;2m\x1b[>1u", kitty),
("\x1b[<u", mode_two),
("\x1b[>4;0m", legacy),
("\x1b[>4;1m", mode_one),
("\x1b[>4;1m", legacy),
("\x1b[>04n", legacy),
("\x1b[>4;2m", mode_two),
("\x1b[>4", mode_two),
@@ -5317,17 +5321,17 @@ mod tests {
}
#[test]
fn ghostty_modify_other_keys_mode_one_preserves_shift_enter() {
fn ghostty_modify_other_keys_mode_one_is_treated_as_unnegotiated() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap();
pane.seed_history_ansi("\x1b[>4;1m");
assert_eq!(pane.modify_other_keys_level(), 1);
assert_eq!(pane.modify_other_keys_level(), 0);
let encoded = pane.encode_terminal_key(key.clone(), crate::input::KeyboardProtocol::Legacy);
assert_eq!(encoded, b"\x1b[27;2;13~");
assert_eq!(encoded, b"\r");
}
#[test]
+3 -2
View File
@@ -6222,11 +6222,12 @@ fn direct_terminal_streams_child_keyboard_and_mouse_modes() {
read_server_message(
client_control_rx
.recv_timeout(Duration::from_millis(100))
.expect("modifyOtherKeys mode-one keyboard message")
.expect("kitty flags change with modifyOtherKeys mode one")
),
// Like Ghostty, modifyOtherKeys level 1 is not a negotiated mode.
ServerMessage::DirectTerminalKeyboardProtocol {
flags: 3,
modify_other_keys_level: 1
modify_other_keys_level: 0
}
));