1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-26 08:25:50 +03:00

kitty keeb: move encoding logic to wezterm-input-types

We need access to the underlying raw/physical key in order
to correctly encode in some modes, so we need the full KeyEvent
struct for that.

Move the encoder up so it sits alongside the win32 input mode
encoder.

This should give us better results for both shifted/unshifted
and the "base layout" (US english) representations of a number
of keys.

Note that this is still not 100% technically correct: the unshifted
keys require knowledge of the keyboard layout that we don't have
at this OS-independent layer.

Right now we're assuming a US layout to unshift punctuation, which
is not right if you're not using that layout.  To resolve that,
more work is needed on each OS to be able to extract that information
and then to store it in the KeyEvent.

refs: https://github.com/wez/wezterm/issues/3479
refs: https://github.com/wez/wezterm/issues/2546
This commit is contained in:
Wez Furlong 2023-04-10 08:22:28 -07:00
parent 54a65a5401
commit e241ea58be
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
6 changed files with 856 additions and 520 deletions

1
Cargo.lock generated
View File

@ -5092,6 +5092,7 @@ dependencies = [
"wezterm-blob-leases",
"wezterm-color-types",
"wezterm-dynamic",
"wezterm-input-types",
"winapi",
]

View File

@ -46,6 +46,7 @@ vtparse = { version="0.6.2", path="../vtparse" }
wezterm-bidi = { path = "../bidi", version="0.2.1" }
wezterm-blob-leases = { path = "../wezterm-blob-leases", version="0.1" }
wezterm-color-types = { path = "../color-types", version="0.2" }
wezterm-input-types = { path = "../wezterm-input-types", version="0.1" }
wezterm-dynamic = { path = "../wezterm-dynamic", version="0.1" }
[features]

View File

@ -52,17 +52,7 @@ fn csi_size() {
assert_eq!(std::mem::size_of::<CSI>(), 32);
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KittyKeyboardFlags: u16 {
const NONE = 0;
const DISAMBIGUATE_ESCAPE_CODES = 1;
const REPORT_EVENT_TYPES = 2;
const REPORT_ALTERNATE_KEYS = 4;
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8;
const REPORT_ASSOCIATED_TEXT = 16;
}
}
pub use wezterm_input_types::KittyKeyboardFlags;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[repr(u16)]

View File

