diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 15:18:08 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-22 15:18:08 +0300 |
| commit | 29fd06747b31e8ec7fbf32af662263040783b6c7 (patch) | |
| tree | f8dc493d02d15129ff22e1dcc6b8bc7350a95731 /src | |
| parent | 3c7c4464e5d46cf28036edbd98450baae768b823 (diff) | |
fix: accept unicode keybindings
refs #247
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/keybinds.rs | 43 | ||||
| -rw-r--r-- | src/raw_input.rs | 47 |
2 files changed, 87 insertions, 3 deletions
diff --git a/src/config/keybinds.rs b/src/config/keybinds.rs index edd64c90..014bab7a 100644 --- a/src/config/keybinds.rs +++ b/src/config/keybinds.rs @@ -820,6 +820,7 @@ pub(super) fn parse_key_combo(s: &str) -> Option<KeyCombo> { } let key_str = key_str?; + let single_char = single_key_char(key_str); let lower = key_str.to_lowercase(); let code = match lower.as_str() { "space" | " " => KeyCode::Char(' '), @@ -848,8 +849,8 @@ pub(super) fn parse_key_combo(s: &str) -> Option<KeyCombo> { "ampersand" => KeyCode::Char('&'), "backtick" => KeyCode::Char('`'), "plus" => KeyCode::Char('+'), - s if s.len() == 1 => { - let ch = key_str.chars().next().unwrap(); + _ if single_char.is_some() => { + let ch = single_char?; if ch.is_ascii_uppercase() { modifiers |= KeyModifiers::SHIFT; KeyCode::Char(ch.to_ascii_lowercase()) @@ -864,6 +865,16 @@ pub(super) fn parse_key_combo(s: &str) -> Option<KeyCombo> { Some(normalize_key_combo((code, modifiers))) } +fn single_key_char(s: &str) -> Option<char> { + let mut chars = s.chars(); + let ch = chars.next()?; + if chars.next().is_none() { + Some(ch) + } else { + None + } +} + fn parse_key_combo_with_diagnostic( s: &str, field: &str, @@ -1005,6 +1016,34 @@ mod tests { } #[test] + fn parse_unicode_char_combo() { + assert_eq!( + parse_key_combo("ö"), + Some((KeyCode::Char('ö'), KeyModifiers::empty())) + ); + assert_eq!( + parse_key_combo("alt+é"), + Some((KeyCode::Char('é'), KeyModifiers::ALT)) + ); + } + + #[test] + fn unicode_prefix_config_is_valid() { + let config: Config = toml::from_str( + r#" +[keys] +prefix = "ö" +"#, + ) + .unwrap(); + assert_eq!( + config.prefix_key(), + (KeyCode::Char('ö'), KeyModifiers::empty()) + ); + assert!(config.collect_diagnostics().is_empty()); + } + + #[test] fn parse_shift_tab_as_backtab() { assert_eq!( parse_key_combo("shift+tab"), diff --git a/src/raw_input.rs b/src/raw_input.rs index fa496b95..d75c6c49 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -231,6 +231,11 @@ pub(crate) fn flush_incomplete_input_bytes(buffer: &mut Vec<u8>) -> Option<Vec<u return None; } + if buffer.first() == Some(&ESC) && starts_with_incomplete_utf8_char(&buffer[1..]) { + tracing::trace!(bytes = ?buffer, "waiting for escaped UTF-8 continuation bytes"); + return None; + } + tracing::debug!(bytes = ?buffer, "dropping incomplete raw input buffer after timeout"); buffer.clear(); None @@ -388,7 +393,12 @@ fn complete_escape_sequence_len(buffer: &[u8]) -> Option<usize> { return (buffer.len() >= 3).then_some(3); } - Some(2) + let escaped_char_width = utf8_char_width(buffer[1])?; + if buffer.len() < 1 + escaped_char_width { + return None; + } + std::str::from_utf8(&buffer[1..1 + escaped_char_width]).ok()?; + Some(1 + escaped_char_width) } fn find_osc_terminator(buffer: &[u8]) -> Option<usize> { @@ -1046,6 +1056,41 @@ mod tests { } #[test] + fn alt_utf8_char_drains_as_one_event_before_following_input() { + let events = parse_raw_input_bytes_sync("\x1béx".as_bytes()); + assert_eq!(events.len(), 2); + assert_raw_key( + events.into_iter().next().unwrap(), + KeyCode::Char('é'), + KeyModifiers::ALT, + ); + } + + #[test] + fn chunked_alt_utf8_waits_for_continuation_byte_after_escape() { + let (tx, mut rx) = mpsc::channel(8); + let mut buffer = Vec::new(); + let bytes = "\x1bé".as_bytes(); + + drain_chunk(&mut buffer, &tx, &bytes[..2]); + assert_eq!(buffer, bytes[..2]); + assert!(collect_events(&mut rx).is_empty()); + flush_incomplete_buffer(&mut buffer, &tx); + assert_eq!(buffer, bytes[..2]); + assert!(collect_events(&mut rx).is_empty()); + + drain_chunk(&mut buffer, &tx, &bytes[2..]); + assert!(buffer.is_empty()); + let events = collect_events(&mut rx); + assert_eq!(events.len(), 1); + assert_raw_key( + events.into_iter().next().unwrap(), + KeyCode::Char('é'), + KeyModifiers::ALT, + ); + } + + #[test] fn chunked_utf8_waits_for_continuation_byte() { let (tx, mut rx) = mpsc::channel(8); let mut buffer = Vec::new(); |
