aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-19 14:24:30 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-19 14:24:30 +0300
commitd616bcd6b86742762fec5f0696a5c13701fe717c (patch)
tree44e20df741c3c587168a351e422a67ede524ca2d /src
parent6cbdba434fd15fc3818302a5843593da47db2eb4 (diff)
fix: preserve modified enter input
refs #168
Diffstat (limited to 'src')
-rw-r--r--src/app/mod.rs21
-rw-r--r--src/client/mod.rs28
-rw-r--r--src/input/mod.rs4
-rw-r--r--src/input/model.rs60
-rw-r--r--src/main.rs24
-rw-r--r--src/pane.rs2
-rw-r--r--src/pane/terminal.rs32
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();