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 { ) } +// 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 { let mut bindings = bindings!( KeyBinding; @@ -439,56 +446,28 @@ pub fn default_key_bindings() -> Vec { 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 { 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 { "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 { 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 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(deserializer: D) -> Result 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 { - 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(), + scrolling_history: self.scrolling.history() as usize, + vi_mode_cursor_style: self.cursor.vi_mode_style(), + 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> Processor { + /// 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 { + 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 { + 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 { + 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 { + 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 { + 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 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 + } +} diff --git a/alacritty/src/input.rs b/alacritty/src/input/mod.rs similarity index 91% rename from alacritty/src/input.rs rename to alacritty/src/input/mod.rs index 2c853488..584b8240 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input/mod.rs @@ -17,16 +17,12 @@ use std::time::{Duration, Instant}; use log::debug; use winit::dpi::PhysicalPosition; use winit::event::{ - ElementState, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, - TouchPhase, + ElementState, Modifiers, MouseButton, MouseScrollDelta, Touch as TouchEvent, TouchPhase, }; use winit::event_loop::EventLoopWindowTarget; +use winit::keyboard::ModifiersState; #[cfg(target_os = "macos")] -use winit::keyboard::ModifiersKeyState; -use winit::keyboard::{Key, ModifiersState}; -#[cfg(target_os = "macos")] -use winit::platform::macos::{EventLoopWindowTargetExtMacOS, OptionAsAlt}; -use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; +use winit::platform::macos::EventLoopWindowTargetExtMacOS; use winit::window::CursorIcon; use alacritty_terminal::event::EventListener; @@ -39,19 +35,18 @@ use alacritty_terminal::vi_mode::ViMotion; use alacritty_terminal::vte::ansi::{ClearMode, Handler}; use crate::clipboard::Clipboard; -use crate::config::{ - Action, BindingKey, BindingMode, MouseAction, SearchAction, UiConfig, ViAction, -}; +use crate::config::{Action, BindingMode, MouseAction, SearchAction, UiConfig, ViAction}; use crate::display::hint::HintMatch; use crate::display::window::Window; use crate::display::{Display, SizeInfo}; use crate::event::{ ClickState, Event, EventType, InlineSearchState, Mouse, TouchPurpose, TouchZoom, - TYPING_SEARCH_DELAY, }; use crate::message_bar::{self, Message}; use crate::scheduler::{Scheduler, TimerId, Topic}; +pub mod keyboard; + /// Font size change interval. pub const FONT_SIZE_STEP: f32 = 0.5; @@ -1021,132 +1016,6 @@ impl> Processor { } } - /// 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 key.state == ElementState::Released || self.ctx.display().ime.preedit().is_some() { - 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. - 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); - } - } - - // 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 self.ctx.terminal().mode().contains(TermMode::VI) || text.is_empty() { - return; - } - - self.ctx.on_terminal_input_start(); - - 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()); - - self.ctx.write_to_pty(bytes); - } - - /// 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 }, - }; - - 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) - } - /// Check mouse icon state in relation to the message bar. fn message_bar_cursor_state(&self) -> Option { // Since search is above the message bar, the button is offset by search's height. diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index eeecc50d..2553270c 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -21,8 +21,9 @@ use crate::term::cell::{Cell, Flags, LineLength}; use crate::term::color::Colors; use crate::vi_mode::{ViModeCursor, ViMotion}; use crate::vte::ansi::{ - self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, Hyperlink, NamedColor, - NamedMode, NamedPrivateMode, PrivateMode, Rgb, StandardCharset, + self, Attr, CharsetIndex, Color, CursorShape, CursorStyle, Handler, Hyperlink, KeyboardModes, + KeyboardModesApplyBehavior, NamedColor, NamedMode, NamedPrivateMode, PrivateMode, Rgb, + StandardCharset, }; pub mod cell; @@ -43,33 +44,69 @@ const TITLE_STACK_MAX_DEPTH: usize = 4096; /// Default semantic escape characters. pub const SEMANTIC_ESCAPE_CHARS: &str = ",│`|:\"' ()[]{}<>\t"; +/// Max size of the keyboard modes. +const KEYBOARD_MODE_STACK_MAX_DEPTH: usize = TITLE_STACK_MAX_DEPTH; + /// Default tab interval, corresponding to terminfo `it` value. const INITIAL_TABSTOPS: usize = 8; bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct TermMode: u32 { - const NONE = 0; - const SHOW_CURSOR = 0b0000_0000_0000_0000_0001; - const APP_CURSOR = 0b0000_0000_0000_0000_0010; - const APP_KEYPAD = 0b0000_0000_0000_0000_0100; - const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000; - const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000; - const SGR_MOUSE = 0b0000_0000_0000_0010_0000; - const MOUSE_MOTION = 0b0000_0000_0000_0100_0000; - const LINE_WRAP = 0b0000_0000_0000_1000_0000; - const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000; - const ORIGIN = 0b0000_0000_0010_0000_0000; - const INSERT = 0b0000_0000_0100_0000_0000; - const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000; - const ALT_SCREEN = 0b0000_0001_0000_0000_0000; - const MOUSE_DRAG = 0b0000_0010_0000_0000_0000; - const MOUSE_MODE = 0b0000_0010_0000_0100_1000; - const UTF8_MOUSE = 0b0000_0100_0000_0000_0000; - const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000; - const VI = 0b0001_0000_0000_0000_0000; - const URGENCY_HINTS = 0b0010_0000_0000_0000_0000; - const ANY = u32::MAX; + const NONE = 0; + const SHOW_CURSOR = 0b0000_0000_0000_0000_0000_0001; + const APP_CURSOR = 0b0000_0000_0000_0000_0000_0010; + const APP_KEYPAD = 0b0000_0000_0000_0000_0000_0100; + const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_0000_1000; + const BRACKETED_PASTE = 0b0000_0000_0000_0000_0001_0000; + const SGR_MOUSE = 0b0000_0000_0000_0000_0010_0000; + const MOUSE_MOTION = 0b0000_0000_0000_0000_0100_0000; + const LINE_WRAP = 0b0000_0000_0000_0000_1000_0000; + const LINE_FEED_NEW_LINE = 0b0000_0000_0000_0001_0000_0000; + const ORIGIN = 0b0000_0000_0000_0010_0000_0000; + const INSERT = 0b0000_0000_0000_0100_0000_0000; + const FOCUS_IN_OUT = 0b0000_0000_0000_1000_0000_0000; + const ALT_SCREEN = 0b0000_0000_0001_0000_0000_0000; + const MOUSE_DRAG = 0b0000_0000_0010_0000_0000_0000; + const MOUSE_MODE = 0b0000_0000_0010_0000_0100_1000; + const UTF8_MOUSE = 0b0000_0000_0100_0000_0000_0000; + const ALTERNATE_SCROLL = 0b0000_0000_1000_0000_0000_0000; + const VI = 0b0000_0001_0000_0000_0000_0000; + const URGENCY_HINTS = 0b0000_0010_0000_0000_0000_0000; + const DISAMBIGUATE_ESC_CODES = 0b0000_0100_0000_0000_0000_0000; + const REPORT_EVENT_TYPES = 0b0000_1000_0000_0000_0000_0000; + const REPORT_ALTERNATE_KEYS = 0b0001_0000_0000_0000_0000_0000; + const REPORT_ALL_KEYS_AS_ESC = 0b0010_0000_0000_0000_0000_0000; + const REPORT_ASSOCIATED_TEXT = 0b0100_0000_0000_0000_0000_0000; + const KITTY_KEYBOARD_PROTOCOL = Self::DISAMBIGUATE_ESC_CODES.bits() + | Self::REPORT_EVENT_TYPES.bits() + | Self::REPORT_ALTERNATE_KEYS.bits() + | Self::REPORT_ALL_KEYS_AS_ESC.bits() + | Self::REPORT_ASSOCIATED_TEXT.bits(); + const ANY = u32::MAX; + } +} + +impl From for TermMode { + fn from(value: KeyboardModes) -> Self { + let mut mode = Self::empty(); + + let disambiguate_esc_codes = value.contains(KeyboardModes::DISAMBIGUATE_ESC_CODES); + mode.set(TermMode::DISAMBIGUATE_ESC_CODES, disambiguate_esc_codes); + + let report_event_types = value.contains(KeyboardModes::REPORT_EVENT_TYPES); + mode.set(TermMode::REPORT_EVENT_TYPES, report_event_types); + + let report_alternate_keys = value.contains(KeyboardModes::REPORT_ALTERNATE_KEYS); + mode.set(TermMode::REPORT_ALTERNATE_KEYS, report_alternate_keys); + + let report_all_keys_as_esc = value.contains(KeyboardModes::REPORT_ALL_KEYS_AS_ESC); + mode.set(TermMode::REPORT_ALL_KEYS_AS_ESC, report_all_keys_as_esc); + + let report_associated_text = value.contains(KeyboardModes::REPORT_ASSOCIATED_TEXT); + mode.set(TermMode::REPORT_ASSOCIATED_TEXT, report_associated_text); + + mode } } @@ -279,6 +316,12 @@ pub struct Term { /// term is set. title_stack: Vec>, + /// The stack for the keyboard modes. + keyboard_mode_stack: Vec, + + /// Currently inactive keyboard mode stack. + inactive_keyboard_mode_stack: Vec, + /// Information about damaged cells. damage: TermDamageState, @@ -303,6 +346,9 @@ pub struct Config { /// The default value is [`SEMANTIC_ESCAPE_CHARS`]. pub semantic_escape_chars: String, + /// Whether to enable kitty keyboard protocol. + pub kitty_keyboard: bool, + /// OSC52 support mode. pub osc52: Osc52, } @@ -314,6 +360,7 @@ impl Default for Config { semantic_escape_chars: SEMANTIC_ESCAPE_CHARS.to_owned(), default_cursor_style: Default::default(), vi_mode_cursor_style: Default::default(), + kitty_keyboard: Default::default(), osc52: Default::default(), } } @@ -388,7 +435,9 @@ impl Term { event_proxy, is_focused: true, title: None, - title_stack: Vec::new(), + title_stack: Default::default(), + keyboard_mode_stack: Default::default(), + inactive_keyboard_mode_stack: Default::default(), selection: None, damage, config: options, @@ -451,7 +500,7 @@ impl Term { where T: EventListener, { - self.config = options; + let old_config = mem::replace(&mut self.config, options); let title_event = match &self.title { Some(title) => Event::Title(title.clone()), @@ -466,6 +515,12 @@ impl Term { self.grid.update_history(self.config.scrolling_history); } + if self.config.kitty_keyboard != old_config.kitty_keyboard { + self.keyboard_mode_stack = Vec::new(); + self.inactive_keyboard_mode_stack = Vec::new(); + self.mode.remove(TermMode::KITTY_KEYBOARD_PROTOCOL); + } + // Damage everything on config updates. self.mark_fully_damaged(); } @@ -668,6 +723,11 @@ impl Term { self.inactive_grid.reset_region(..); } + mem::swap(&mut self.keyboard_mode_stack, &mut self.inactive_keyboard_mode_stack); + let keyboard_mode = + self.keyboard_mode_stack.last().copied().unwrap_or(KeyboardModes::NO_MODE).into(); + self.set_keyboard_mode(keyboard_mode, KeyboardModesApplyBehavior::Replace); + mem::swap(&mut self.grid, &mut self.inactive_grid); self.mode ^= TermMode::ALT_SCREEN; self.selection = None; @@ -959,6 +1019,19 @@ impl Term { Point::new(self.grid.cursor.point.line.0 as usize, self.grid.cursor.point.column); self.damage.damage_point(point); } + + #[inline] + fn set_keyboard_mode(&mut self, mode: TermMode, apply: KeyboardModesApplyBehavior) { + let active_mode = self.mode & TermMode::KITTY_KEYBOARD_PROTOCOL; + self.mode &= !TermMode::KITTY_KEYBOARD_PROTOCOL; + let new_mode = match apply { + KeyboardModesApplyBehavior::Replace => mode, + KeyboardModesApplyBehavior::Union => active_mode.union(mode), + KeyboardModesApplyBehavior::Difference => active_mode.difference(mode), + }; + trace!("Setting keyboard mode to {new_mode:?}"); + self.mode |= new_mode; + } } impl Dimensions for Term { @@ -1193,6 +1266,63 @@ impl Handler for Term { } } + #[inline] + fn report_keyboard_mode(&mut self) { + if !self.config.kitty_keyboard { + return; + } + + trace!("Reporting active keyboard mode"); + let current_mode = + self.keyboard_mode_stack.last().unwrap_or(&KeyboardModes::NO_MODE).bits(); + let text = format!("\x1b[?{current_mode}u"); + self.event_proxy.send_event(Event::PtyWrite(text)); + } + + #[inline] + fn push_keyboard_mode(&mut self, mode: KeyboardModes) { + if !self.config.kitty_keyboard { + return; + } + + trace!("Pushing `{mode:?}` keyboard mode into the stack"); + + if self.keyboard_mode_stack.len() >= KEYBOARD_MODE_STACK_MAX_DEPTH { + let removed = self.title_stack.remove(0); + trace!( + "Removing '{:?}' from bottom of keyboard mode stack that exceeds its maximum depth", + removed + ); + } + + self.keyboard_mode_stack.push(mode); + self.set_keyboard_mode(mode.into(), KeyboardModesApplyBehavior::Replace); + } + + #[inline] + fn pop_keyboard_modes(&mut self, to_pop: u16) { + if !self.config.kitty_keyboard { + return; + } + + trace!("Attemting to pop {to_pop} keyboard modes from the stack"); + let new_len = self.keyboard_mode_stack.len().saturating_sub(to_pop as usize); + self.keyboard_mode_stack.truncate(new_len); + + // Reload active mode. + let mode = self.keyboard_mode_stack.last().copied().unwrap_or(KeyboardModes::NO_MODE); + self.set_keyboard_mode(mode.into(), KeyboardModesApplyBehavior::Replace); + } + + #[inline] + fn set_keyboard_mode(&mut self, mode: KeyboardModes, apply: KeyboardModesApplyBehavior) { + if !self.config.kitty_keyboard { + return; + } + + self.set_keyboard_mode(mode.into(), apply); + } + #[inline] fn device_status(&mut self, arg: usize) { trace!("Reporting device status: {}", arg); @@ -1685,6 +1815,8 @@ impl Handler for Term { self.title = None; self.selection = None; self.vi_mode_cursor = Default::default(); + self.keyboard_mode_stack = Default::default(); + self.inactive_keyboard_mode_stack = Default::default(); // Preserve vi mode across resets. self.mode &= TermMode::VI; diff --git a/docs/escape_support.md b/docs/escape_support.md index 1fc1079d..cf03c710 100644 --- a/docs/escape_support.md +++ b/docs/escape_support.md @@ -81,6 +81,10 @@ brevity. | `CSI t` | PARTIAL | Only parameters `22` and `23` are supported | | | REJECTED | `1`-`13`, `15`, `19`-`21`, `24` | | `CSI u` | IMPLEMENTED | | +| `CSI ? u` | IMPLEMENTED | | +| `CSI = u` | IMPLEMENTED | | +| `CSI < u` | IMPLEMENTED | | +| `CSI > u` | IMPLEMENTED | | | `CSI X` | IMPLEMENTED | | | `CSI Z` | IMPLEMENTED | |