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

Adopt CSI u modifier encoding for keypresses

See http://www.leonerd.org.uk/hacks/fixterms/ for the specification.

Refs: https://github.com/wez/wezterm/issues/63
This commit is contained in:
Wez Furlong 2019-11-16 11:47:45 -08:00
parent 9e3c6a4ef9
commit 6289c08a4e
4 changed files with 219 additions and 78 deletions

View File

@ -436,6 +436,13 @@ impl TermWindow {
WK::Char(c) => KC::Char(*c),
WK::Composed(ref s) => {
let mut chars = s.chars();
if let Some(first_char) = chars.next() {
if chars.next().is_none() {
// Was just a single char after all
return self.win_key_code_to_termwiz_key_code(&WK::Char(first_char));
}
}
return Key::Composed(s.to_owned());
}
WK::Function(f) => KC::Function(*f),

View File

@ -834,17 +834,70 @@ impl TerminalState {
// 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_uppercase() && mods.contains(KeyModifiers::SHIFT) => {
Char(c)
if (c.is_ascii_punctuation() || c.is_ascii_uppercase())
&& mods.contains(KeyModifiers::SHIFT) =>
{
mods & !KeyModifiers::SHIFT
}
_ => mods,
};
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
}
let mut buf = String::new();
// TODO: also respect self.application_keypad
fn csi_u_encode(buf: &mut String, c: char, mods: KeyModifiers) -> Result<(), Error> {
write!(buf, "\x1b[{};{}u", c as u32, 1 + encode_modifiers(mods))?;
Ok(())
}
/// 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,
}
}
let to_send = match key {
Char(c) if is_ambiguous_ascii_ctrl(c) && mods.contains(KeyModifiers::CTRL) => {
csi_u_encode(&mut buf, c, mods)?;
buf.as_str()
}
Char(c) if c.is_ascii_uppercase() && mods.contains(KeyModifiers::CTRL) => {
csi_u_encode(&mut buf, c, mods)?;
buf.as_str()
}
Char(c)
if (c.is_ascii_alphanumeric() || c.is_ascii_punctuation())
&& mods.contains(KeyModifiers::CTRL) =>
{
let c = ((c as u8) & 0x1f) as char;
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
@ -858,76 +911,124 @@ impl TerminalState {
buf.push(c);
buf.as_str()
}
Backspace if mods.contains(KeyModifiers::ALT) => "\x1b\x08",
Backspace => "\x08",
Tab => "\t",
Enter => "\r",
Escape => "\x1b",
// Delete
// TODO: application delete key? See https://github.com/wez/wezterm/issues/52
Char('\x7f') | Delete => "\x7f", // "\x1b[3~"
Char(c)
if c <= 0xff as char
&& c > 0x40 as char
&& mods == KeyModifiers::CTRL | KeyModifiers::SHIFT =>
{
// If shift is held we have C == 0x43 and want to translate
// that into 0x03
buf.push((c as u8 - 0x40) as char);
Enter | Escape | Backspace | Char('\x08') | Delete | Char('\x7f') => {
let c = match key {
Enter => '\r',
Escape => '\x1b',
Char('\x08') | Backspace => '\x08',
Char('\x7f') | Delete => '\x7f',
_ => unreachable!(),
};
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
csi_u_encode(&mut buf, c, mods)?;
} else {
if mods.contains(KeyModifiers::ALT) && key != Escape {
buf.push(0x1b as char);
}
buf.push(c);
}
buf.as_str()
}
Char(c)
if c <= 0xff as char && c > 0x60 as char && mods.contains(KeyModifiers::CTRL) =>
{
// If shift is not held we have C == 0x63 and want to translate
// that into 0x03
buf.push((c as u8 - 0x60) as char);
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) => {
buf.push(c);
if mods.is_empty() {
buf.push(c);
} else {
csi_u_encode(&mut buf, c, mods)?;
}
buf.as_str()
}
UpArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[A",
DownArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[B",
RightArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[C",
LeftArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[D",
UpArrow if self.application_cursor_keys => "\x1bOA",
DownArrow if self.application_cursor_keys => "\x1bOB",
RightArrow if self.application_cursor_keys => "\x1bOC",
LeftArrow if self.application_cursor_keys => "\x1bOD",
UpArrow => "\x1b[A",
DownArrow => "\x1b[B",
RightArrow => "\x1b[C",
LeftArrow => "\x1b[D",
ApplicationUpArrow => "\x1bOA",
ApplicationDownArrow => "\x1bOB",
ApplicationRightArrow => "\x1bOC",
ApplicationLeftArrow => "\x1bOD",
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!(),
};
PageUp if mods.contains(KeyModifiers::SHIFT) => {
let csi_or_ss3 = if force_app || self.application_cursor_keys {
// Use SS3 in application mode
"\x1bO"
} else {
// otherwise use regular CSI
"\x1b["
};
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
write!(buf, "{}1;{}{}", csi_or_ss3, 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 if mods == KeyModifiers::SHIFT => {
let rows = self.screen().physical_rows as i64;
self.scroll_viewport(-rows);
""
}
PageDown if mods.contains(KeyModifiers::SHIFT) => {
PageDown if mods == KeyModifiers::SHIFT => {
let rows = self.screen().physical_rows as i64;
self.scroll_viewport(rows);
""
}
PageUp => "\x1b[5~",
PageDown => "\x1b[6~",
Home if self.application_cursor_keys => "\x1bOH",
End if self.application_cursor_keys => "\x1bOF",
Home => "\x1b[H",
End => "\x1b[F",
PageUp | PageDown | Insert => {
let c = match key {
Insert => 2,
PageUp => 5,
PageDown => 6,
_ => unreachable!(),
};
Insert => "\x1b[2~",
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 {
@ -957,7 +1058,7 @@ impl TerminalState {
12 => "\x1b[24",
_ => bail!("unhandled fkey number {}", n),
};
write!(buf, "{};{}~", intro, 1 + mods.bits())?;
write!(buf, "{};{}~", intro, 1 + encode_modifiers(mods))?;
buf.as_str()
}
}

View File

@ -458,9 +458,21 @@ impl InputParser {
fn build_basic_key_map() -> KeyMap<InputEvent> {
let mut map = KeyMap::new();
let modifier_combos = &[
("", Modifiers::NONE),
(";1", Modifiers::NONE),
(";2", Modifiers::SHIFT),
(";3", Modifiers::ALT),
(";4", Modifiers::ALT | Modifiers::SHIFT),
(";5", Modifiers::CTRL),
(";6", Modifiers::CTRL | Modifiers::SHIFT),
(";7", Modifiers::CTRL | Modifiers::ALT),
(";8", Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT),
];
for alpha in b'A'..=b'Z' {
// Ctrl-[A..=Z] are sent as 1..=26
let ctrl = [alpha - 0x40];
let ctrl = [alpha & 0x1f];
map.insert(
&ctrl,
InputEvent::Key(KeyEvent {
@ -480,6 +492,21 @@ impl InputParser {
);
}
// `CSI u` encodings for the ascii range;
// see http://www.leonerd.org.uk/hacks/fixterms/
for c in 0..=0x7fu8 {
for (suffix, modifiers) in modifier_combos {
let key = format!("\x1b[{}{}u", c, suffix);
map.insert(
key,
InputEvent::Key(KeyEvent {
key: KeyCode::Char(c as char),
modifiers: *modifiers,
}),
);
}
}
// Common arrow keys
for (keycode, dir) in &[
(KeyCode::UpArrow, b'A'),
@ -499,16 +526,7 @@ impl InputParser {
}),
);
// TODO: check compat; this happens to match up to iterm in my setup
for (suffix, modifiers) in &[
(";2", Modifiers::SHIFT),
(";3", Modifiers::ALT),
(";4", Modifiers::ALT | Modifiers::SHIFT),
(";5", Modifiers::CTRL),
(";6", Modifiers::CTRL | Modifiers::SHIFT),
(";7", Modifiers::CTRL | Modifiers::ALT),
(";8", Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT),
] {
for (suffix, modifiers) in modifier_combos {
let key = format!("\x1b[1{}{}", suffix, *dir as char);
map.insert(
key,
@ -535,6 +553,16 @@ impl InputParser {
modifiers: Modifiers::NONE,
}),
);
for (suffix, modifiers) in modifier_combos {
let key = format!("\x1bO1{}{}", suffix, *dir as char);
map.insert(
key,
InputEvent::Key(KeyEvent {
key: *keycode,
modifiers: *modifiers,
}),
);
}
}
// Function keys 1-4 with no modifiers encoded using SS3
@ -556,16 +584,7 @@ impl InputParser {
// Function keys with modifiers encoded using CSI
for n in 1..=12 {
for (suffix, modifiers) in &[
("", Modifiers::NONE),
(";2", Modifiers::SHIFT),
(";3", Modifiers::ALT),
(";4", Modifiers::ALT | Modifiers::SHIFT),
(";5", Modifiers::CTRL),
(";6", Modifiers::CTRL | Modifiers::SHIFT),
(";7", Modifiers::CTRL | Modifiers::ALT),
(";8", Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT),
] {
for (suffix, modifiers) in modifier_combos {
let key = format!("\x1b[{code}{suffix}~", code = n + 10, suffix = suffix);
map.insert(
key,

View File

@ -625,9 +625,16 @@ impl WindowView {
// sequences
extern "C" fn do_command_by_selector(this: &mut Object, _sel: Sel, a_selector: Sel) {
let selector = format!("{:?}", a_selector);
let mut modifiers = Modifiers::default();
let key = match selector.as_ref() {
"deleteBackward:" => KeyCode::Char('\x08'),
"deleteForward:" => KeyCode::Char('\x7f'),
"cancel:" => {
// FIXME: this isn't scalable to various keys
// and we lose eg: SHIFT if that is also pressed at the same time
modifiers = Modifiers::CTRL;
KeyCode::Char('\x1b')
}
"cancelOperation:" => KeyCode::Char('\x1b'),
"insertNewline:" => KeyCode::Char('\r'),
"insertTab:" => KeyCode::Char('\t'),
@ -648,7 +655,7 @@ impl WindowView {
let event = KeyEvent {
key,
raw_key: None,
modifiers: Modifiers::default(),
modifiers,
repeat_count: 1,
key_is_down: true,
};
@ -888,6 +895,8 @@ impl WindowView {
let unmod =
if virtual_key == super::keycodes::kVK_Delete && modifiers.contains(Modifiers::ALT) {
"\x08"
} else if virtual_key == super::keycodes::kVK_Tab {
"\t"
} else {
unmod
};
@ -974,11 +983,16 @@ impl WindowView {
}
if let Some(key) = key_string_to_key_code(chars) {
let raw_key = if chars == unmod {
None
let (key, raw_key) = if chars == unmod {
(key, None)
} else {
key_string_to_key_code(unmod)
.map(|kc| normalize_shifted_unmodified_key(kc, virtual_key))
let raw = key_string_to_key_code(unmod)
.map(|kc| normalize_shifted_unmodified_key(kc, virtual_key));
match (&key, &raw) {
(KeyCode::Char(c), Some(raw)) if c.is_ascii_control() => (raw.clone(), None),
_ => (key, raw),
}
};
let event = KeyEvent {