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:
parent
aaa9e3562d
commit
c5d1f67853
@ -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())?;
|
||||
|
@ -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)]
|
||||
|
Loading…
Reference in New Issue
Block a user