diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | alacritty/src/config/bindings.rs | 226 | ||||
-rw-r--r-- | alacritty/src/config/ui_config.rs | 10 | ||||
-rw-r--r-- | alacritty/src/input/keyboard.rs | 584 | ||||
-rw-r--r-- | alacritty/src/input/mod.rs (renamed from alacritty/src/input.rs) | 143 | ||||
-rw-r--r-- | alacritty_terminal/src/term/mod.rs | 182 | ||||
-rw-r--r-- | docs/escape_support.md | 4 |
7 files changed, 850 insertions, 300 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 238769ca..e383ebde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `window.blur` config option to request blur for transparent windows - `--option` argument for `alacritty msg create-window` - Support for `DECRQM`/`DECRPM` escape sequences +- Support for kitty's keyboard protocol ### Changed diff --git a/alacritty/src/config/bindings.rs b/alacritty/src/config/bindings.rs index d836c5e6..36f5f521 100644 --- a/alacritty/src/config/bindings.rs +++ b/alacritty/src/config/bindings.rs @@ -7,7 +7,9 @@ use serde::de::{self, Error as SerdeError, MapAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer}; use toml::Value as SerdeValue; use winit::event::MouseButton; -use winit::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey, PhysicalKey}; +use winit::keyboard::{ + Key, KeyCode, KeyLocation as WinitKeyLocation, ModifiersState, NamedKey, PhysicalKey, +}; use winit::platform::scancode::PhysicalKeyExtScancode; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; @@ -413,10 +415,13 @@ macro_rules! trigger { BindingKey::Keycode { key: Key::Character($key.into()), location: $location } }}; (KeyBinding, $key:literal,) => {{ - BindingKey::Keycode { key: Key::Character($key.into()), location: KeyLocation::Standard } + BindingKey::Keycode { key: Key::Character($key.into()), location: KeyLocation::Any } + }}; + (KeyBinding, $key:ident, $location:expr) => {{ + BindingKey::Keycode { key: Key::Named(NamedKey::$key), location: $location } }}; (KeyBinding, $key:ident,) => {{ - BindingKey::Keycode { key: Key::Named(NamedKey::$key), location: KeyLocation::Standard } + BindingKey::Keycode { key: Key::Named(NamedKey::$key), location: KeyLocation::Any } }}; (MouseBinding, $base:ident::$button:ident,) => {{ $base::$button @@ -432,6 +437,8 @@ pub fn default_mouse_bindings() -> Vec<MouseBinding> { ) } +// NOTE: key sequences which are not present here, like F5-F20, PageUp/PageDown codes are +// built on the fly in input/keyboard.rs. pub fn default_key_bindings() -> Vec<KeyBinding> { let mut bindings = bindings!( KeyBinding; @@ -439,56 +446,28 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { Copy, +BindingMode::VI; Action::ClearSelection; Paste, ~BindingMode::VI; Action::Paste; "l", ModifiersState::CONTROL; Action::ClearLogNotice; - "l", ModifiersState::CONTROL, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x0c".into()); - Tab, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[Z".into()); - Backspace, ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b\x7f".into()); - Backspace, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x7f".into()); + "l", ModifiersState::CONTROL; Action::ReceiveChar; Home, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToTop; End, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollToBottom; PageUp, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageUp; PageDown, ModifiersState::SHIFT, ~BindingMode::ALT_SCREEN; Action::ScrollPageDown; - Home, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[1;2H".into()); - End, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[1;2F".into()); - PageUp, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5;2~".into()); - PageDown, ModifiersState::SHIFT, +BindingMode::ALT_SCREEN, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6;2~".into()); + // App cursor mode. Home, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOH".into()); - Home, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[H".into()); End, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOF".into()); - End, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[F".into()); ArrowUp, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOA".into()); - ArrowUp, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[A".into()); ArrowDown, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOB".into()); - ArrowDown, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[B".into()); ArrowRight, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOC".into()); - ArrowRight, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[C".into()); ArrowLeft, +BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOD".into()); - ArrowLeft, ~BindingMode::APP_CURSOR, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[D".into()); - Backspace, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x7f".into()); - Insert, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[2~".into()); - Delete, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[3~".into()); - PageUp, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[5~".into()); - PageDown, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[6~".into()); - F1, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOP".into()); - F2, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOQ".into()); - F3, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOR".into()); - F4, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1bOS".into()); - F5, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[15~".into()); - F6, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[17~".into()); - F7, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[18~".into()); - F8, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[19~".into()); - F9, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[20~".into()); - F10, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[21~".into()); - F11, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[23~".into()); - F12, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[24~".into()); - F13, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[25~".into()); - F14, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[26~".into()); - F15, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[28~".into()); - F16, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[29~".into()); - F17, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[31~".into()); - F18, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[32~".into()); - F19, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[33~".into()); - F20, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc("\x1b[34~".into()); - + // Legacy keys handling which can't be automatically encoded. + F1, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOP".into()); + F2, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOQ".into()); + F3, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOR".into()); + F4, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC, ~BindingMode::DISAMBIGUATE_ESC_CODES; Action::Esc("\x1bOS".into()); + Tab, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b[Z".into()); + Tab, ModifiersState::SHIFT | ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b\x1b[Z".into()); + Backspace, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x7f".into()); + Backspace, ModifiersState::ALT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x1b\x7f".into()); + Backspace, ModifiersState::SHIFT, ~BindingMode::VI, ~BindingMode::SEARCH, ~BindingMode::REPORT_ALL_KEYS_AS_ESC; Action::Esc("\x7f".into()); // Vi mode. Space, ModifiersState::SHIFT | ModifiersState::CONTROL, ~BindingMode::SEARCH; Action::ToggleViMode; Space, ModifiersState::SHIFT | ModifiersState::CONTROL, +BindingMode::VI, ~BindingMode::SEARCH; Action::ScrollToBottom; @@ -557,72 +536,6 @@ pub fn default_key_bindings() -> Vec<KeyBinding> { Enter, ModifiersState::SHIFT, +BindingMode::SEARCH, ~BindingMode::VI; SearchAction::SearchFocusPrevious; ); - // Code Modifiers - // ---------+--------------------------- - // 2 | Shift - // 3 | Alt - // 4 | Shift + Alt - // 5 | Control - // 6 | Shift + Control - // 7 | Alt + Control - // 8 | Shift + Alt + Control - // ---------+--------------------------- - // - // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - let mut modifiers = vec![ - ModifiersState::SHIFT, - ModifiersState::ALT, - ModifiersState::SHIFT | ModifiersState::ALT, - ModifiersState::CONTROL, - ModifiersState::SHIFT | ModifiersState::CONTROL, - ModifiersState::ALT | ModifiersState::CONTROL, - ModifiersState::SHIFT | ModifiersState::ALT | ModifiersState::CONTROL, - ]; - - for (index, mods) in modifiers.drain(..).enumerate() { - let modifiers_code = index + 2; - bindings.extend(bindings!( - KeyBinding; - Delete, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[3;{}~", modifiers_code)); - ArrowUp, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}A", modifiers_code)); - ArrowDown, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}B", modifiers_code)); - ArrowRight, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}C", modifiers_code)); - ArrowLeft, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}D", modifiers_code)); - F1, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}P", modifiers_code)); - F2, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}Q", modifiers_code)); - F3, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}R", modifiers_code)); - F4, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}S", modifiers_code)); - F5, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[15;{}~", modifiers_code)); - F6, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[17;{}~", modifiers_code)); - F7, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[18;{}~", modifiers_code)); - F8, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[19;{}~", modifiers_code)); - F9, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[20;{}~", modifiers_code)); - F10, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[21;{}~", modifiers_code)); - F11, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[23;{}~", modifiers_code)); - F12, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[24;{}~", modifiers_code)); - F13, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[25;{}~", modifiers_code)); - F14, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[26;{}~", modifiers_code)); - F15, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[28;{}~", modifiers_code)); - F16, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[29;{}~", modifiers_code)); - F17, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[31;{}~", modifiers_code)); - F18, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[32;{}~", modifiers_code)); - F19, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[33;{}~", modifiers_code)); - F20, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[34;{}~", modifiers_code)); - )); - - // We're adding the following bindings with `Shift` manually above, so skipping them here. - if modifiers_code != 2 { - bindings.extend(bindings!( - KeyBinding; - Insert, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[2;{}~", modifiers_code)); - PageUp, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[5;{}~", modifiers_code)); - PageDown, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[6;{}~", modifiers_code)); - End, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}F", modifiers_code)); - Home, mods, ~BindingMode::VI, ~BindingMode::SEARCH; Action::Esc(format!("\x1b[1;{}H", modifiers_code)); - )); - } - } - bindings.extend(platform_key_bindings()); bindings @@ -683,7 +596,6 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { "7", ModifiersState::SUPER; Action::SelectTab7; "8", ModifiersState::SUPER; Action::SelectTab8; "9", ModifiersState::SUPER; Action::SelectLastTab; - "0", ModifiersState::SUPER; Action::ResetFontSize; "=", ModifiersState::SUPER; Action::IncreaseFontSize; "+", ModifiersState::SUPER; Action::IncreaseFontSize; @@ -712,12 +624,46 @@ pub fn platform_key_bindings() -> Vec<KeyBinding> { vec![] } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum BindingKey { Scancode(PhysicalKey), Keycode { key: Key, location: KeyLocation }, } +/// Key location for matching bindings. +#[derive(Debug, Clone, Copy, Eq)] +pub enum KeyLocation { + /// The key is in its standard position. + Standard, + /// The key is on the numeric pad. + Numpad, + /// The key could be anywhere on the keyboard. + Any, +} + +impl From<WinitKeyLocation> for KeyLocation { + fn from(value: WinitKeyLocation) -> Self { + match value { + WinitKeyLocation::Standard => KeyLocation::Standard, + WinitKeyLocation::Left => KeyLocation::Any, + WinitKeyLocation::Right => KeyLocation::Any, + WinitKeyLocation::Numpad => KeyLocation::Numpad, + } + } +} + +impl PartialEq for KeyLocation { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (_, KeyLocation::Any) + | (KeyLocation::Any, _) + | (KeyLocation::Standard, KeyLocation::Standard) + | (KeyLocation::Numpad, KeyLocation::Numpad) + ) + } +} + impl<'a> Deserialize<'a> for BindingKey { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -729,23 +675,26 @@ impl<'a> Deserialize<'a> for BindingKey { Err(_) => { let keycode = String::deserialize(value.clone()).map_err(D::Error::custom)?; let (key, location) = if keycode.chars().count() == 1 { - (Key::Character(keycode.to_lowercase().into()), KeyLocation::Standard) + (Key::Character(keycode.to_lowercase().into()), KeyLocation::Any) } else { // Translate legacy winit codes into their modern counterparts. match keycode.as_str() { - "Up" => (Key::Named(NamedKey::ArrowUp), KeyLocation::Standard), - "Back" => (Key::Named(NamedKey::Backspace), KeyLocation::Standard), - "Down" => (Key::Named(NamedKey::ArrowDown), KeyLocation::Standard), - "Left" => (Key::Named(NamedKey::ArrowLeft), KeyLocation::Standard), - "Right" => (Key::Named(NamedKey::ArrowRight), KeyLocation::Standard), - "At" => (Key::Character("@".into()), KeyLocation::Standard), - "Colon" => (Key::Character(":".into()), KeyLocation::Standard), - "Period" => (Key::Character(".".into()), KeyLocation::Standard), + "Back" => (Key::Named(NamedKey::Backspace), KeyLocation::Any), + "Up" => (Key::Named(NamedKey::ArrowUp), KeyLocation::Any), + "Down" => (Key::Named(NamedKey::ArrowDown), KeyLocation::Any), + "Left" => (Key::Named(NamedKey::ArrowLeft), KeyLocation::Any), + "Right" => (Key::Named(NamedKey::ArrowRight), KeyLocation::Any), + "At" => (Key::Character("@".into()), KeyLocation::Any), + "Colon" => (Key::Character(":".into()), KeyLocation::Any), + "Period" => (Key::Character(".".into()), KeyLocation::Any), + "LBracket" => (Key::Character("[".into()), KeyLocation::Any), + "RBracket" => (Key::Character("]".into()), KeyLocation::Any), + "Semicolon" => (Key::Character(";".into()), KeyLocation::Any), + "Backslash" => (Key::Character("\\".into()), KeyLocation::Any), + + // The keys which has alternative on numeric pad. + "Enter" => (Key::Named(NamedKey::Enter), KeyLocation::Standard), "Return" => (Key::Named(NamedKey::Enter), KeyLocation::Standard), - "LBracket" => (Key::Character("[".into()), KeyLocation::Standard), - "RBracket" => (Key::Character("]".into()), KeyLocation::Standard), - "Semicolon" => (Key::Character(";".into()), KeyLocation::Standard), - "Backslash" => (Key::Character("\\".into()), KeyLocation::Standard), "Plus" => (Key::Character("+".into()), KeyLocation::Standard), "Comma" => (Key::Character(",".into()), KeyLocation::Standard), "Slash" => (Key::Character("/".into()), KeyLocation::Standard), @@ -781,13 +730,12 @@ impl<'a> Deserialize<'a> for BindingKey { "Numpad8" => (Key::Character("8".into()), KeyLocation::Numpad), "Numpad9" => (Key::Character("9".into()), KeyLocation::Numpad), "Numpad0" => (Key::Character("0".into()), KeyLocation::Numpad), - _ if keycode.starts_with("Dead") => ( - Key::deserialize(value).map_err(D::Error::custom)?, - KeyLocation::Standard, - ), + _ if keycode.starts_with("Dead") => { + (Key::deserialize(value).map_err(D::Error::custom)?, KeyLocation::Any) + }, _ => ( Key::Named(NamedKey::deserialize(value).map_err(D::Error::custom)?), - KeyLocation::Standard, + KeyLocation::Any, ), } }; @@ -808,11 +756,13 @@ bitflags! { /// Modes available for key bindings. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BindingMode: u8 { - const APP_CURSOR = 0b0000_0001; - const APP_KEYPAD = 0b0000_0010; - const ALT_SCREEN = 0b0000_0100; - const VI = 0b0000_1000; - const SEARCH = 0b0001_0000; + const APP_CURSOR = 0b0000_0001; + const APP_KEYPAD = 0b0000_0010; + const ALT_SCREEN = 0b0000_0100; + const VI = 0b0000_1000; + const SEARCH = 0b0001_0000; + const DISAMBIGUATE_ESC_CODES = 0b0010_0000; + const REPORT_ALL_KEYS_AS_ESC = 0b0100_0000; } } @@ -824,6 +774,14 @@ impl BindingMode { binding_mode.set(BindingMode::ALT_SCREEN, mode.contains(TermMode::ALT_SCREEN)); binding_mode.set(BindingMode::VI, mode.contains(TermMode::VI)); binding_mode.set(BindingMode::SEARCH, search); + binding_mode.set( + BindingMode::DISAMBIGUATE_ESC_CODES, + mode.contains(TermMode::DISAMBIGUATE_ESC_CODES), + ); + binding_mode.set( + BindingMode::REPORT_ALL_KEYS_AS_ESC, + mode.contains(TermMode::REPORT_ALL_KEYS_AS_ESC), + ); binding_mode } } diff --git a/alacritty/src/config/ui_config.rs b/alacritty/src/config/ui_config.rs index a76bbb78..f4f67cb6 100644 --- a/alacritty/src/config/ui_config.rs +++ b/alacritty/src/config/ui_config.rs @@ -10,14 +10,15 @@ use log::{error, warn}; use serde::de::{Error as SerdeError, MapAccess, Visitor}; use serde::{self, Deserialize, Deserializer}; use unicode_width::UnicodeWidthChar; -use winit::keyboard::{Key, KeyLocation, ModifiersState}; +use winit::keyboard::{Key, ModifiersState}; use alacritty_config_derive::{ConfigDeserialize, SerdeReplace}; use alacritty_terminal::term::search::RegexSearch; use crate::config::bell::BellConfig; use crate::config::bindings::{ - self, Action, Binding, BindingKey, KeyBinding, ModeWrapper, ModsWrapper, MouseBinding, + self, Action, Binding, BindingKey, KeyBinding, KeyLocation, ModeWrapper, ModsWrapper, + MouseBinding, }; use crate::config::color::Colors; use crate::config::cursor::Cursor; @@ -152,11 +153,12 @@ impl UiConfig { /// Derive [`TermConfig`] from the config. pub fn term_options(&self) -> TermConfig { TermConfig { + semantic_escape_chars: self.selection.semantic_escape_chars.clone(), scrolling_history: self.scrolling.history() as usize, - default_cursor_style: self.cursor.style(), vi_mode_cursor_style: self.cursor.vi_mode_style(), - semantic_escape_chars: self.selection.semantic_escape_chars.clone(), + default_cursor_style: self.cursor.style(), osc52: self.terminal.osc52.0, + kitty_keyboard: true, } } 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. |