@ -272,257 +272,6 @@ impl KeyCode {
)
}
/// <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions>
fn kitty_function_code(self) -> Option<u32> {
use KeyCode::*;
Some(match self {
Escape => 27,
Enter => 13,
Tab => 9,
Backspace => 127,
CapsLock => 57358,
ScrollLock => 57359,
NumLock => 57360,
PrintScreen => 57361,
Pause => 57362,
Menu => 57363,
Function(n) if n >= 13 && n <= 35 => 57376 + n as u32 - 13,
Numpad0 => 57399,
Numpad1 => 57400,
Numpad2 => 57401,
Numpad3 => 57402,
Numpad4 => 57403,
Numpad5 => 57404,
Numpad6 => 57405,
Numpad7 => 57406,
Numpad8 => 57407,
Numpad9 => 57408,
Decimal => 57409,
Divide => 57410,
Multiply => 57411,
Subtract => 57412,
Add => 57413,
// KeypadEnter => 57414,
// KeypadEquals => 57415,
Separator => 57416,
ApplicationLeftArrow => 57417,
ApplicationRightArrow => 57418,
ApplicationUpArrow => 57419,
ApplicationDownArrow => 57420,
KeyPadHome => 57423,
KeyPadEnd => 57424,
KeyPadBegin => 57427,
KeyPadPageUp => 57421,
KeyPadPageDown => 57422,
Insert => 57425,
// KeypadDelete => 57426,
MediaPlayPause => 57430,
MediaStop => 57432,
MediaNextTrack => 57435,
MediaPrevTrack => 57436,
VolumeDown => 57436,
VolumeUp => 57439,
VolumeMute => 57440,
LeftShift => 57441,
LeftControl => 57442,
LeftAlt => 57443,
LeftWindows => 57444,
RightShift => 57447,
RightControl => 57448,
RightAlt => 57449,
RightWindows => 57450,
_ => return None,
})
}
fn encode_kitty(
&self,
mods: Modifiers,
is_down: bool,
flags: KittyKeyboardFlags,
) -> Result<String> {
use KeyCode::*;
if !flags.contains(KittyKeyboardFlags::REPORT_EVENT_TYPES) && !is_down {
return Ok(String::new());
}
// Normalize
let key = match self {
Char('\r') => Enter,
Char('\t') => Tab,
Char('\x7f') => Delete,
Char('\x08') => Backspace,
c => *c,
};
if mods.is_empty()
&& !flags.contains(KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)
&& is_down
{
// Check for simple text generating keys
match key {
Enter => return Ok("\r".to_string()),
Tab => return Ok("\t".to_string()),
Backspace => return Ok("\x7f".to_string()),
Char(c) => return Ok(c.to_string()),
_ => {}
}
}
let mut modifiers = 0;
if mods.contains(Modifiers::SHIFT) {
modifiers |= 1;
}
if mods.contains(Modifiers::ALT) {
modifiers |= 2;
}
if mods.contains(Modifiers::CTRL) {
modifiers |= 4;
}
if mods.contains(Modifiers::SUPER) {
modifiers |= 8;
}
if mods.contains(Modifiers::CAPS_LOCK) {
modifiers |= 64;
}
if mods.contains(Modifiers::NUM_LOCK) {
modifiers |= 128;
}
modifiers += 1;
let event_type = if flags.contains(KittyKeyboardFlags::REPORT_EVENT_TYPES) && !is_down {
":3"
} else {
""
};
let is_legacy_key = match key {
Char(c) => c.is_ascii_alphanumeric() || c.is_ascii_punctuation(),
_ => false,
};
match key {
Char(shifted_key) => {
let mut use_legacy = false;
if !flags.contains(KittyKeyboardFlags::REPORT_ALTERNATE_KEYS)
&& event_type.is_empty()
&& is_legacy_key
&& !(flags.contains(KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES)
&& (mods.contains(Modifiers::CTRL) || mods.contains(Modifiers::ALT)))
{
use_legacy = true;
}
if use_legacy {
// Legacy text key
let mut output = String::new();
if mods.contains(Modifiers::ALT) {
output.push('\x1b');
}
if mods.contains(Modifiers::CTRL) {
csi_u_encode(
&mut output,
shifted_key.to_ascii_uppercase(),
mods,
&KeyCodeEncodeModes {
encoding: KeyboardEncoding::Xterm,
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
},
)?;
} else {
output.push(shifted_key);
}
return Ok(output);
}
let c = shifted_key.to_ascii_lowercase();
let key_code = if flags.contains(KittyKeyboardFlags::REPORT_ALTERNATE_KEYS)
&& c != shifted_key
{
// Note: we don't have enough information here to know what the base-layout key
// should really be.
let base_layout = c;
format!(
"{}:{}:{}",
(c as u32),
(shifted_key as u32),
(base_layout as u32)
)
} else {
(c as u32).to_string()
};
Ok(format!("\x1b[{key_code};{modifiers}{event_type}u"))
}
LeftArrow | RightArrow | UpArrow | DownArrow | Home | End => {
let c = match key {
UpArrow => 'A',
DownArrow => 'B',
RightArrow => 'C',
LeftArrow => 'D',
Home => 'H',
End => 'F',
_ => unreachable!(),
};
Ok(format!("\x1b[1;{modifiers}{event_type}{c}"))
}
PageUp | PageDown | Insert | Delete => {
let c = match key {
Insert => 2,
Delete => 3,
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
Ok(format!("\x1b[{c};{modifiers}{event_type}~"))
}
Function(n) if n < 13 => {
// The spec says that kitty prefers an SS3 form for F1-F4,
// but then has some variance in the encoding and cites a
// compatibility issue with a cursor position report.
// Since it allows reporting these all unambiguously with
// the same general scheme, that is what we're using here.
let intro = match n {
1 => "\x1b[11",
2 => "\x1b[12",
3 => "\x1b[13",
4 => "\x1b[14",
5 => "\x1b[15",
6 => "\x1b[17",
7 => "\x1b[18",
8 => "\x1b[19",
9 => "\x1b[20",
10 => "\x1b[21",
11 => "\x1b[23",
12 => "\x1b[24",
_ => unreachable!(),
};
Ok(format!("{intro};{modifiers}{event_type}~"))
}
_ => {
if key.is_modifier()
&& !flags.contains(KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)
{
// Don't report bare modifier only key events unless
// we're reporting all keys with escape codes
Ok(String::new())
} else if let Some(code) = key.kitty_function_code() {
Ok(format!("\x1b[{code};{modifiers}{event_type}u"))
} else {
Ok(String::new())
}
}
}
}
/// Returns the byte sequence that represents this KeyCode and Modifier combination,
pub fn encode(
&self,
@ -530,12 +279,6 @@ impl KeyCode {
modes: KeyCodeEncodeModes,
is_down: bool,
) -> Result<String> {
match &modes.encoding {
KeyboardEncoding::Kitty(flags) if *flags != KittyKeyboardFlags::NONE => {
return self.encode_kitty(mods, is_down, *flags);
}
_ => {}
}
if !is_down {
// We only want down events
return Ok(String::new());
@ -2194,151 +1937,6 @@ mod test {
);
}
#[test]
fn encode_issue_3220() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES,
),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Char('o')
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"o".to_string()
);
assert_eq!(
KeyCode::Char('o')
.encode(Modifiers::NONE, mode, false)
.unwrap(),
"\x1b[111;1:3u".to_string()
);
}
#[test]
fn encode_issue_3473() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Function(1)
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\x1b[11;1~".to_string()
);
assert_eq!(
KeyCode::Function(1)
.encode(Modifiers::NONE, mode, false)
.unwrap(),
"\x1b[11;1:3~".to_string()
);
}
#[test]
fn encode_issue_2546() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Char('i')
.encode(Modifiers::ALT | Modifiers::SHIFT, mode, true)
.unwrap(),
"\x1b[105;4u".to_string()
);
assert_eq!(
KeyCode::Char('1')
.encode(Modifiers::ALT | Modifiers::SHIFT, mode, true)
.unwrap(),
"\x1b[49;4u".to_string()
);
}
#[test]
fn encode_issue_3474() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Char('A')
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\u{1b}[97:65:97;1u".to_string()
);
assert_eq!(
KeyCode::Char('A')
.encode(Modifiers::NONE, mode, false)
.unwrap(),
"\u{1b}[97:65:97;1:3u".to_string()
);
}
#[test]
fn encode_issue_3476() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::LeftShift
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\u{1b}[57441;1u".to_string()
);
assert_eq!(
KeyCode::LeftShift
.encode(Modifiers::NONE, mode, false)
.unwrap(),
"\u{1b}[57441;1:3u".to_string()
);
assert_eq!(
KeyCode::LeftControl
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\u{1b}[57442;1u".to_string()
);
assert_eq!(
KeyCode::LeftControl
.encode(Modifiers::NONE, mode, false)
.unwrap(),
"\u{1b}[57442;1:3u".to_string()
);
}
#[test]
fn encode_issue_3478_xterm() {
let mode = KeyCodeEncodeModes {
@ -2374,83 +1972,4 @@ mod test {
"\u{1b}[1;2F".to_string()
);
}
#[test]
fn encode_issue_3478_kitty() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Numpad0
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\u{1b}[57399;1u".to_string()
);
assert_eq!(
KeyCode::Numpad0
.encode(Modifiers::SHIFT, mode, true)
.unwrap(),
"\u{1b}[57399;2u".to_string()
);
assert_eq!(
KeyCode::Numpad1
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\u{1b}[57400;1u".to_string()
);
assert_eq!(
KeyCode::Numpad1
.encode(Modifiers::NONE | Modifiers::SHIFT, mode, true)
.unwrap(),
"\u{1b}[57400;2u".to_string()
);
}
#[test]
fn encode_issue_3315() {
let mode = KeyCodeEncodeModes {
encoding: KeyboardEncoding::Kitty(KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES),
newline_mode: false,
application_cursor_keys: false,
modify_other_keys: None,
};
assert_eq!(
KeyCode::Char('"')
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"\"".to_string()
);
assert_eq!(
KeyCode::Char('"')
.encode(Modifiers::SHIFT, mode, true)
.unwrap(),
"\"".to_string()
);
assert_eq!(
KeyCode::Char('!')
.encode(Modifiers::SHIFT, mode, true)
.unwrap(),
"!".to_string()
);
assert_eq!(
KeyCode::LeftShift
.encode(Modifiers::NONE, mode, true)
.unwrap(),
"".to_string()
);
}
}

