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.
This commit is contained in:
Kirill Chibisov 2023-12-06 09:26:07 +04:00 committed by GitHub
parent 7c9d9f3b16
commit cb03806e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 851 additions and 301 deletions

View File

@ -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

View File

@ -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
}
}

View File

@ -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,
}
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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> {
@ -1193,6 +1266,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);
@ -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;

View File

@ -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 | |