aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-22 15:18:08 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-22 15:18:08 +0300
commit29fd06747b31e8ec7fbf32af662263040783b6c7 (patch)
treef8dc493d02d15129ff22e1dcc6b8bc7350a95731 /src
parent3c7c4464e5d46cf28036edbd98450baae768b823 (diff)
fix: accept unicode keybindings
refs #247
Diffstat (limited to 'src')
-rw-r--r--src/config/keybinds.rs43
-rw-r--r--src/raw_input.rs47
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();