aboutsummaryrefslogtreecommitdiff
path: root/src/input
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 16:33:26 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-03-30 16:33:26 +0300
commit800a8c1a7a73c383fc97d4e5b6febf7f3249e948 (patch)
tree287677cb2a8aaa98aa529e847dda34fe688fc459 /src/input
parentfff21d64905cc50424aec698c88965aefd2df724 (diff)
fix ghostty macos arrow and modifier key parsing
Diffstat (limited to 'src/input')
-rw-r--r--src/input/parse.rs67
-rw-r--r--src/input/tests.rs26
2 files changed, 84 insertions, 9 deletions
diff --git a/src/input/parse.rs b/src/input/parse.rs
index 1662d9ff..995ad75b 100644
--- a/src/input/parse.rs
+++ b/src/input/parse.rs
@@ -1,4 +1,4 @@
-use crossterm::event::{KeyCode, KeyModifiers};
+use crossterm::event::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
use super::TerminalKey;
@@ -116,10 +116,10 @@ fn parse_legacy_special_sequence(data: &str) -> Option<TerminalKey> {
"\x1b\x1b[B" => Some(TerminalKey::new(KeyCode::Down, KeyModifiers::ALT)),
"\x1b\x1b[C" => Some(TerminalKey::new(KeyCode::Right, KeyModifiers::ALT)),
"\x1b\x1b[D" => Some(TerminalKey::new(KeyCode::Left, KeyModifiers::ALT)),
- "\x1b[A" => Some(TerminalKey::new(KeyCode::Up, KeyModifiers::empty())),
- "\x1b[B" => Some(TerminalKey::new(KeyCode::Down, KeyModifiers::empty())),
- "\x1b[C" => Some(TerminalKey::new(KeyCode::Right, KeyModifiers::empty())),
- "\x1b[D" => Some(TerminalKey::new(KeyCode::Left, KeyModifiers::empty())),
+ "\x1b[A" | "\x1bOA" => Some(TerminalKey::new(KeyCode::Up, KeyModifiers::empty())),
+ "\x1b[B" | "\x1bOB" => Some(TerminalKey::new(KeyCode::Down, KeyModifiers::empty())),
+ "\x1b[C" | "\x1bOC" => Some(TerminalKey::new(KeyCode::Right, KeyModifiers::empty())),
+ "\x1b[D" | "\x1bOD" => Some(TerminalKey::new(KeyCode::Left, KeyModifiers::empty())),
"\x1b[H" | "\x1bOH" | "\x1b[1~" | "\x1b[7~" => {
Some(TerminalKey::new(KeyCode::Home, KeyModifiers::empty()))
}
@@ -141,8 +141,9 @@ fn parse_xterm_modified_special_sequence(data: &str) -> Option<TerminalKey> {
if let Some(body) = body.strip_prefix("1;") {
let suffix_char = body.chars().last()?;
if suffix_char.is_ascii_alphabetic() {
- let modifier = body.strip_suffix(suffix_char)?;
- let mod_value = modifier.parse::<u8>().ok()?.checked_sub(1)?;
+ let modifier_and_event = body.strip_suffix(suffix_char)?;
+ let (modifier_text, event_type) = split_modifier_and_event(modifier_and_event);
+ let mod_value = modifier_text.parse::<u8>().ok()?.checked_sub(1)?;
let code = match suffix_char {
'A' => KeyCode::Up,
'B' => KeyCode::Down,
@@ -152,7 +153,10 @@ fn parse_xterm_modified_special_sequence(data: &str) -> Option<TerminalKey> {
'F' => KeyCode::End,
_ => return None,
};
- return Some(TerminalKey::new(code, key_modifiers_from_u8(mod_value)));
+ return Some(
+ TerminalKey::new(code, key_modifiers_from_u8(mod_value))
+ .with_kind(parse_kitty_event_type(event_type)?),
+ );
}
}
@@ -169,6 +173,13 @@ fn parse_xterm_modified_special_sequence(data: &str) -> Option<TerminalKey> {
Some(TerminalKey::new(code, key_modifiers_from_u8(mod_value)))
}
+fn split_modifier_and_event(input: &str) -> (&str, Option<&str>) {
+ match input.split_once(':') {
+ Some((modifier, event)) if !modifier.is_empty() => (modifier, Some(event)),
+ _ => (input, None),
+ }
+}
+
#[allow(dead_code)] // Reserved for the upcoming raw stdin parser.
fn parse_kitty_event_type(value: Option<&str>) -> Option<crossterm::event::KeyEventKind> {
match value.unwrap_or("1") {
@@ -186,6 +197,13 @@ fn kitty_codepoint_to_keycode(codepoint: u32) -> Option<KeyCode> {
9 => Some(KeyCode::Tab),
13 | 57414 => Some(KeyCode::Enter),
27 => Some(KeyCode::Esc),
+ 57358 => Some(KeyCode::CapsLock),
+ 57359 => Some(KeyCode::ScrollLock),
+ 57360 => Some(KeyCode::NumLock),
+ 57361 => Some(KeyCode::PrintScreen),
+ 57362 => Some(KeyCode::Pause),
+ 57363 => Some(KeyCode::Menu),
+ 57376..=57398 => Some(KeyCode::F((codepoint - 57376 + 13) as u8)),
57417 => Some(KeyCode::Left),
57418 => Some(KeyCode::Right),
57419 => Some(KeyCode::Up),
@@ -196,10 +214,43 @@ fn kitty_codepoint_to_keycode(codepoint: u32) -> Option<KeyCode> {
57424 => Some(KeyCode::End),
57425 => Some(KeyCode::Insert),
57426 => Some(KeyCode::Delete),
+ 57427 => Some(KeyCode::KeypadBegin),
+ 57428 => Some(KeyCode::Media(MediaKeyCode::Play)),
+ 57429 => Some(KeyCode::Media(MediaKeyCode::Pause)),
+ 57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)),
+ 57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)),
+ 57432 => Some(KeyCode::Media(MediaKeyCode::Stop)),
+ 57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)),
+ 57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)),
+ 57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)),
+ 57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)),
+ 57437 => Some(KeyCode::Media(MediaKeyCode::Record)),
+ 57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)),
+ 57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)),
+ 57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)),
+ 57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)),
+ 57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)),
+ 57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)),
+ 57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)),
+ 57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)),
+ 57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)),
+ 57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)),
+ 57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)),
+ 57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)),
+ 57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)),
+ 57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)),
+ 57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)),
+ 57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)),
+ 57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)),
+ value if is_kitty_functional_codepoint(value) => None,
value => char::from_u32(value).map(KeyCode::Char),
}
}
+fn is_kitty_functional_codepoint(codepoint: u32) -> bool {
+ (57358..=57454).contains(&codepoint)
+}
+
#[allow(dead_code)] // Reserved for the upcoming raw stdin parser.
fn key_modifiers_from_u8(modifier: u8) -> KeyModifiers {
let mut mods = KeyModifiers::empty();
diff --git a/src/input/tests.rs b/src/input/tests.rs
index a4f32626..0d9d2c74 100644
--- a/src/input/tests.rs
+++ b/src/input/tests.rs
@@ -1,4 +1,4 @@
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, ModifierKeyCode};
use super::{
encode_key, encode_terminal_key, parse_terminal_key_sequence, KeyboardProtocol, TerminalKey,
@@ -326,6 +326,30 @@ fn parse_legacy_up_arrow_sequence() {
}
#[test]
+fn parse_kitty_modifier_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[57441;2:1u").unwrap();
+ assert_eq!(key.code, KeyCode::Modifier(ModifierKeyCode::LeftShift));
+ assert_eq!(key.modifiers, KeyModifiers::SHIFT);
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Press);
+}
+
+#[test]
+fn parse_ghostty_enhanced_up_arrow_press_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[1;1:1A").unwrap();
+ assert_eq!(key.code, KeyCode::Up);
+ assert_eq!(key.modifiers, KeyModifiers::empty());
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Press);
+}
+
+#[test]
+fn parse_ghostty_enhanced_up_arrow_release_sequence() {
+ let key = parse_terminal_key_sequence("\x1b[1;1:3A").unwrap();
+ assert_eq!(key.code, KeyCode::Up);
+ assert_eq!(key.modifiers, KeyModifiers::empty());
+ assert_eq!(key.kind, crossterm::event::KeyEventKind::Release);
+}
+
+#[test]
fn parse_xterm_alt_up_arrow_sequence() {
let key = parse_terminal_key_sequence("\x1b[1;3A").unwrap();
assert_eq!(key.code, KeyCode::Up);