mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 22:01:47 +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::input::*;
|
||||||
use crate::TerminalState;
|
use crate::TerminalState;
|
||||||
use crate::{CSI, SS3};
|
use termwiz::input::KeyCodeEncodeModes;
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TerminalState {
|
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
|
/// Processes a key_down event generated by the gui/render layer
|
||||||
/// that is embedding the Terminal. This method translates the
|
/// that is embedding the Terminal. This method translates the
|
||||||
/// keycode into a sequence of bytes to send to the slave end
|
/// keycode into a sequence of bytes to send to the slave end
|
||||||
/// of the pty via the `Write`-able object provided by the caller.
|
/// 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<()> {
|
pub fn key_down(&mut self, key: KeyCode, mods: KeyModifiers) -> anyhow::Result<()> {
|
||||||
use crate::KeyCode::*;
|
let to_send = key.encode(
|
||||||
|
mods,
|
||||||
let key = key.normalize_shift_to_upper_case(mods);
|
KeyCodeEncodeModes {
|
||||||
// Normalize the modifier state for Char's that are uppercase; remove
|
enable_csi_u_key_encoding: self.config.enable_csi_u_key_encoding(),
|
||||||
// the SHIFT modifier so that reduce ambiguity below
|
newline_mode: self.newline_mode,
|
||||||
let mods = match key {
|
application_cursor_keys: self.application_cursor_keys,
|
||||||
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 => "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// debug!("sending {:?}, {:?}", to_send, key);
|
// debug!("sending {:?}, {:?}", to_send, key);
|
||||||
self.writer.write_all(to_send.as_bytes())?;
|
self.writer.write_all(to_send.as_bytes())?;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! This module provides an InputParser struct to help with parsing
|
//! This module provides an InputParser struct to help with parsing
|
||||||
//! input received from a terminal.
|
//! input received from a terminal.
|
||||||
|
use crate::bail;
|
||||||
|
use crate::error::Result;
|
||||||
use crate::escape::csi::MouseReport;
|
use crate::escape::csi::MouseReport;
|
||||||
use crate::escape::parser::Parser;
|
use crate::escape::parser::Parser;
|
||||||
use crate::escape::{Action, CSI};
|
use crate::escape::{Action, CSI};
|
||||||
@ -8,6 +10,19 @@ use crate::readbuf::ReadBuffer;
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
#[cfg(feature = "use_serde")]
|
#[cfg(feature = "use_serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[cfg(windows)]
|
||||||
use winapi::um::wincon::{
|
use winapi::um::wincon::{
|
||||||
@ -216,6 +231,332 @@ impl KeyCode {
|
|||||||
| Self::RightWindows
|
| 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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
Loading…
Reference in New Issue
Block a user