From e857710d73ad660d17ce39fa3201a39aa49f6932 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Tue, 31 Mar 2026 23:03:11 +0300 Subject: fix: align pane scrolling with tmux and ghostty --- src/input/encode.rs | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/input/mod.rs | 2 +- src/input/tests.rs | 29 ++++++++++++++++++++- 3 files changed, 101 insertions(+), 3 deletions(-) (limited to 'src/input') diff --git a/src/input/encode.rs b/src/input/encode.rs index b114935a..ee466f42 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -1,4 +1,4 @@ -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEventKind}; use super::{KeyboardProtocol, TerminalKey}; @@ -23,6 +23,77 @@ pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec< encode_legacy(key.as_key_event()) } +pub fn encode_cursor_key(code: KeyCode, application_cursor: bool) -> Vec { + match (code, application_cursor) { + (KeyCode::Up, true) => b"\x1bOA".to_vec(), + (KeyCode::Down, true) => b"\x1bOB".to_vec(), + (KeyCode::Right, true) => b"\x1bOC".to_vec(), + (KeyCode::Left, true) => b"\x1bOD".to_vec(), + (KeyCode::Up, false) => b"\x1b[A".to_vec(), + (KeyCode::Down, false) => b"\x1b[B".to_vec(), + (KeyCode::Right, false) => b"\x1b[C".to_vec(), + (KeyCode::Left, false) => b"\x1b[D".to_vec(), + _ => encode_legacy(KeyEvent::new(code, KeyModifiers::empty())), + } +} + +pub fn encode_mouse_scroll( + kind: MouseEventKind, + column: u16, + row: u16, + modifiers: KeyModifiers, + encoding: vt100::MouseProtocolEncoding, +) -> Option> { + let button = match kind { + MouseEventKind::ScrollUp => 64u16, + MouseEventKind::ScrollDown => 65u16, + MouseEventKind::ScrollLeft => 66u16, + MouseEventKind::ScrollRight => 67u16, + _ => return None, + }; + + let mut cb = button; + if modifiers.contains(KeyModifiers::SHIFT) { + cb += 4; + } + if modifiers.contains(KeyModifiers::ALT) { + cb += 8; + } + if modifiers.contains(KeyModifiers::CONTROL) { + cb += 16; + } + + let column = column as u32 + 1; + let row = row as u32 + 1; + + match encoding { + vt100::MouseProtocolEncoding::Sgr => { + Some(format!("\x1b[<{cb};{column};{row}M").into_bytes()) + } + vt100::MouseProtocolEncoding::Default => { + let cb = u8::try_from(cb + 32).ok()?; + let column = u8::try_from(column + 32).ok()?; + let row = u8::try_from(row + 32).ok()?; + Some(vec![0x1b, b'[', b'M', cb, column, row]) + } + vt100::MouseProtocolEncoding::Utf8 => { + let mut bytes = Vec::with_capacity(16); + bytes.extend_from_slice(b"\x1b[M"); + push_mouse_codepoint(&mut bytes, cb as u32 + 32)?; + push_mouse_codepoint(&mut bytes, column + 32)?; + push_mouse_codepoint(&mut bytes, row + 32)?; + Some(bytes) + } + } +} + +fn push_mouse_codepoint(bytes: &mut Vec, value: u32) -> Option<()> { + let ch = char::from_u32(value)?; + let mut buf = [0u8; 4]; + bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes()); + Some(()) +} + /// CSI u encoding: \e[{codepoint};{modifiers}u /// Used when the child has pushed Kitty keyboard enhancement. /// Returns None if the key doesn't need CSI u (unmodified basic keys). diff --git a/src/input/mod.rs b/src/input/mod.rs index de4330a4..b5ed99b4 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2,7 +2,7 @@ mod encode; mod model; mod parse; -pub use encode::{encode_key, encode_terminal_key}; +pub use encode::{encode_cursor_key, encode_key, encode_mouse_scroll, encode_terminal_key}; pub use model::{KeyboardProtocol, TerminalKey}; pub use parse::parse_terminal_key_sequence; diff --git a/src/input/tests.rs b/src/input/tests.rs index 0d9d2c74..e6900df7 100644 --- a/src/input/tests.rs +++ b/src/input/tests.rs @@ -1,7 +1,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, ModifierKeyCode}; use super::{ - encode_key, encode_terminal_key, parse_terminal_key_sequence, KeyboardProtocol, TerminalKey, + encode_cursor_key, encode_key, encode_mouse_scroll, encode_terminal_key, + parse_terminal_key_sequence, KeyboardProtocol, TerminalKey, }; fn assert_terminal_key_eq( @@ -142,6 +143,32 @@ fn legacy_alt_char_still_esc_prefix() { assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1ba"); } +#[test] +fn application_cursor_keys_use_ss3_sequences() { + assert_eq!(encode_cursor_key(KeyCode::Up, true), b"\x1bOA"); + assert_eq!(encode_cursor_key(KeyCode::Down, true), b"\x1bOB"); +} + +#[test] +fn normal_cursor_keys_use_csi_sequences() { + assert_eq!(encode_cursor_key(KeyCode::Up, false), b"\x1b[A"); + assert_eq!(encode_cursor_key(KeyCode::Down, false), b"\x1b[B"); +} + +#[test] +fn sgr_mouse_scroll_encodes_wheel_button_and_coordinates() { + let encoded = encode_mouse_scroll( + crossterm::event::MouseEventKind::ScrollDown, + 4, + 6, + KeyModifiers::SHIFT, + vt100::MouseProtocolEncoding::Sgr, + ) + .expect("mouse scroll should encode"); + + assert_eq!(encoded, b"\x1b[<69;5;7M"); +} + #[test] fn kitty_shift_enter() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT); -- cgit v1.2.3