aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/input/keyboard.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty/src/input/keyboard.rs')
-rw-r--r--alacritty/src/input/keyboard.rs584
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
+ }
+}