diff options
author | Kirill Chibisov <contact@kchibisov.com> | 2023-12-06 09:26:07 +0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-06 09:26:07 +0400 |
commit | cb03806e2ab85674c45e87e1bb24dfe2fd1a918c (patch) | |
tree | 3561fc6785281fb3a963c199fe9a12df4007bed7 /alacritty/src/input/keyboard.rs | |
parent | 7c9d9f3b166f2aade76d35408b5acb5d3ccd1c94 (diff) | |
download | r-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.tar.gz r-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.tar.bz2 r-alacritty-cb03806e2ab85674c45e87e1bb24dfe2fd1a918c.zip |
Implement kitty's keyboard protocol
The protocol enables robust key reporting for the applications, so
they could bind more keys and the user won't have collisions with
the normal control keys.
Links: https://sw.kovidgoyal.net/kitty/keyboard-protocol
Fixes #6378.
Diffstat (limited to 'alacritty/src/input/keyboard.rs')
-rw-r--r-- | alacritty/src/input/keyboard.rs | 584 |
1 files changed, 584 insertions, 0 deletions
diff --git a/alacritty/src/input/keyboard.rs b/alacritty/src/input/keyboard.rs new file mode 100644 index 00000000..94633cb1 --- /dev/null +++ b/alacritty/src/input/keyboard.rs @@ -0,0 +1,584 @@ +use std::borrow::Cow; +use std::mem; + +use winit::event::{ElementState, KeyEvent}; +#[cfg(target_os = "macos")] +use winit::keyboard::ModifiersKeyState; +use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey}; +#[cfg(target_os = "macos")] +use winit::platform::macos::OptionAsAlt; + +use alacritty_terminal::event::EventListener; +use alacritty_terminal::term::TermMode; +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; + +use crate::config::{Action, BindingKey, BindingMode}; +use crate::event::TYPING_SEARCH_DELAY; +use crate::input::{ActionContext, Execute, Processor}; +use crate::scheduler::{TimerId, Topic}; + +impl<T: EventListener, A: ActionContext<T>> Processor<T, A> { + /// Process key input. + pub fn key_input(&mut self, key: KeyEvent) { + // IME input will be applied on commit and shouldn't trigger key bindings. + if self.ctx.display().ime.preedit().is_some() { + return; + } + + let mode = *self.ctx.terminal().mode(); + let mods = self.ctx.modifiers().state(); + + if key.state == ElementState::Released { + self.key_release(key, mode, mods); + return; + } + + let text = key.text_with_all_modifiers().unwrap_or_default(); + + // All key bindings are disabled while a hint is being selected. + if self.ctx.display().hint_state.active() { + for character in text.chars() { + self.ctx.hint_input(character); + } + return; + } + + // First key after inline search is captured. + let inline_state = self.ctx.inline_search_state(); + if mem::take(&mut inline_state.char_pending) { + if let Some(c) = text.chars().next() { + inline_state.character = Some(c); + + // Immediately move to the captured character. + self.ctx.inline_search_next(); + } + + // Ignore all other characters in `text`. + return; + } + + // Reset search delay when the user is still typing. + self.reset_search_delay(); + + // Key bindings suppress the character input. + if self.process_key_bindings(&key) { + return; + } + + if self.ctx.search_active() { + for character in text.chars() { + self.ctx.search_input(character); + } + + return; + } + + // Vi mode on its own doesn't have any input, the search input was done before. + if mode.contains(TermMode::VI) { + return; + } + + let build_key_sequence = Self::should_build_sequence(&key, text, mode, mods); + + let bytes = if build_key_sequence { + build_sequence(key, mods, mode) + } else { + let mut bytes = Vec::with_capacity(text.len() + 1); + if self.alt_send_esc() && text.len() == 1 { + bytes.push(b'\x1b'); + } + + bytes.extend_from_slice(text.as_bytes()); + bytes + }; + + // Write only if we have something to write. + if !bytes.is_empty() { + self.ctx.on_terminal_input_start(); + self.ctx.write_to_pty(bytes); + } + } + + /// Check whether we should try to build escape sequence for the [`KeyEvent`]. + fn should_build_sequence( + key: &KeyEvent, + text: &str, + mode: TermMode, + mods: ModifiersState, + ) -> bool { + if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) { + true + } else if mode.contains(TermMode::DISAMBIGUATE_ESC_CODES) { + let on_numpad = key.location == KeyLocation::Numpad; + let is_escape = key.logical_key == Key::Named(NamedKey::Escape); + is_escape || (!mods.is_empty() && mods != ModifiersState::SHIFT) || on_numpad + } else { + // `Delete` key always has text attached to it, but it's a named key, thus needs to be + // excluded here as well. + text.is_empty() || key.logical_key == Key::Named(NamedKey::Delete) + } + } + + /// Whether we should send `ESC` due to `Alt` being pressed. + #[cfg(not(target_os = "macos"))] + fn alt_send_esc(&mut self) -> bool { + self.ctx.modifiers().state().alt_key() + } + + #[cfg(target_os = "macos")] + fn alt_send_esc(&mut self) -> bool { + let option_as_alt = self.ctx.config().window.option_as_alt(); + self.ctx.modifiers().state().alt_key() + && (option_as_alt == OptionAsAlt::Both + || (option_as_alt == OptionAsAlt::OnlyLeft + && self.ctx.modifiers().lalt_state() == ModifiersKeyState::Pressed) + || (option_as_alt == OptionAsAlt::OnlyRight + && self.ctx.modifiers().ralt_state() == ModifiersKeyState::Pressed)) + } + + /// Attempt to find a binding and execute its action. + /// + /// The provided mode, mods, and key must match what is allowed by a binding + /// for its action to be executed. + fn process_key_bindings(&mut self, key: &KeyEvent) -> bool { + let mode = BindingMode::new(self.ctx.terminal().mode(), self.ctx.search_active()); + let mods = self.ctx.modifiers().state(); + + // Don't suppress char if no bindings were triggered. + let mut suppress_chars = None; + + for i in 0..self.ctx.config().key_bindings().len() { + let binding = &self.ctx.config().key_bindings()[i]; + + // We don't want the key without modifier, because it means something else most of + // the time. However what we want is to manually lowercase the character to account + // for both small and capital letters on regular characters at the same time. + let logical_key = if let Key::Character(ch) = key.logical_key.as_ref() { + Key::Character(ch.to_lowercase().into()) + } else { + key.logical_key.clone() + }; + + let key = match (&binding.trigger, logical_key) { + (BindingKey::Scancode(_), _) => BindingKey::Scancode(key.physical_key), + (_, code) => BindingKey::Keycode { key: code, location: key.location.into() }, + }; + + if binding.is_triggered_by(mode, mods, &key) { + // Pass through the key if any of the bindings has the `ReceiveChar` action. + *suppress_chars.get_or_insert(true) &= binding.action != Action::ReceiveChar; + + // Binding was triggered; run the action. + binding.action.clone().execute(&mut self.ctx); + } + } + + suppress_chars.unwrap_or(false) + } + + /// Handle key release. + fn key_release(&mut self, key: KeyEvent, mode: TermMode, mods: ModifiersState) { + if !mode.contains(TermMode::REPORT_EVENT_TYPES) + || mode.contains(TermMode::VI) + || self.ctx.search_active() + || self.ctx.display().hint_state.active() + { + return; + } + + let bytes: Cow<'static, [u8]> = match key.logical_key.as_ref() { + // NOTE: Echo the key back on release to follow kitty/foot behavior. When + // KEYBOARD_REPORT_ALL_KEYS_AS_ESC is used, we build proper escapes for + // the keys below. + _ if mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC) => { + build_sequence(key, mods, mode).into() + }, + // Winit uses different keys for `Backspace` so we expliictly specify the + // values, instead of using what was passed to us from it. + Key::Named(NamedKey::Tab) => [b'\t'].as_slice().into(), + Key::Named(NamedKey::Enter) => [b'\r'].as_slice().into(), + Key::Named(NamedKey::Backspace) => [b'\x7f'].as_slice().into(), + Key::Named(NamedKey::Escape) => [b'\x1b'].as_slice().into(), + _ => build_sequence(key, mods, mode).into(), + }; + + self.ctx.write_to_pty(bytes); + } + + /// Reset search delay. + fn reset_search_delay(&mut self) { + if self.ctx.search_active() { + let timer_id = TimerId::new(Topic::DelayedSearch, self.ctx.window().id()); + let scheduler = self.ctx.scheduler_mut(); + if let Some(timer) = scheduler.unschedule(timer_id) { + scheduler.schedule(timer.event, TYPING_SEARCH_DELAY, false, timer.id); + } + } + } +} + +/// Build a key's keyboard escape sequence based on the given `key`, `mods`, and `mode`. +/// +/// The key sequences for `APP_KEYPAD` and alike are handled inside the bindings. +#[inline(never)] +fn build_sequence(key: KeyEvent, mods: ModifiersState, mode: TermMode) -> Vec<u8> { + let modifiers = mods.into(); + + let kitty_seq = mode.intersects( + TermMode::REPORT_ALL_KEYS_AS_ESC + | TermMode::DISAMBIGUATE_ESC_CODES + | TermMode::REPORT_EVENT_TYPES, + ); + + let kitty_encode_all = mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC); + // The default parameter is 1, so we can omit it. + let kitty_event_type = mode.contains(TermMode::REPORT_EVENT_TYPES) + && (key.repeat || key.state == ElementState::Released); + + let context = + SequenceBuilder { mode, modifiers, kitty_seq, kitty_encode_all, kitty_event_type }; + + let sequence_base = context + .try_build_numpad(&key) + .or_else(|| context.try_build_named(&key)) + .or_else(|| context.try_build_control_char_or_mod(&key)) + .or_else(|| context.try_build_textual(&key)); + + let (payload, terminator) = match sequence_base { + Some(SequenceBase { payload, terminator }) => (payload, terminator), + _ => return Vec::new(), + }; + + let mut payload = format!("\x1b[{}", payload); + + // Add modifiers information. + if kitty_event_type + || !modifiers.is_empty() + || (mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) && key.text.is_some()) + { + payload.push_str(&format!(";{}", modifiers.encode_esc_sequence())); + } + + // Push event type. + if kitty_event_type { + payload.push(':'); + let event_type = match key.state { + _ if key.repeat => '2', + ElementState::Pressed => '1', + ElementState::Released => '3', + }; + payload.push(event_type); + } + + // Associated text is not reported when the control/alt/logo is pressesed. + if mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) + && key.state != ElementState::Released + && (modifiers.is_empty() || modifiers == SequenceModifiers::SHIFT) + { + if let Some(text) = key.text { + let mut codepoints = text.chars().map(u32::from); + if let Some(codepoint) = codepoints.next() { + payload.push_str(&format!(";{codepoint}")); + } + for codepoint in codepoints { + payload.push_str(&format!(":{codepoint}")); + } + } + } + + payload.push(terminator.encode_esc_sequence()); + + payload.into_bytes() +} + +/// Helper to build escape sequence payloads from [`KeyEvent`]. +pub struct SequenceBuilder { + mode: TermMode, + /// The emitted sequence should follow the kitty keyboard protocol. + kitty_seq: bool, + /// Encode all the keys according to the protocol. + kitty_encode_all: bool, + /// Report event types. + kitty_event_type: bool, + modifiers: SequenceModifiers, +} + +impl SequenceBuilder { + /// Try building sequence from the event's emitting text. + fn try_build_textual(&self, key: &KeyEvent) -> Option<SequenceBase> { + let character = match key.logical_key.as_ref() { + Key::Character(character) => character, + _ => return None, + }; + + if character.chars().count() == 1 { + let character = character.chars().next().unwrap(); + let base_character = character.to_lowercase().next().unwrap(); + + let codepoint = u32::from(character); + let base_codepoint = u32::from(base_character); + + // NOTE: Base layouts are ignored, since winit doesn't expose this information + // yet. + let payload = if self.mode.contains(TermMode::REPORT_ALTERNATE_KEYS) + && codepoint != base_codepoint + { + format!("{codepoint}:{base_codepoint}") + } else { + codepoint.to_string() + }; + + Some(SequenceBase::new(payload.into(), SequenceTerminator::Kitty)) + } else if self.kitty_encode_all + && self.mode.contains(TermMode::REPORT_ASSOCIATED_TEXT) + && key.text.is_some() + { + // Fallback when need to report text, but we don't have any key associated with this + // text. + Some(SequenceBase::new("0".into(), SequenceTerminator::Kitty)) + } else { + None + } + } + + /// Try building from numpad key. + /// + /// `None` is returned when the key is neither known nor numpad. + fn try_build_numpad(&self, key: &KeyEvent) -> Option<SequenceBase> { + if !self.kitty_seq || key.location != KeyLocation::Numpad { + return None; + } + + let base = match key.logical_key.as_ref() { + Key::Character("0") => "57399", + Key::Character("1") => "57400", + Key::Character("2") => "57401", + Key::Character("3") => "57402", + Key::Character("4") => "57403", + Key::Character("5") => "57404", + Key::Character("6") => "57405", + Key::Character("7") => "57406", + Key::Character("8") => "57407", + Key::Character("9") => "57408", + Key::Character(".") => "57409", + Key::Character("/") => "57410", + Key::Character("*") => "57411", + Key::Character("-") => "57412", + Key::Character("+") => "57413", + Key::Character("=") => "57415", + Key::Named(named) => match named { + NamedKey::Enter => "57414", + NamedKey::ArrowLeft => "57417", + NamedKey::ArrowRight => "57418", + NamedKey::ArrowUp => "57419", + NamedKey::ArrowDown => "57420", + NamedKey::PageUp => "57421", + NamedKey::PageDown => "57422", + NamedKey::Home => "57423", + NamedKey::End => "57424", + NamedKey::Insert => "57425", + NamedKey::Delete => "57426", + _ => return None, + }, + _ => return None, + }; + + Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty)) + } + + /// Try building from [`NamedKey`]. + fn try_build_named(&self, key: &KeyEvent) -> Option<SequenceBase> { + let named = match key.logical_key { + Key::Named(named) => named, + _ => return None, + }; + + // The default parameter is 1, so we can omit it. + let one_based = if self.modifiers.is_empty() && !self.kitty_event_type { "" } else { "1" }; + let (base, terminator) = match named { + NamedKey::PageUp => ("5", SequenceTerminator::Normal('~')), + NamedKey::PageDown => ("6", SequenceTerminator::Normal('~')), + NamedKey::Insert => ("2", SequenceTerminator::Normal('~')), + NamedKey::Delete => ("3", SequenceTerminator::Normal('~')), + NamedKey::Home => (one_based, SequenceTerminator::Normal('H')), + NamedKey::End => (one_based, SequenceTerminator::Normal('F')), + NamedKey::ArrowLeft => (one_based, SequenceTerminator::Normal('D')), + NamedKey::ArrowRight => (one_based, SequenceTerminator::Normal('C')), + NamedKey::ArrowUp => (one_based, SequenceTerminator::Normal('A')), + NamedKey::ArrowDown => (one_based, SequenceTerminator::Normal('B')), + NamedKey::F1 => (one_based, SequenceTerminator::Normal('P')), + NamedKey::F2 => (one_based, SequenceTerminator::Normal('Q')), + NamedKey::F3 => { + // F3 in kitty protocol diverges from alacritty's terminfo. + if self.kitty_seq { + ("13", SequenceTerminator::Normal('~')) + } else { + (one_based, SequenceTerminator::Normal('R')) + } + }, + NamedKey::F4 => (one_based, SequenceTerminator::Normal('S')), + NamedKey::F5 => ("15", SequenceTerminator::Normal('~')), + NamedKey::F6 => ("17", SequenceTerminator::Normal('~')), + NamedKey::F7 => ("18", SequenceTerminator::Normal('~')), + NamedKey::F8 => ("19", SequenceTerminator::Normal('~')), + NamedKey::F9 => ("20", SequenceTerminator::Normal('~')), + NamedKey::F10 => ("21", SequenceTerminator::Normal('~')), + NamedKey::F11 => ("23", SequenceTerminator::Normal('~')), + NamedKey::F12 => ("24", SequenceTerminator::Normal('~')), + NamedKey::F13 => ("57376", SequenceTerminator::Kitty), + NamedKey::F14 => ("57377", SequenceTerminator::Kitty), + NamedKey::F15 => ("57378", SequenceTerminator::Kitty), + NamedKey::F16 => ("57379", SequenceTerminator::Kitty), + NamedKey::F17 => ("57380", SequenceTerminator::Kitty), + NamedKey::F18 => ("57381", SequenceTerminator::Kitty), + NamedKey::F19 => ("57382", SequenceTerminator::Kitty), + NamedKey::F20 => ("57383", SequenceTerminator::Kitty), + NamedKey::F21 => ("57384", SequenceTerminator::Kitty), + NamedKey::F22 => ("57385", SequenceTerminator::Kitty), + NamedKey::F23 => ("57386", SequenceTerminator::Kitty), + NamedKey::F24 => ("57387", SequenceTerminator::Kitty), + NamedKey::F25 => ("57388", SequenceTerminator::Kitty), + NamedKey::F26 => ("57389", SequenceTerminator::Kitty), + NamedKey::F27 => ("57390", SequenceTerminator::Kitty), + NamedKey::F28 => ("57391", SequenceTerminator::Kitty), + NamedKey::F29 => ("57392", SequenceTerminator::Kitty), + NamedKey::F30 => ("57393", SequenceTerminator::Kitty), + NamedKey::F31 => ("57394", SequenceTerminator::Kitty), + NamedKey::F32 => ("57395", SequenceTerminator::Kitty), + NamedKey::F33 => ("57396", SequenceTerminator::Kitty), + NamedKey::F34 => ("57397", SequenceTerminator::Kitty), + NamedKey::F35 => ("57398", SequenceTerminator::Kitty), + NamedKey::ScrollLock => ("57359", SequenceTerminator::Kitty), + NamedKey::PrintScreen => ("57361", SequenceTerminator::Kitty), + NamedKey::Pause => ("57362", SequenceTerminator::Kitty), + NamedKey::ContextMenu => ("57363", SequenceTerminator::Kitty), + NamedKey::MediaPlay => ("57428", SequenceTerminator::Kitty), + NamedKey::MediaPause => ("57429", SequenceTerminator::Kitty), + NamedKey::MediaPlayPause => ("57430", SequenceTerminator::Kitty), + NamedKey::MediaStop => ("57432", SequenceTerminator::Kitty), + NamedKey::MediaFastForward => ("57433", SequenceTerminator::Kitty), + NamedKey::MediaRewind => ("57434", SequenceTerminator::Kitty), + NamedKey::MediaTrackNext => ("57435", SequenceTerminator::Kitty), + NamedKey::MediaTrackPrevious => ("57436", SequenceTerminator::Kitty), + NamedKey::MediaRecord => ("57437", SequenceTerminator::Kitty), + NamedKey::AudioVolumeDown => ("57438", SequenceTerminator::Kitty), + NamedKey::AudioVolumeUp => ("57439", SequenceTerminator::Kitty), + NamedKey::AudioVolumeMute => ("57440", SequenceTerminator::Kitty), + _ => return None, + }; + + Some(SequenceBase::new(base.into(), terminator)) + } + + /// Try building escape from control characters (e.g. Enter) and modifiers. + fn try_build_control_char_or_mod(&self, key: &KeyEvent) -> Option<SequenceBase> { + if !self.kitty_encode_all && !self.kitty_seq { + return None; + } + + let named = match key.logical_key { + Key::Named(named) => named, + _ => return None, + }; + + let base = match named { + NamedKey::Tab => "9", + NamedKey::Enter => "13", + NamedKey::Escape => "27", + NamedKey::Space => "32", + NamedKey::Backspace => "127", + _ => "", + }; + + // Fail when the key is not a named control character and the active mode prohibits us + // from encoding modifier keys. + if !self.kitty_encode_all && base.is_empty() { + return None; + } + + let base = match (named, key.location) { + (NamedKey::Shift, KeyLocation::Left) => "57441", + (NamedKey::Control, KeyLocation::Left) => "57442", + (NamedKey::Alt, KeyLocation::Left) => "57443", + (NamedKey::Super, KeyLocation::Left) => "57444", + (NamedKey::Hyper, KeyLocation::Left) => "57445", + (NamedKey::Meta, KeyLocation::Left) => "57446", + (NamedKey::Shift, _) => "57447", + (NamedKey::Control, _) => "57448", + (NamedKey::Alt, _) => "57449", + (NamedKey::Super, _) => "57450", + (NamedKey::Hyper, _) => "57451", + (NamedKey::Meta, _) => "57452", + (NamedKey::CapsLock, _) => "57358", + (NamedKey::NumLock, _) => "57360", + _ => base, + }; + + if base.is_empty() { + None + } else { + Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty)) + } + } +} + +pub struct SequenceBase { + /// The base of the payload, which is the `number` and optionally an alt base from the kitty + /// spec. + payload: Cow<'static, str>, + terminator: SequenceTerminator, +} + +impl SequenceBase { + fn new(payload: Cow<'static, str>, terminator: SequenceTerminator) -> Self { + Self { payload, terminator } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum SequenceTerminator { + /// The normal key esc sequence terminator defined by xterm/dec. + Normal(char), + /// The terminator is for kitty escape sequence. + Kitty, +} + +impl SequenceTerminator { + fn encode_esc_sequence(self) -> char { + match self { + SequenceTerminator::Normal(char) => char, + SequenceTerminator::Kitty => 'u', + } + } +} + +bitflags::bitflags! { + /// The modifiers encoding for escape sequence. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct SequenceModifiers : u8 { + const SHIFT = 0b0000_0001; + const ALT = 0b0000_0010; + const CONTROL = 0b0000_0100; + const SUPER = 0b0000_1000; + // NOTE: Kitty protocol defines additional modifiers to what is present here, like + // Capslock, but it's not a modifier as per winit. + } +} + +impl SequenceModifiers { + /// Get the value which should be passed to escape sequence. + pub fn encode_esc_sequence(self) -> u8 { + self.bits() + 1 + } +} + +impl From<ModifiersState> for SequenceModifiers { + fn from(mods: ModifiersState) -> Self { + let mut modifiers = Self::empty(); + modifiers.set(Self::SHIFT, mods.shift_key()); + modifiers.set(Self::ALT, mods.alt_key()); + modifiers.set(Self::CONTROL, mods.control_key()); + modifiers.set(Self::SUPER, mods.super_key()); + modifiers + } +} |