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:
parent
9e3c6a4ef9
commit
6289c08a4e
@ -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),
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user