fix: preserve shift-enter in windows panes (#1909)

refs #1743
This commit is contained in:
JJ Liebig 2026-07-27 01:16:57 +02:00 committed by GitHub
parent bb29eedb72
commit 04205ac070
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 10 deletions

View File

@ -5441,8 +5441,8 @@ last_pane = "prefix+tab"
let mut app = test_app();
let mut workspace = Workspace::test_new("test");
let focused = workspace.focused_pane_id().unwrap();
let (runtime, mut rx) =
TerminalRuntime::test_with_channel_and_scrollback_bytes(80, 24, 0, b"\x1b[>4;1m", 4);
let (runtime, mut rx) = TerminalRuntime::test_with_channel(80, 24);
runtime.test_process_pty_bytes(b"\x1b[>4;1m");
workspace.tabs[0].runtimes.insert(focused, runtime);
app.state.workspaces = vec![workspace];
app.state.active = Some(0);

View File

@ -3,6 +3,8 @@ pub(crate) struct KittyKeyboardTracker {
pending: Vec<u8>,
stack: Vec<u16>,
flags: u16,
#[cfg(windows)]
modify_other_keys: bool,
}
impl KittyKeyboardTracker {
@ -30,6 +32,10 @@ impl KittyKeyboardTracker {
self.store_pending(&bytes[index..]);
break;
}
#[cfg(windows)]
if bytes[index + 1] == b'c' {
self.modify_other_keys = false;
}
if bytes[index + 1] != b'[' {
index += 1;
continue;
@ -44,13 +50,52 @@ impl KittyKeyboardTracker {
break;
}
if bytes[end] == b'u' {
self.observe_csi_u(&bytes[index + 2..end]);
match bytes[end] {
b'u' => self.observe_csi_u(&bytes[index + 2..end]),
#[cfg(windows)]
b'm' => self.observe_modify_other_keys(&bytes[index + 2..end]),
#[cfg(windows)]
b'n' if bytes[index + 2..end]
.strip_prefix(b">")
.is_some_and(|params| {
!params.contains(&b';') && parse_kitty_keyboard_flags(params) == 4
}) =>
{
self.modify_other_keys = false;
}
_ => {}
}
index = end + 1;
}
}
#[cfg(windows)]
pub(crate) fn modify_other_keys_enabled(&self) -> bool {
self.modify_other_keys
}
#[cfg(windows)]
fn observe_modify_other_keys(&mut self, params: &[u8]) {
let Some(params) = params.strip_prefix(b">") else {
return;
};
if params.is_empty() {
self.modify_other_keys = false;
return;
}
let mut parts = params.split(|byte| *byte == b';');
let resource = parts.next().unwrap_or_default();
let value = parts.next();
if parts.next().is_some() {
return;
}
if parse_kitty_keyboard_flags(resource) == 4 {
self.modify_other_keys =
value.is_some_and(|value| parse_kitty_keyboard_flags(value) != 0);
}
}
fn store_pending(&mut self, bytes: &[u8]) {
self.pending.clear();
if bytes.len() <= 64 {
@ -115,11 +160,19 @@ mod tests {
fn buffers_split_csi_sequences() {
let mut tracker = KittyKeyboardTracker::default();
tracker.observe(b"\x1b[>1u\x1b[>5");
tracker.observe(b"u\x1b[<");
tracker.observe(b"\x1b[>1u\x1b[>4;");
tracker.observe(b"01m\x1b[>5u\x1b[<");
tracker.observe(b"u");
assert_eq!(tracker.flags, 1);
assert_eq!(tracker.stack, vec![0]);
#[cfg(windows)]
{
assert!(tracker.modify_other_keys_enabled());
tracker.observe(b"\x1b[>1m");
assert!(tracker.modify_other_keys_enabled());
tracker.observe(b"\x1b[>04n");
assert!(!tracker.modify_other_keys_enabled());
}
}
}

View File

@ -1286,6 +1286,8 @@ impl GhosttyPaneTerminal {
let Ok(mut core) = self.core.lock() else {
return;
};
#[cfg(windows)]
core.kitty_keyboard.observe(ansi.as_bytes());
core.terminal.write(ansi.as_bytes());
#[cfg(windows)]
windows_recent_fallback::update(&mut core);
@ -1565,6 +1567,9 @@ impl GhosttyPaneTerminal {
mouse_protocol_mode,
mouse_protocol_encoding,
mouse_alternate_scroll,
#[cfg(windows)]
modify_other_keys: core.kitty_keyboard.modify_other_keys_enabled(),
#[cfg(not(windows))]
modify_other_keys: core
.terminal
.keyboard_state_ansi()
@ -1619,6 +1624,18 @@ impl GhosttyPaneTerminal {
key: crate::input::TerminalKey,
protocol: crate::input::KeyboardProtocol,
) -> Vec<u8> {
#[cfg(windows)]
if let Some(bytes) = crate::platform::encode_windows_conpty_shift_enter(key) {
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()
}) {
return bytes;
}
}
if ghostty_prefers_herdr_text_encoding(key) {
return crate::input::encode_terminal_key(key, protocol);
}
@ -3967,11 +3984,16 @@ mod tests {
fn ghostty_modify_other_keys_mode_one_preserves_shift_enter() {
let (tx, _rx) = mpsc::channel(4);
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);
pane.process_pty_bytes(pane_id, 0, b"\x1b[>4;1m", &tx);
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap();
#[cfg(windows)]
assert_eq!(
pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy),
b"\x1b[13;28;13;1;16;1_"
);
pane.seed_history_ansi("\x1b[>4;1m");
let encoded = pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy);
assert_eq!(encoded, b"\x1b[27;2;13~");

View File

@ -56,6 +56,17 @@ use super::{ClipboardImage, ForegroundJob, Signal};
const STILL_ACTIVE: u32 = 259;
const FOREGROUND_PROCESS_SNAPSHOT_CACHE_TTL: Duration = Duration::from_millis(250);
pub(crate) fn encode_windows_conpty_shift_enter(key: crate::input::TerminalKey) -> Option<Vec<u8>> {
use crossterm::event::{KeyCode, KeyEventKind, KeyModifiers};
if key.code != KeyCode::Enter || key.modifiers != KeyModifiers::SHIFT {
return None;
}
let key_down = !matches!(key.kind, KeyEventKind::Release);
Some(format!("\x1b[13;28;13;{};16;1_", u8::from(key_down)).into_bytes())
}
#[derive(Debug)]
struct CachedProcessSnapshot {
built_at: Instant,