View File

@ -256,6 +256,17 @@ impl super::TermWindow {
key.encode_win32_input_mode()
}
fn encode_kitty_input(&self, pane: &Arc<dyn Pane>, key: &KeyEvent) -> Option<String> {
if !self.config.enable_kitty_keyboard {
return None;
}
if let KeyboardEncoding::Kitty(flags) = pane.get_keyboard_encoding() {
Some(key.encode_kitty(flags))
} else {
None
}
}
fn lookup_key(
&mut self,
pane: &Arc<dyn Pane>,
@ -294,6 +305,7 @@ impl super::TermWindow {
leader_mod: Modifiers,
only_key_bindings: OnlyKeyBindings,
is_down: bool,
key_event: Option<&KeyEvent>,
) -> bool {
if is_down && !leader_active {
// Check to see if this key-press is the leader activating
@ -406,23 +418,49 @@ impl super::TermWindow {
if bypass_compose {
if let Key::Code(term_key) = self.win_key_code_to_termwiz_key_code(keycode) {
let tw_raw_modifiers = window_mods_to_termwiz_mods(raw_modifiers);
if self.config.debug_key_events {
log::info!(
"{:?} {:?} -> send to pane {:?} {:?}",
keycode,
raw_modifiers,
term_key,
tw_raw_modifiers
);
}
let res = if is_down {
pane.key_down(term_key, tw_raw_modifiers)
} else {
pane.key_up(term_key, tw_raw_modifiers)
let mut did_encode = false;
if let Some(key_event) = key_event {
if let Some(encoded) = self.encode_win32_input(&pane, &key_event) {
if self.config.debug_key_events {
log::info!("win32: Encoded input as {:?}", encoded);
}
pane.writer()
.write_all(encoded.as_bytes())
.context("sending win32-input-mode encoded data")
.ok();
did_encode = true;
} else if let Some(encoded) = self.encode_kitty_input(&pane, &key_event) {
if self.config.debug_key_events {
log::info!("kitty: Encoded input as {:?}", encoded);
}
pane.writer()
.write_all(encoded.as_bytes())
.context("sending kitty encoded data")
.ok();
did_encode = true;
}
};
if !did_encode {
if self.config.debug_key_events {
log::info!(
"{:?} {:?} -> send to pane {:?} {:?}",
keycode,
raw_modifiers,
term_key,
tw_raw_modifiers
);
}
did_encode = if is_down {
pane.key_down(term_key, tw_raw_modifiers)
} else {
pane.key_up(term_key, tw_raw_modifiers)
}
.is_ok();
};
if res.is_ok() {
if did_encode {
if is_down
&& !keycode.is_modifier()
&& *keycode != KeyCode::CapsLock
@ -492,6 +530,7 @@ impl super::TermWindow {
leader_mod,
OnlyKeyBindings::Yes,
key.key_is_down,
None,
) {
key.set_handled();
return;
@ -512,6 +551,7 @@ impl super::TermWindow {
leader_mod,
OnlyKeyBindings::Yes,
key.key_is_down,
None,
) {
key.set_handled();
return;
@ -532,6 +572,7 @@ impl super::TermWindow {
leader_mod,
OnlyKeyBindings::Yes,
key.key_is_down,
None,
) {
key.set_handled();
}
@ -643,6 +684,7 @@ impl super::TermWindow {
leader_mod,
OnlyKeyBindings::No,
window_key.key_is_down,
Some(&window_key),
) {
return;
}
@ -669,15 +711,6 @@ impl super::TermWindow {
self.key_table_state.did_process_key();
}
if self.config.debug_key_events {
log::info!(
"send to pane {} key={:?} mods={:?}",
if window_key.key_is_down { "DOWN" } else { "UP" },
key,
modifiers
);
}
if let Some(modal) = self.get_modal() {
if window_key.key_is_down {
modal.key_down(key, modifiers, self).ok();
@ -687,15 +720,33 @@ impl super::TermWindow {
let res = if let Some(encoded) = self.encode_win32_input(&pane, &window_key) {
if self.config.debug_key_events {
log::info!("Encoded input as {:?}", encoded);
log::info!("win32: Encoded input as {:?}", encoded);
}
pane.writer()
.write_all(encoded.as_bytes())
.context("sending win32-input-mode encoded data")
} else if window_key.key_is_down {
pane.key_down(key, modifiers)
} else if let Some(encoded) = self.encode_kitty_input(&pane, &window_key) {
if self.config.debug_key_events {
log::info!("kitty: Encoded input as {:?}", encoded);
}
pane.writer()
.write_all(encoded.as_bytes())
.context("sending kitty encoded data")
} else {
pane.key_up(key, modifiers)
if self.config.debug_key_events {
log::info!(
"send to pane {} key={:?} mods={:?}",
if window_key.key_is_down { "DOWN" } else { "UP" },
key,
modifiers
);
}
if window_key.key_is_down {
pane.key_down(key, modifiers)
} else {
pane.key_up(key, modifiers)
}
};
if res.is_ok() {

View File

@ -2,6 +2,7 @@ use bitflags::*;
use serde::*;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use wezterm_dynamic::{FromDynamic, ToDynamic};
@ -1234,6 +1235,145 @@ impl RawKeyEvent {
pub fn set_handled(&self) {
self.handled.set_handled();
}
/// <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#functional-key-definitions>
fn kitty_function_code(&self) -> Option<u32> {
use KeyCode::*;
Some(match self.key {
// Tab => 9,
// Backspace => 127,
// CapsLock => 57358,
// ScrollLock => 57359,
// NumLock => 57360,
// PrintScreen => 57361,
// Pause => 57362,
// Menu => 57363,
Function(n) if n >= 13 && n <= 35 => 57376 + n as u32 - 13,
Numpad(n) => n as u32 + 57399,
Decimal => 57409,
Divide => 57410,
Multiply => 57411,
Subtract => 57412,
Add => 57413,
// KeypadEnter => 57414,
// KeypadEquals => 57415,
Separator => 57416,
ApplicationLeftArrow => 57417,
ApplicationRightArrow => 57418,
ApplicationUpArrow => 57419,
ApplicationDownArrow => 57420,
KeyPadHome => 57423,
KeyPadEnd => 57424,
KeyPadBegin => 57427,
KeyPadPageUp => 57421,
KeyPadPageDown => 57422,
Insert => 57425,
// KeypadDelete => 57426,
MediaPlayPause => 57430,
MediaStop => 57432,
MediaNextTrack => 57435,
MediaPrevTrack => 57436,
VolumeDown => 57436,
VolumeUp => 57439,
VolumeMute => 57440,
LeftShift => 57441,
LeftControl => 57442,
LeftAlt => 57443,
LeftWindows => 57444,
RightShift => 57447,
RightControl => 57448,
RightAlt => 57449,
RightWindows => 57450,
_ => match &self.phys_code {
Some(phys) => {
use PhysKeyCode::*;
match *phys {
Escape => 27,
Return => 13,
Tab => 9,
Backspace => 127,
CapsLock => 57358,
// ScrollLock => 57359,
NumLock => 57360,
// PrintScreen => 57361,
// Pause => 57362,
// Menu => 57363,
F13 => 57376,
F14 => 57377,
F15 => 57378,
F16 => 57379,
F17 => 57380,
F18 => 57381,
F19 => 57382,
F20 => 57383,
/*
F21 => 57384,
F22 => 57385,
F23 => 57386,
F24 => 57387,
F25 => 57388,
F26 => 57389,
F27 => 57390,
F28 => 57391,
F29 => 57392,
F30 => 57393,
F31 => 57394,
F32 => 57395,
F33 => 57396,
F34 => 57397,
*/
Keypad0 => 57399,
Keypad1 => 57400,
Keypad2 => 57401,
Keypad3 => 57402,
Keypad4 => 57403,
Keypad5 => 57404,
Keypad6 => 57405,
Keypad7 => 57406,
Keypad8 => 57407,
Keypad9 => 57408,
KeypadDecimal => 57409,
KeypadDivide => 57410,
KeypadMultiply => 57411,
KeypadSubtract => 57412,
KeypadAdd => 57413,
KeypadEnter => 57414,
KeypadEquals => 57415,
// KeypadSeparator => 57416,
// ApplicationLeftArrow => 57417,
// ApplicationRightArrow => 57418,
// ApplicationUpArrow => 57419,
// ApplicationDownArrow => 57420,
// KeyPadHome => 57423,
// KeyPadEnd => 57424,
// KeyPadBegin => 57427,
// KeyPadPageUp => 57421,
// KeyPadPageDown => 57422,
Insert => 57425,
// KeypadDelete => 57426,
// MediaPlayPause => 57430,
// MediaStop => 57432,
// MediaNextTrack => 57435,
// MediaPrevTrack => 57436,
VolumeDown => 57436,
VolumeUp => 57439,
VolumeMute => 57440,
LeftShift => 57441,
LeftControl => 57442,
LeftAlt => 57443,
LeftWindows => 57444,
RightShift => 57447,
RightControl => 57448,
RightAlt => 57449,
RightWindows => 57450,
_ => return None,
}
}
_ => return None,
},
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -1489,6 +1629,208 @@ impl KeyEvent {
}
}
}
pub fn encode_kitty(&self, flags: KittyKeyboardFlags) -> String {
use KeyCode::*;
if !flags.contains(KittyKeyboardFlags::REPORT_EVENT_TYPES) && !self.key_is_down {
return String::new();
}
if self.modifiers.is_empty()
&& !flags.contains(KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)
&& self.key_is_down
{
// Check for simple text generating keys
match &self.key {
Char(c) => return c.to_string(),
_ => {}
}
}
let mut modifiers = 0;
if self.modifiers.contains(Modifiers::SHIFT) {
modifiers |= 1;
}
if self.modifiers.contains(Modifiers::ALT) {
modifiers |= 2;
}
if self.modifiers.contains(Modifiers::CTRL) {
modifiers |= 4;
}
if self.modifiers.contains(Modifiers::SUPER) {
modifiers |= 8;
}
if self.modifiers.contains(Modifiers::CAPS_LOCK) {
modifiers |= 64;
}
if self.modifiers.contains(Modifiers::NUM_LOCK) {
modifiers |= 128;
}
modifiers += 1;
let event_type =
if flags.contains(KittyKeyboardFlags::REPORT_EVENT_TYPES) && !self.key_is_down {
":3"
} else {
""
};
let is_legacy_key = match &self.key {
Char(c) => c.is_ascii_alphanumeric() || c.is_ascii_punctuation(),
_ => false,
};
match &self.key {
PageUp | PageDown | Insert | Char('\x7f') => {
let c = match &self.key {
Insert => 2,
Char('\x7f') => 3, // Delete
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
format!("\x1b[{c};{modifiers}{event_type}~")
}
Char(shifted_key) => {
let mut use_legacy = false;
if !flags.contains(KittyKeyboardFlags::REPORT_ALTERNATE_KEYS)
&& event_type.is_empty()
&& is_legacy_key
&& !(flags.contains(KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES)
&& (self.modifiers.contains(Modifiers::CTRL)
|| self.modifiers.contains(Modifiers::ALT)))
{
use_legacy = true;
}
if use_legacy {
// Legacy text key
let mut output = String::new();
if self.modifiers.contains(Modifiers::ALT) {
output.push('\x1b');
}
if self.modifiers.contains(Modifiers::CTRL) {
csi_u_encode(
&mut output,
shifted_key.to_ascii_uppercase(),
self.modifiers,
);
} else {
output.push(*shifted_key);
}
return output;
}
// FIXME: ideally we'd get the correct unshifted key from
// the OS based on the current keyboard layout. That needs
// more plumbing, so for now, we're assuming the US layout.
let c = us_layout_unshift(*shifted_key);
let base_layout = self
.raw
.as_ref()
.and_then(|raw| raw.phys_code.as_ref())
.and_then(|phys| match phys.to_key_code() {
KeyCode::Char(base) if base != c => Some(base),
_ => None,
});
let mut key_code = format!("{}", (c as u32));
if flags.contains(KittyKeyboardFlags::REPORT_ALTERNATE_KEYS)
&& (c != *shifted_key || base_layout.is_some())
{
key_code.push(':');
if c != *shifted_key {
key_code.push_str(&format!("{}", (*shifted_key as u32)));
}
if let Some(base) = base_layout {
key_code.push_str(&format!(":{}", (base as u32)));
}
}
format!("\x1b[{key_code};{modifiers}{event_type}u")
}
LeftArrow | RightArrow | UpArrow | DownArrow | Home | End => {
let c = match &self.key {
UpArrow => 'A',
DownArrow => 'B',
RightArrow => 'C',
LeftArrow => 'D',
Home => 'H',
End => 'F',
_ => unreachable!(),
};
format!("\x1b[1;{modifiers}{event_type}{c}")
}
Function(n) if *n < 13 => {
// The spec says that kitty prefers an SS3 form for F1-F4,
// but then has some variance in the encoding and cites a
// compatibility issue with a cursor position report.
// Since it allows reporting these all unambiguously with
// the same general scheme, that is what we're using here.
let intro = match *n {
1 => "\x1b[11",
2 => "\x1b[12",
3 => "\x1b[13",
4 => "\x1b[14",
5 => "\x1b[15",
6 => "\x1b[17",
7 => "\x1b[18",
8 => "\x1b[19",
9 => "\x1b[20",
10 => "\x1b[21",
11 => "\x1b[23",
12 => "\x1b[24",
_ => unreachable!(),
};
format!("{intro};{modifiers}{event_type}~")
}
_ => {
if self.key.is_modifier()
&& !flags.contains(KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES)
{
// Don't report bare modifier only key events unless
// we're reporting all keys with escape codes
String::new()
} else if let Some(code) =
self.raw.as_ref().and_then(|raw| raw.kitty_function_code())
{
format!("\x1b[{code};{modifiers}{event_type}u")
} else {
String::new()
}
}
}
}
}
fn csi_u_encode(buf: &mut String, c: char, mods: Modifiers) {
let c = if mods.contains(Modifiers::CTRL) && ctrl_mapping(c).is_some() {
ctrl_mapping(c).unwrap()
} else {
c
};
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}", c).ok();
}
bitflags::bitflags! {
pub struct KittyKeyboardFlags: u16 {
const NONE = 0;
const DISAMBIGUATE_ESCAPE_CODES = 1;
const REPORT_EVENT_TYPES = 2;
const REPORT_ALTERNATE_KEYS = 4;
const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8;
const REPORT_ASSOCIATED_TEXT = 16;
}
}
bitflags! {
@ -1629,6 +1971,45 @@ impl FromDynamic for IntegratedTitleButtonStyle {
}
}
/// Kitty wants us to report the un-shifted version of a key.
/// It's a PITA to obtain that from the OS-dependent keyboard
/// layout stuff. For the moment, we'll do the slightly gross
/// thing and make an assumption that a US ANSI layout is in
/// use; this function encodes that mapping.
fn us_layout_unshift(c: char) -> char {
match c {
'!' => '1',
'@' => '2',
'#' => '3',
'$' => '4',
'%' => '5',
'^' => '6',
'&' => '7',
'*' => '8',
'(' => '9',
')' => '0',
'_' => '-',
'+' => '=',
'~' => '`',
'{' => '[',
'}' => ']',
'|' => '\\',
':' => ';',
'"' => '\'',
'<' => ',',
'>' => '.',
'?' => '/',
c => {
let s: Vec<char> = c.to_lowercase().collect();
if s.len() == 1 {
s[0]
} else {
c
}
}
}
}
/// Map c to its Ctrl equivalent.
/// In theory, this mapping is simply translating alpha characters
/// to upper case and then masking them by 0x1f, but xterm inherits
@ -1637,7 +2018,6 @@ impl FromDynamic for IntegratedTitleButtonStyle {
/// to US keyboard layout (particularly the punctuation characters
/// produced in combination with SHIFT) that may not be 100%
/// the right thing to do here for users with non-US layouts.
#[cfg(windows)]
fn ctrl_mapping(c: char) -> Option<char> {
// Please also sync with the copy of this function that
// lives in termwiz :-/
@ -1704,3 +2084,397 @@ impl Default for UIKeyCapRendering {
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn encode_issue_3220() {
let flags =
KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags::REPORT_EVENT_TYPES;
assert_eq!(
KeyEvent {
key: KeyCode::Char('o'),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"o".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('o'),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: false,
raw: None
}
.encode_kitty(flags),
"\x1b[111;1:3u".to_string()
);
}
#[test]
fn encode_issue_3473() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
assert_eq!(
KeyEvent {
key: KeyCode::Function(1),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\x1b[11;1~".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Function(1),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: false,
raw: None
}
.encode_kitty(flags),
"\x1b[11;1:3~".to_string()
);
}
#[test]
fn encode_issue_2546() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES;
assert_eq!(
KeyEvent {
key: KeyCode::Char('i'),
modifiers: Modifiers::ALT | Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\x1b[105;4u".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('I'),
modifiers: Modifiers::ALT | Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\x1b[105;4u".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('1'),
modifiers: Modifiers::ALT | Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\x1b[49;4u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Char('!'),
modifiers: Modifiers::ALT | Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
},
Some(PhysKeyCode::K1)
)
.encode_kitty(flags),
"\x1b[49;4u".to_string()
);
}
#[test]
fn encode_issue_3474() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
assert_eq!(
KeyEvent {
key: KeyCode::Char('A'),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\u{1b}[97:65;1u".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('A'),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: false,
raw: None
}
.encode_kitty(flags),
"\u{1b}[97:65;1:3u".to_string()
);
}
fn make_event_with_raw(mut event: KeyEvent, phys: Option<PhysKeyCode>) -> KeyEvent {
let phys = match phys {
Some(phys) => Some(phys),
None => event.key.to_phys(),
};
event.raw = Some(RawKeyEvent {
key: event.key.clone(),
modifiers: event.modifiers,
phys_code: phys,
raw_code: 0,
#[cfg(windows)]
scan_code: 0,
repeat_count: 1,
key_is_down: event.key_is_down,
handled: Handled::new(),
});
event
}
#[test]
fn encode_issue_3476() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::LeftShift,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57441;1u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::LeftShift,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: false,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57441;1:3u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::LeftControl,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57442;1u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::LeftControl,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: false,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57442;1:3u".to_string()
);
}
#[test]
fn encode_issue_3478() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Numpad(0),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57399;1u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Numpad(0),
modifiers: Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57399;2u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Numpad(1),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57400;1u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Numpad(1),
modifiers: Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
},
None
)
.encode_kitty(flags),
"\u{1b}[57400;2u".to_string()
);
}
#[test]
fn encode_issue_3315() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES;
assert_eq!(
KeyEvent {
key: KeyCode::Char('"'),
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\"".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('"'),
modifiers: Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"\"".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::Char('!'),
modifiers: Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"!".to_string()
);
assert_eq!(
KeyEvent {
key: KeyCode::LeftShift,
modifiers: Modifiers::NONE,
repeat_count: 1,
key_is_down: true,
raw: None
}
.encode_kitty(flags),
"".to_string()
);
}
#[test]
fn encode_issue_3479() {
let flags = KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
| KittyKeyboardFlags::REPORT_EVENT_TYPES
| KittyKeyboardFlags::REPORT_ALTERNATE_KEYS
| KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Char('ф'),
modifiers: Modifiers::CTRL,
repeat_count: 1,
key_is_down: true,
raw: None
},
Some(PhysKeyCode::A)
)
.encode_kitty(flags),
"\x1b[1092::97;5u".to_string()
);
assert_eq!(
make_event_with_raw(
KeyEvent {
key: KeyCode::Char('Ф'),
modifiers: Modifiers::CTRL | Modifiers::SHIFT,
repeat_count: 1,
key_is_down: true,
raw: None
},
Some(PhysKeyCode::A)
)
.encode_kitty(flags),
"\x1b[1092:1060:97;6u".to_string()
);
}
}