1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 21:32:13 +03:00

term/termwiz: move key encoding to termwiz

This will enable eg: a lua helper function to serialize keycodes to
assist in some key rebinding scenarios (see:
https://github.com/wez/wezterm/pull/1091#issuecomment-910940833 for the
gist of it) but also makes it a bit easier to write unit tests for key
encoding so that situations like those in #892 are potentially less
likely to occur in the future.
This commit is contained in:
Wez Furlong 2021-09-03 11:12:42 -07:00
parent aaa9e3562d
commit c5d1f67853
2 changed files with 350 additions and 329 deletions

View File

@ -1,341 +1,21 @@
use crate::input::*;
use crate::TerminalState;
use crate::{CSI, SS3};
use anyhow::bail;
use std::fmt::Write;
fn encode_modifiers(mods: KeyModifiers) -> u8 {
let mut number = 0;
if mods.contains(KeyModifiers::SHIFT) {
number |= 1;
}
if mods.contains(KeyModifiers::ALT) {
number |= 2;
}
if mods.contains(KeyModifiers::CTRL) {
number |= 4;
}
number
}
/// characters that when masked for CTRL could be an ascii control character
/// or could be a key that a user legitimately wants to process in their
/// terminal application
fn is_ambiguous_ascii_ctrl(c: char) -> bool {
match c {
'i' | 'I' | 'm' | 'M' | '[' | '{' | '@' => true,
_ => false,
}
}
/// 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
/// some built-in translation from legacy X11 so that are some
/// aliased mappings and a couple that might be technically tied
/// 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.
fn ctrl_mapping(c: char) -> Option<char> {
Some(match c {
'@' | '`' | ' ' | '2' => '\x00',
'A' | 'a' => '\x01',
'B' | 'b' => '\x02',
'C' | 'c' => '\x03',
'D' | 'd' => '\x04',
'E' | 'e' => '\x05',
'F' | 'f' => '\x06',
'G' | 'g' => '\x07',
'H' | 'h' => '\x08',
'I' | 'i' => '\x09',
'J' | 'j' => '\x0a',
'K' | 'k' => '\x0b',
'L' | 'l' => '\x0c',
'M' | 'm' => '\x0d',
'N' | 'n' => '\x0e',
'O' | 'o' => '\x0f',
'P' | 'p' => '\x10',
'Q' | 'q' => '\x11',
'R' | 'r' => '\x12',
'S' | 's' => '\x13',
'T' | 't' => '\x14',
'U' | 'u' => '\x15',
'V' | 'v' => '\x16',
'W' | 'w' => '\x17',
'X' | 'x' => '\x18',
'Y' | 'y' => '\x19',
'Z' | 'z' => '\x1a',
'[' | '3' | '{' => '\x1b',
'\\' | '4' | '|' => '\x1c',
']' | '5' | '}' => '\x1d',
'^' | '6' | '~' => '\x1e',
'_' | '7' | '/' => '\x1f',
'8' | '?' => '\x7f', // `Delete`
_ => return None,
})
}
use termwiz::input::KeyCodeEncodeModes;
impl TerminalState {
fn csi_u_encode(&self, buf: &mut String, c: char, mods: KeyModifiers) -> anyhow::Result<()> {
if self.config.enable_csi_u_key_encoding() {
write!(buf, "\x1b[{};{}u", c as u32, 1 + encode_modifiers(mods))?;
} else {
let c = if mods.contains(KeyModifiers::CTRL) && ctrl_mapping(c).is_some() {
ctrl_mapping(c).unwrap()
} else {
c
};
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}", c)?;
}
Ok(())
}
/// Processes a key_down event generated by the gui/render layer
/// that is embedding the Terminal. This method translates the
/// keycode into a sequence of bytes to send to the slave end
/// of the pty via the `Write`-able object provided by the caller.
#[allow(clippy::cognitive_complexity)]
pub fn key_down(&mut self, key: KeyCode, mods: KeyModifiers) -> anyhow::Result<()> {
use crate::KeyCode::*;
let key = key.normalize_shift_to_upper_case(mods);
// Normalize the modifier state for Char's that are uppercase; remove
// the SHIFT modifier so that reduce ambiguity below
let mods = match key {
Char(c)
if (c.is_ascii_punctuation() || c.is_ascii_uppercase())
&& mods.contains(KeyModifiers::SHIFT) =>
{
mods & !KeyModifiers::SHIFT
}
_ => mods,
};
// Normalize Backspace and Delete
let key = match key {
Char('\x7f') => Delete,
Char('\x08') => Backspace,
c => c,
};
let mut buf = String::new();
// TODO: also respect self.application_keypad
let to_send = match key {
Char(c)
if is_ambiguous_ascii_ctrl(c)
&& mods.contains(KeyModifiers::CTRL)
&& self.config.enable_csi_u_key_encoding() =>
{
self.csi_u_encode(&mut buf, c, mods)?;
buf.as_str()
}
Char(c) if c.is_ascii_uppercase() && mods.contains(KeyModifiers::CTRL) => {
self.csi_u_encode(&mut buf, c, mods)?;
buf.as_str()
}
Char(c) if mods.contains(KeyModifiers::CTRL) && ctrl_mapping(c).is_some() => {
let c = ctrl_mapping(c).unwrap();
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
buf.as_str()
}
// When alt is pressed, send escape first to indicate to the peer that
// ALT is pressed. We do this only for ascii alnum characters because
// eg: on macOS generates altgr style glyphs and keeps the ALT key
// in the modifier set. This confuses eg: zsh which then just displays
// <fffffffff> as the input, so we want to avoid that.
Char(c)
if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
&& mods.contains(KeyModifiers::ALT) =>
{
buf.push(0x1b as char);
buf.push(c);
buf.as_str()
}
Enter | Escape | Backspace => {
let c = match key {
Enter => '\r',
Escape => '\x1b',
// Backspace sends the default VERASE which is confusingly
// the DEL ascii codepoint
Backspace => '\x7f',
_ => unreachable!(),
};
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
self.csi_u_encode(&mut buf, c, mods)?;
} else {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
if self.newline_mode && key == Enter {
buf.push(0x0a as char);
}
}
buf.as_str()
}
Tab => {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
let mods = mods & !KeyModifiers::ALT;
if mods == KeyModifiers::CTRL {
buf.push_str("\x1b[9;5u");
} else if mods == KeyModifiers::CTRL | KeyModifiers::SHIFT {
buf.push_str("\x1b[1;5Z");
} else if mods == KeyModifiers::SHIFT {
buf.push_str("\x1b[Z");
} else {
buf.push('\t');
}
buf.as_str()
}
Char(c) => {
if mods.is_empty() {
buf.push(c);
} else {
self.csi_u_encode(&mut buf, c, mods)?;
}
buf.as_str()
}
Home
| End
| UpArrow
| DownArrow
| RightArrow
| LeftArrow
| ApplicationUpArrow
| ApplicationDownArrow
| ApplicationRightArrow
| ApplicationLeftArrow => {
let (force_app, c) = match key {
UpArrow => (false, 'A'),
DownArrow => (false, 'B'),
RightArrow => (false, 'C'),
LeftArrow => (false, 'D'),
Home => (false, 'H'),
End => (false, 'F'),
ApplicationUpArrow => (true, 'A'),
ApplicationDownArrow => (true, 'B'),
ApplicationRightArrow => (true, 'C'),
ApplicationLeftArrow => (true, 'D'),
_ => unreachable!(),
};
let csi_or_ss3 = if force_app
|| (
self.application_cursor_keys
// Strict reading of DECCKM suggests that application_cursor_keys
// only applies when DECANM and DECKPAM are active, but that seems
// to break unmodified cursor keys in vim
/* && self.dec_ansi_mode && self.application_keypad */
) {
// Use SS3 in application mode
SS3
} else {
// otherwise use regular CSI
CSI
};
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
write!(buf, "{}1;{}{}", CSI, 1 + encode_modifiers(mods), c)?;
} else {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}{}", csi_or_ss3, c)?;
}
buf.as_str()
}
PageUp | PageDown | Insert | Delete => {
let c = match key {
Insert => 2,
Delete => 3,
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
write!(buf, "\x1b[{};{}~", c, 1 + encode_modifiers(mods))?;
} else {
if mods.contains(KeyModifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "\x1b[{}~", c)?;
}
buf.as_str()
}
Function(n) => {
if mods.is_empty() && n < 5 {
// F1-F4 are encoded using SS3 if there are no modifiers
match n {
1 => "\x1bOP",
2 => "\x1bOQ",
3 => "\x1bOR",
4 => "\x1bOS",
_ => unreachable!("wat?"),
}
} else {
// Higher numbered F-keys plus modified F-keys are encoded
// using CSI instead of SS3.
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",
_ => bail!("unhandled fkey number {}", n),
};
let encoded_mods = encode_modifiers(mods);
if encoded_mods == 0 {
// If no modifiers are held, don't send the modifier
// sequence, as the modifier encoding is a CSI-u extension.
write!(buf, "{}~", intro)?;
} else {
write!(buf, "{};{}~", intro, 1 + encoded_mods)?;
}
buf.as_str()
}
}
// TODO: emit numpad sequences
Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6 | Numpad7
| Numpad8 | Numpad9 | Multiply | Add | Separator | Subtract | Decimal | Divide => "",
// Modifier keys pressed on their own don't expand to anything
Control | LeftControl | RightControl | Alt | LeftAlt | RightAlt | Menu | LeftMenu
| RightMenu | Super | Hyper | Shift | LeftShift | RightShift | Meta | LeftWindows
| RightWindows | NumLock | ScrollLock => "",
Cancel | Clear | Pause | CapsLock | Select | Print | PrintScreen | Execute | Help
| Applications | Sleep | BrowserBack | BrowserForward | BrowserRefresh
| BrowserStop | BrowserSearch | BrowserFavorites | BrowserHome | VolumeMute
| VolumeDown | VolumeUp | MediaNextTrack | MediaPrevTrack | MediaStop
| MediaPlayPause | InternalPasteStart | InternalPasteEnd => "",
};
let to_send = key.encode(
mods,
KeyCodeEncodeModes {
enable_csi_u_key_encoding: self.config.enable_csi_u_key_encoding(),
newline_mode: self.newline_mode,
application_cursor_keys: self.application_cursor_keys,
},
)?;
// debug!("sending {:?}, {:?}", to_send, key);
self.writer.write_all(to_send.as_bytes())?;

