diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-19 14:24:30 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-19 14:24:30 +0300 |
| commit | d616bcd6b86742762fec5f0696a5c13701fe717c (patch) | |
| tree | 44e20df741c3c587168a351e422a67ede524ca2d | |
| parent | 6cbdba434fd15fc3818302a5843593da47db2eb4 (diff) | |
fix: preserve modified enter input
refs #168
| -rw-r--r-- | src/app/mod.rs | 21 | ||||
| -rw-r--r-- | src/client/mod.rs | 28 | ||||
| -rw-r--r-- | src/input/mod.rs | 4 | ||||
| -rw-r--r-- | src/input/model.rs | 60 | ||||
| -rw-r--r-- | src/main.rs | 24 | ||||
| -rw-r--r-- | src/pane.rs | 2 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 32 |
7 files changed, 148 insertions, 23 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 125fabed..31ea3f6f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2453,6 +2453,27 @@ mod tests { } #[tokio::test] + async fn route_client_input_preserves_shift_enter_for_modify_other_keys_pane() { + 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); + workspace.tabs[0].runtimes.insert(focused, runtime); + app.state.workspaces = vec![workspace]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + + app.route_client_input(b"\x1b[13;2u".to_vec()); + + assert_eq!( + rx.recv().await.unwrap(), + bytes::Bytes::from_static(b"\x1b[27;2;13~") + ); + } + + #[tokio::test] async fn route_client_input_splits_multi_event_payloads_before_forwarding() { let mut app = test_app(); let mut workspace = Workspace::test_new("test"); diff --git a/src/client/mod.rs b/src/client/mod.rs index c743a24b..101e168c 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -234,20 +234,28 @@ fn setup_terminal_with_capabilities( execute!(io::stdout(), DisableMouseCapture)?; } - // tmux doesn't understand kitty keyboard protocol push. - // Enable modifyOtherKeys mode 2 for tmux only for the full app client. - let in_tmux = enable_client_protocols && std::env::var("TMUX").is_ok(); - if in_tmux { - io::stdout().write_all(b"\x1b[>4;2m")?; + let modify_other_keys_mode = enable_client_protocols + .then(|| { + crate::input::host_modify_other_keys_mode( + std::env::var("TMUX").is_ok(), + std::env::var("TERM_PROGRAM").ok().as_deref(), + std::env::var_os("WEZTERM_PANE").is_some(), + ) + }) + .flatten(); + if let Some(mode) = modify_other_keys_mode { + io::stdout().write_all(mode.set_sequence())?; io::stdout().flush()?; } - Ok(TerminalGuard { in_tmux }) + Ok(TerminalGuard { + reset_modify_other_keys: modify_other_keys_mode.is_some(), + }) } /// Guard that restores the terminal when dropped. struct TerminalGuard { - in_tmux: bool, + reset_modify_other_keys: bool, } fn write_terminal_restore_postlude(writer: &mut impl io::Write) -> io::Result<()> { @@ -264,11 +272,11 @@ fn set_mouse_capture(enabled: bool) -> io::Result<()> { } } -fn restore_terminal_state(in_tmux: bool) { +fn restore_terminal_state(reset_modify_other_keys: bool) { let _ = clear_received_kitty_graphics(&mut io::stdout()); // Reset modifyOtherKeys if we enabled it. - if in_tmux { + if reset_modify_other_keys { let _ = io::stdout().write_all(b"\x1b[>4;0m"); let _ = io::stdout().flush(); } @@ -286,7 +294,7 @@ fn restore_terminal_state(in_tmux: bool) { impl Drop for TerminalGuard { fn drop(&mut self) { - restore_terminal_state(self.in_tmux); + restore_terminal_state(self.reset_modify_other_keys); } } diff --git a/src/input/mod.rs b/src/input/mod.rs index b4886256..ab7192c7 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -7,7 +7,7 @@ pub use encode::{ encode_cursor_key, encode_mouse_button, encode_mouse_scroll, encode_terminal_key, }; pub use model::{ - ime_compatible_keyboard_enhancement_flags, KeyboardProtocol, MouseProtocolEncoding, - MouseProtocolMode, TerminalKey, + host_modify_other_keys_mode, ime_compatible_keyboard_enhancement_flags, KeyboardProtocol, + MouseProtocolEncoding, MouseProtocolMode, TerminalKey, }; pub use parse::parse_terminal_key_sequence; diff --git a/src/input/model.rs b/src/input/model.rs index 95732bbc..5dddebda 100644 --- a/src/input/model.rs +++ b/src/input/model.rs @@ -47,6 +47,37 @@ pub fn ime_compatible_keyboard_enhancement_flags() -> KeyboardEnhancementFlags { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModifyOtherKeysMode { + Mode1, + Mode2, +} + +impl ModifyOtherKeysMode { + pub fn set_sequence(self) -> &'static [u8] { + match self { + Self::Mode1 => b"\x1b[>4;1m", + Self::Mode2 => b"\x1b[>4;2m", + } + } +} + +pub fn host_modify_other_keys_mode( + in_tmux: bool, + term_program: Option<&str>, + wezterm_pane: bool, +) -> Option<ModifyOtherKeysMode> { + if in_tmux { + return Some(ModifyOtherKeysMode::Mode2); + } + + if wezterm_pane || term_program.is_some_and(|program| program.eq_ignore_ascii_case("wezterm")) { + return Some(ModifyOtherKeysMode::Mode1); + } + + None +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeyboardProtocol { Legacy, Kitty { flags: u16 }, @@ -113,4 +144,33 @@ mod tests { assert!(flags.contains(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS)); assert!(!flags.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)); } + + #[test] + fn modify_other_keys_mode_is_enabled_for_tmux() { + assert_eq!( + host_modify_other_keys_mode(true, Some("WezTerm"), true), + Some(ModifyOtherKeysMode::Mode2) + ); + } + + #[test] + fn modify_other_keys_mode_is_enabled_for_wezterm_hosts() { + assert_eq!( + host_modify_other_keys_mode(false, Some("WezTerm"), false), + Some(ModifyOtherKeysMode::Mode1) + ); + assert_eq!( + host_modify_other_keys_mode(false, None, true), + Some(ModifyOtherKeysMode::Mode1) + ); + } + + #[test] + fn modify_other_keys_mode_is_not_enabled_for_unknown_hosts() { + assert_eq!( + host_modify_other_keys_mode(false, Some("ghostty"), false), + None + ); + assert_eq!(host_modify_other_keys_mode(false, None, false), None); + } } diff --git a/src/main.rs b/src/main.rs index efdd08b0..744d4368 100644 --- a/src/main.rs +++ b/src/main.rs @@ -453,13 +453,17 @@ fn main() -> io::Result<()> { Err(err) => return Err(err), }; - let in_tmux = std::env::var("TMUX").is_ok(); + let modify_other_keys_mode = crate::input::host_modify_other_keys_mode( + std::env::var("TMUX").is_ok(), + std::env::var("TERM_PROGRAM").ok().as_deref(), + std::env::var_os("WEZTERM_PANE").is_some(), + ); let original_hook = std::panic::take_hook(); - let panic_in_tmux = in_tmux; + let panic_resets_modify_other_keys = modify_other_keys_mode.is_some(); std::panic::set_hook(Box::new(move |info| { tracing::error!("PANIC: {info}"); - if panic_in_tmux { + if panic_resets_modify_other_keys { let _ = std::io::Write::write_all(&mut io::stdout(), b"\x1b[>4;0m"); } if crate::kitty_graphics::is_enabled() { @@ -503,12 +507,12 @@ fn main() -> io::Result<()> { PushKeyboardEnhancementFlags(crate::input::ime_compatible_keyboard_enhancement_flags()) )?; - // tmux doesn't understand kitty keyboard protocol push (\e[>1u). - // It uses modifyOtherKeys mode to send CSI u sequences for modified keys. - // Enable modifyOtherKeys mode 2 so tmux sends Shift+Enter as \e[13;2u etc. - if in_tmux { + // Some hosts do not honor Kitty keyboard enhancement pushes for + // Shift+Enter. Enable xterm modifyOtherKeys only on hosts where we + // know it is needed and parseable, so modified Enter stays distinct. + if let Some(mode) = modify_other_keys_mode { use std::io::Write; - std::io::stdout().write_all(b"\x1b[>4;2m")?; + std::io::stdout().write_all(mode.set_sequence())?; std::io::stdout().flush()?; } @@ -524,8 +528,8 @@ fn main() -> io::Result<()> { ); let result = app.run(&mut terminal).await; - // Reset modifyOtherKeys if we enabled it - if in_tmux { + // Reset modifyOtherKeys if we enabled it. + if modify_other_keys_mode.is_some() { use std::io::Write; std::io::stdout().write_all(b"\x1b[>4;0m")?; std::io::stdout().flush()?; diff --git a/src/pane.rs b/src/pane.rs index 592255a5..aba31943 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -1025,7 +1025,7 @@ impl PaneRuntime { Self::test_with_channel_and_scrollback_bytes(cols, rows, scrollback_limit_bytes, bytes, 4).0 } - fn test_with_channel_and_scrollback_bytes( + pub(crate) fn test_with_channel_and_scrollback_bytes( cols: u16, rows: u16, scrollback_limit_bytes: usize, diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index fcbedeff..f25c70ef 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1503,6 +1503,38 @@ mod tests { } #[test] + fn ghostty_kitty_pane_encodes_shift_enter_as_csi_u() { + 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[>5u", &tx); + + let key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap(); + let encoded = pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy); + + assert_eq!( + pane.keyboard_protocol(), + Some(crate::input::KeyboardProtocol::Kitty { flags: 5 }) + ); + assert_eq!(encoded, b"\x1b[13;2u"); + } + + #[test] + 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 key = crate::input::parse_terminal_key_sequence("\x1b[13;2u").unwrap(); + let encoded = pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy); + + assert_eq!(encoded, b"\x1b[27;2;13~"); + } + + #[test] fn ghostty_kitty_pane_encodes_parsed_legacy_alt_backspace_as_csi_u() { let (tx, _rx) = mpsc::channel(4); let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); |
