summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 = character.chars().next().unwrap();
+ let base_character = character.to_lowercase().next().unwrap();
+
+ let codepoint = u32::from(character);
+ let base_codepoint = u32::from(base_character);
+
+ // NOTE: Base layouts are ignored, since winit doesn't expose this information
+ // yet.
+ let payload = if self.mode.contains(TermMode::REPORT_ALTERNATE_KEYS)
+ && codepoint != base_codepoint
+ {
+ format!("{codepoint}:{base_codepoint}")
+ } else {
+ codepoint.to_string()
+ };
+
+ Some(SequenceBase::new(payload.into(), SequenceTerminator::Kitty))
+ } else if self.kitty_encode_all
+ && self.mode.contains(TermMode::REPORT_ASSOCIATED_TEXT)
+ && key.text.is_some()
+ {
+ // Fallback when need to report text, but we don't have any key associated with this
+ // text.
+ Some(SequenceBase::new("0".into(), SequenceTerminator::Kitty))
+ } else {
+ None
+ }
+ }
+
+ /// Try building from numpad key.
+ ///
+ /// `None` is returned when the key is neither known nor numpad.
+ fn try_build_numpad(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ if !self.kitty_seq || key.location != KeyLocation::Numpad {
+ return None;
+ }
+
+ let base = match key.logical_key.as_ref() {
+ Key::Character("0") => "57399",
+ Key::Character("1") => "57400",
+ Key::Character("2") => "57401",
+ Key::Character("3") => "57402",
+ Key::Character("4") => "57403",
+ Key::Character("5") => "57404",
+ Key::Character("6") => "57405",
+ Key::Character("7") => "57406",
+ Key::Character("8") => "57407",
+ Key::Character("9") => "57408",
+ Key::Character(".") => "57409",
+ Key::Character("/") => "57410",
+ Key::Character("*") => "57411",
+ Key::Character("-") => "57412",
+ Key::Character("+") => "57413",
+ Key::Character("=") => "57415",
+ Key::Named(named) => match named {
+ NamedKey::Enter => "57414",
+ NamedKey::ArrowLeft => "57417",
+ NamedKey::ArrowRight => "57418",
+ NamedKey::ArrowUp => "57419",
+ NamedKey::ArrowDown => "57420",
+ NamedKey::PageUp => "57421",
+ NamedKey::PageDown => "57422",
+ NamedKey::Home => "57423",
+ NamedKey::End => "57424",
+ NamedKey::Insert => "57425",
+ NamedKey::Delete => "57426",
+ _ => return None,
+ },
+ _ => return None,
+ };
+
+ Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty))
+ }
+
+ /// Try building from [`NamedKey`].
+ fn try_build_named(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ let named = match key.logical_key {
+ Key::Named(named) => named,
+ _ => return None,
+ };
+
+ // The default parameter is 1, so we can omit it.
+ let one_based = if self.modifiers.is_empty() && !self.kitty_event_type { "" } else { "1" };
+ let (base, terminator) = match named {
+ NamedKey::PageUp => ("5", SequenceTerminator::Normal('~')),
+ NamedKey::PageDown => ("6", SequenceTerminator::Normal('~')),
+ NamedKey::Insert => ("2", SequenceTerminator::Normal('~')),
+ NamedKey::Delete => ("3", SequenceTerminator::Normal('~')),
+ NamedKey::Home => (one_based, SequenceTerminator::Normal('H')),
+ NamedKey::End => (one_based, SequenceTerminator::Normal('F')),
+ NamedKey::ArrowLeft => (one_based, SequenceTerminator::Normal('D')),
+ NamedKey::ArrowRight => (one_based, SequenceTerminator::Normal('C')),
+ NamedKey::ArrowUp => (one_based, SequenceTerminator::Normal('A')),
+ NamedKey::ArrowDown => (one_based, SequenceTerminator::Normal('B')),
+ NamedKey::F1 => (one_based, SequenceTerminator::Normal('P')),
+ NamedKey::F2 => (one_based, SequenceTerminator::Normal('Q')),
+ NamedKey::F3 => {
+ // F3 in kitty protocol diverges from alacritty's terminfo.
+ if self.kitty_seq {
+ ("13", SequenceTerminator::Normal('~'))
+ } else {
+ (one_based, SequenceTerminator::Normal('R'))
+ }
+ },
+ NamedKey::F4 => (one_based, SequenceTerminator::Normal('S')),
+ NamedKey::F5 => ("15", SequenceTerminator::Normal('~')),
+ NamedKey::F6 => ("17", SequenceTerminator::Normal('~')),
+ NamedKey::F7 => ("18", SequenceTerminator::Normal('~')),
+ NamedKey::F8 => ("19", SequenceTerminator::Normal('~')),
+ NamedKey::F9 => ("20", SequenceTerminator::Normal('~')),
+ NamedKey::F10 => ("21", SequenceTerminator::Normal('~')),
+ NamedKey::F11 => ("23", SequenceTerminator::Normal('~')),
+ NamedKey::F12 => ("24", SequenceTerminator::Normal('~')),
+ NamedKey::F13 => ("57376", SequenceTerminator::Kitty),
+ NamedKey::F14 => ("57377", SequenceTerminator::Kitty),
+ NamedKey::F15 => ("57378", SequenceTerminator::Kitty),
+ NamedKey::F16 => ("57379", SequenceTerminator::Kitty),
+ NamedKey::F17 => ("57380", SequenceTerminator::Kitty),
+ NamedKey::F18 => ("57381", SequenceTerminator::Kitty),
+ NamedKey::F19 => ("57382", SequenceTerminator::Kitty),
+ NamedKey::F20 => ("57383", SequenceTerminator::Kitty),
+ NamedKey::F21 => ("57384", SequenceTerminator::Kitty),
+ NamedKey::F22 => ("57385", SequenceTerminator::Kitty),
+ NamedKey::F23 => ("57386", SequenceTerminator::Kitty),
+ NamedKey::F24 => ("57387", SequenceTerminator::Kitty),
+ NamedKey::F25 => ("57388", SequenceTerminator::Kitty),
+ NamedKey::F26 => ("57389", SequenceTerminator::Kitty),
+ NamedKey::F27 => ("57390", SequenceTerminator::Kitty),
+ NamedKey::F28 => ("57391", SequenceTerminator::Kitty),
+ NamedKey::F29 => ("57392", SequenceTerminator::Kitty),
+ NamedKey::F30 => ("57393", SequenceTerminator::Kitty),
+ NamedKey::F31 => ("57394", SequenceTerminator::Kitty),
+ NamedKey::F32 => ("57395", SequenceTerminator::Kitty),
+ NamedKey::F33 => ("57396", SequenceTerminator::Kitty),
+ NamedKey::F34 => ("57397", SequenceTerminator::Kitty),
+ NamedKey::F35 => ("57398", SequenceTerminator::Kitty),
+ NamedKey::ScrollLock => ("57359", SequenceTerminator::Kitty),
+ NamedKey::PrintScreen => ("57361", SequenceTerminator::Kitty),
+ NamedKey::Pause => ("57362", SequenceTerminator::Kitty),
+ NamedKey::ContextMenu => ("57363", SequenceTerminator::Kitty),
+ NamedKey::MediaPlay => ("57428", SequenceTerminator::Kitty),
+ NamedKey::MediaPause => ("57429", SequenceTerminator::Kitty),
+ NamedKey::MediaPlayPause => ("57430", SequenceTerminator::Kitty),
+ NamedKey::MediaStop => ("57432", SequenceTerminator::Kitty),
+ NamedKey::MediaFastForward => ("57433", SequenceTerminator::Kitty),
+ NamedKey::MediaRewind => ("57434", SequenceTerminator::Kitty),
+ NamedKey::MediaTrackNext => ("57435", SequenceTerminator::Kitty),
+ NamedKey::MediaTrackPrevious => ("57436", SequenceTerminator::Kitty),
+ NamedKey::MediaRecord => ("57437", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeDown => ("57438", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeUp => ("57439", SequenceTerminator::Kitty),
+ NamedKey::AudioVolumeMute => ("57440", SequenceTerminator::Kitty),
+ _ => return None,
+ };
+
+ Some(SequenceBase::new(base.into(), terminator))
+ }
+
+ /// Try building escape from control characters (e.g. Enter) and modifiers.
+ fn try_build_control_char_or_mod(&self, key: &KeyEvent) -> Option<SequenceBase> {
+ if !self.kitty_encode_all && !self.kitty_seq {
+ return None;
+ }
+
+ let named = match key.logical_key {
+ Key::Named(named) => named,
+ _ => return None,
+ };
+
+ let base = match named {
+ NamedKey::Tab => "9",
+ NamedKey::Enter => "13",
+ NamedKey::Escape => "27",
+ NamedKey::Space => "32",
+ NamedKey::Backspace => "127",
+ _ => "",
+ };
+
+ // Fail when the key is not a named control character and the active mode prohibits us
+ // from encoding modifier keys.
+ if !self.kitty_encode_all && base.is_empty() {
+ return None;
+ }
+
+ let base = match (named, key.location) {
+ (NamedKey::Shift, KeyLocation::Left) => "57441",
+ (NamedKey::Control, KeyLocation::Left) => "57442",
+ (NamedKey::Alt, KeyLocation::Left) => "57443",
+ (NamedKey::Super, KeyLocation::Left) => "57444",
+ (NamedKey::Hyper, KeyLocation::Left) => "57445",
+ (NamedKey::Meta, KeyLocation::Left) => "57446",
+ (NamedKey::Shift, _) => "57447",
+ (NamedKey::Control, _) => "57448",
+ (NamedKey::Alt, _) => "57449",
+ (NamedKey::Super, _) => "57450",
+ (NamedKey::Hyper, _) => "57451",
+ (NamedKey::Meta, _) => "57452",
+ (NamedKey::CapsLock, _) => "57358",
+ (NamedKey::NumLock, _) => "57360",
+ _ => base,
+ };
+
+ if base.is_empty() {
+ None
+ } else {
+ Some(SequenceBase::new(base.into(), SequenceTerminator::Kitty))
+ }
+ }
+}
+
+pub struct SequenceBase {
+ /// The base of the payload, which is the `number` and optionally an alt base from the kitty
+ /// spec.
+ payload: Cow<'static, str>,
+ terminator: SequenceTerminator,
+}
+
+impl SequenceBase {
+ fn new(payload: Cow<'static, str>, terminator: SequenceTerminator) -> Self {
+ Self { payload, terminator }
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum SequenceTerminator {
+ /// The normal key esc sequence terminator defined by xterm/dec.
+ Normal(char),
+ /// The terminator is for kitty escape sequence.
+ Kitty,
+}
+
+impl SequenceTerminator {
+ fn encode_esc_sequence(self) -> char {
+ match self {
+ SequenceTerminator::Normal(char) => char,
+ SequenceTerminator::Kitty => 'u',
+ }
+ }
+}
+
+bitflags::bitflags! {
+ /// The modifiers encoding for escape sequence.
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ struct SequenceModifiers : u8 {
+ const SHIFT = 0b0000_0001;
+ const ALT = 0b0000_0010;
+ const CONTROL = 0b0000_0100;
+ const SUPER = 0b0000_1000;
+ // NOTE: Kitty protocol defines additional modifiers to what is present here, like
+ // Capslock, but it's not a modifier as per winit.
+ }
+}
+
+impl SequenceModifiers {
+ /// Get the value which should be passed to escape sequence.
+ pub fn encode_esc_sequence(self) -> u8 {
+ self.bits() + 1
+ }
+}
+
+impl From<ModifiersState> for SequenceModifiers {
+ fn from(mods: ModifiersState) -> Self {
+ let mut modifiers = Self::empty();
+ modifiers.set(Self::SHIFT, mods.shift_key());
+ modifiers.set(Self::ALT, mods.alt_key());
+ modifiers.set(Self::CONTROL, mods.control_key());
+ modifiers.set(Self::SUPER, mods.super_key());
+ modifiers
+ }
+}
diff --git a/alacritty/src/input.rs b/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<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 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<CursorIcon> {
// 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<KeyboardModes> 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<T> {
/// term is set.
title_stack: Vec<Option<String>>,
+ /// The stack for the keyboard modes.
+ keyboard_mode_stack: Vec<KeyboardModes>,
+
+ /// Currently inactive keyboard mode stack.
+ inactive_keyboard_mode_stack: Vec<KeyboardModes>,
+
/// 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<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
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<T> Term<T> {
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<T> Dimensions for Term<T> {
@@ -1194,6 +1267,63 @@ impl<T: EventListener> Handler for Term<T> {
}
#[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);
match arg {
@@ -1685,6 +1815,8 @@ impl<T: EventListener> Handler for Term<T> {
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 | |