summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKirill Chibisov <contact@kchibisov.com>2023-12-06 09:26:07 +0400
committerGitHub <noreply@github.com>2023-12-06 09:26:07 +0400
commitcb03806e2ab85674c45e87e1bb24dfe2fd1a918c (patch)
tree3561fc6785281fb3a963c199fe9a12df4007bed7
parent7c9d9f3b166f2aade76d35408b5acb5d3ccd1c94 (diff)
Implement kitty's keyboard protocol
The protocol enables robust key reporting for the applications, so they could bind more keys and the user won't have collisions with the normal control keys. Links: https://sw.kovidgoyal.net/kitty/keyboard-protocol Fixes #6378.
-rw-r--r--CHANGELOG.md1
-rw-r--r--alacritty/src/config/bindings.rs226
-rw-r--r--alacritty/src/config/ui_config.rs10
-rw-r--r--alacritty/src/input/keyboard.rs584
-rw-r--r--alacritty/src/input/mod.rs (renamed from alacritty/src/input.rs)143
-rw-r--r--alacritty_terminal/src/term/mod.rs182
-rw-r--r--docs/escape_support.md4
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