View File

@ -1,5 +1,7 @@
//! This module provides an InputParser struct to help with parsing
//! input received from a terminal.
use crate::bail;
use crate::error::Result;
use crate::escape::csi::MouseReport;
use crate::escape::parser::Parser;
use crate::escape::{Action, CSI};
@ -8,6 +10,19 @@ use crate::readbuf::ReadBuffer;
use bitflags::bitflags;
#[cfg(feature = "use_serde")]
use serde::{Deserialize, Serialize};
use std::fmt::Write;
pub const CSI: &str = "\x1b[";
pub const SS3: &str = "\x1bO";
/// Specifies terminal modes/configuration that can influence how a KeyCode
/// is encoded when being sent to and application via the pty.
#[derive(Debug, Clone, Copy)]
pub struct KeyCodeEncodeModes {
pub enable_csi_u_key_encoding: bool,
pub application_cursor_keys: bool,
pub newline_mode: bool,
}
#[cfg(windows)]
use winapi::um::wincon::{
@ -216,6 +231,332 @@ impl KeyCode {
| Self::RightWindows
)
}
/// Returns the xterm compatible byte sequence that represents this KeyCode
/// and Modifier combination.
pub fn encode(&self, mods: Modifiers, modes: KeyCodeEncodeModes) -> Result<String> {
use KeyCode::*;
let key = self.normalize_shift_to_upper_case(mods);
// Normalize the modifier state for Char's that are uppercase; remove
// the SHIFT modifier so that reduce ambiguity below
let mods = match key {
Char(c)
if (c.is_ascii_punctuation() || c.is_ascii_uppercase())
&& mods.contains(Modifiers::SHIFT) =>
{
mods & !Modifiers::SHIFT
}
_ => mods,
};
// Normalize Backspace and Delete
let key = match key {
Char('\x7f') => Delete,
Char('\x08') => Backspace,
c => c,
};
let mut buf = String::new();
// TODO: also respect self.application_keypad
match key {
Char(c)
if is_ambiguous_ascii_ctrl(c)
&& mods.contains(Modifiers::CTRL)
&& modes.enable_csi_u_key_encoding =>
{
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
Char(c) if c.is_ascii_uppercase() && mods.contains(Modifiers::CTRL) => {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
Char(c) if mods.contains(Modifiers::CTRL) && ctrl_mapping(c).is_some() => {
let c = ctrl_mapping(c).unwrap();
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
}
// When alt is pressed, send escape first to indicate to the peer that
// ALT is pressed. We do this only for ascii alnum characters because
// eg: on macOS generates altgr style glyphs and keeps the ALT key
// in the modifier set. This confuses eg: zsh which then just displays
// <fffffffff> as the input, so we want to avoid that.
Char(c)
if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
&& mods.contains(Modifiers::ALT) =>
{
buf.push(0x1b as char);
buf.push(c);
}
Enter | Escape | Backspace => {
let c = match key {
Enter => '\r',
Escape => '\x1b',
// Backspace sends the default VERASE which is confusingly
// the DEL ascii codepoint
Backspace => '\x7f',
_ => unreachable!(),
};
if mods.contains(Modifiers::SHIFT) || mods.contains(Modifiers::CTRL) {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
} else {
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
buf.push(c);
if modes.newline_mode && key == Enter {
buf.push(0x0a as char);
}
}
}
Tab => {
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
let mods = mods & !Modifiers::ALT;
if mods == Modifiers::CTRL {
buf.push_str("\x1b[9;5u");
} else if mods == Modifiers::CTRL | Modifiers::SHIFT {
buf.push_str("\x1b[1;5Z");
} else if mods == Modifiers::SHIFT {
buf.push_str("\x1b[Z");
} else {
buf.push('\t');
}
}
Char(c) => {
if mods.is_empty() {
buf.push(c);
} else {
csi_u_encode(&mut buf, c, mods, modes.enable_csi_u_key_encoding)?;
}
}
Home
| End
| UpArrow
| DownArrow
| RightArrow
| LeftArrow
| ApplicationUpArrow
| ApplicationDownArrow
| ApplicationRightArrow
| ApplicationLeftArrow => {
let (force_app, c) = match key {
UpArrow => (false, 'A'),
DownArrow => (false, 'B'),
RightArrow => (false, 'C'),
LeftArrow => (false, 'D'),
Home => (false, 'H'),
End => (false, 'F'),
ApplicationUpArrow => (true, 'A'),
ApplicationDownArrow => (true, 'B'),
ApplicationRightArrow => (true, 'C'),
ApplicationLeftArrow => (true, 'D'),
_ => unreachable!(),
};
let csi_or_ss3 = if force_app
|| (
modes.application_cursor_keys
// Strict reading of DECCKM suggests that application_cursor_keys
// only applies when DECANM and DECKPAM are active, but that seems
// to break unmodified cursor keys in vim
/* && self.dec_ansi_mode && self.application_keypad */
) {
// Use SS3 in application mode
SS3
} else {
// otherwise use regular CSI
CSI
};
if mods.contains(Modifiers::SHIFT) || mods.contains(Modifiers::CTRL) {
write!(buf, "{}1;{}{}", CSI, 1 + encode_modifiers(mods), c)?;
} else {
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "{}{}", csi_or_ss3, c)?;
}
}
PageUp | PageDown | Insert | Delete => {
let c = match key {
Insert => 2,
Delete => 3,
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
if mods.contains(Modifiers::SHIFT) || mods.contains(Modifiers::CTRL) {
write!(buf, "\x1b[{};{}~", c, 1 + encode_modifiers(mods))?;
} else {
if mods.contains(Modifiers::ALT) {
buf.push(0x1b as char);
}
write!(buf, "\x1b[{}~", c)?;
}
}
Function(n) => {
if mods.is_empty() && n < 5 {
// F1-F4 are encoded using SS3 if there are no modifiers
match n {
1 => "\x1bOP",
2 => "\x1bOQ",
3 => "\x1bOR",
4 => "\x1bOS",
_ => unreachable!("wat?"),
};
} else {
// Higher numbered F-keys plus modified F-keys are encoded
// using CSI instead of SS3.
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",
_ => bail!("unhandled fkey number {}", n),
};
let encoded_mods = encode_modifiers(mods);
if encoded_mods == 0 {
// If no modifiers are held, don't send the modifier
// sequence, as the modifier encoding is a CSI-u extension.
write!(buf, "{}~", intro)?;
} else {
write!(buf, "{};{}~", intro, 1 + encoded_mods)?;
}
}
}
// TODO: emit numpad sequences
Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6 | Numpad7
| Numpad8 | Numpad9 | Multiply | Add | Separator | Subtract | Decimal | Divide => {}
// Modifier keys pressed on their own don't expand to anything
Control | LeftControl | RightControl | Alt | LeftAlt | RightAlt | Menu | LeftMenu
| RightMenu | Super | Hyper | Shift | LeftShift | RightShift | Meta | LeftWindows
| RightWindows | NumLock | ScrollLock | Cancel | Clear | Pause | CapsLock | Select
| Print | PrintScreen | Execute | Help | Applications | Sleep | BrowserBack
| BrowserForward | BrowserRefresh | BrowserStop | BrowserSearch | BrowserFavorites
| BrowserHome | VolumeMute | VolumeDown | VolumeUp | MediaNextTrack
| MediaPrevTrack | MediaStop | MediaPlayPause | InternalPasteStart
| InternalPasteEnd => {}
};
Ok(buf)
}
}
fn encode_modifiers(mods: Modifiers) -> u8 {
let mut number = 0;
if mods.contains(Modifiers::SHIFT) {
number |= 1;
}
if mods.contains(Modifiers::ALT) {
number |= 2;
}
if mods.contains(Modifiers::CTRL) {
number |= 4;
}
number
}
/// characters that when masked for CTRL could be an ascii control character
/// or could be a key that a user legitimately wants to process in their
/// terminal application
fn is_ambiguous_ascii_ctrl(c: char) -> bool {
match c {
'i' | 'I' | 'm' | 'M' | '[' | '{' | '@' => true,
_ => false,
}
}
/// 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
/// some built-in translation from legacy X11 so that are some
/// aliased mappings and a couple that might be technically tied
/// 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.
fn ctrl_mapping(c: char) -> Option<char> {
Some(match c {
'@' | '`' | ' ' | '2' => '\x00',
'A' | 'a' => '\x01',
'B' | 'b' => '\x02',
'C' | 'c' => '\x03',
'D' | 'd' => '\x04',
'E' | 'e' => '\x05',
'F' | 'f' => '\x06',
'G' | 'g' => '\x07',
'H' | 'h' => '\x08',
'I' | 'i' => '\x09',
'J' | 'j' => '\x0a',
'K' | 'k' => '\x0b',
'L' | 'l' => '\x0c',
'M' | 'm' => '\x0d',
'N' | 'n' => '\x0e',
'O' | 'o' => '\x0f',
'P' | 'p' => '\x10',
'Q' | 'q' => '\x11',
'R' | 'r' => '\x12',
'S' | 's' => '\x13',
'T' | 't' => '\x14',
'U' | 'u' => '\x15',
'V' | 'v' => '\x16',
'W' | 'w' => '\x17',
'X' | 'x' => '\x18',
'Y' | 'y' => '\x19',
'Z' | 'z' => '\x1a',
'[' | '3' | '{' => '\x1b',
'\\' | '4' | '|' => '\x1c',
']' | '5' | '}' => '\x1d',
'^' | '6' | '~' => '\x1e',
'_' | '7' | '/' => '\x1f',
'8' | '?' => '\x7f', // `Delete`
_ => return None,
})
}
fn csi_u_encode(
buf: &mut String,
c: char,
mods: Modifiers,
enable_csi_u_key_encoding: bool,
) -> Result<()> {
if enable_csi_u_key_encoding {
write!(buf, "\x1b[{};{}u", c as u32, 1 + encode_modifiers(mods))?;
} else {
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(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]