mirror of
https://github.com/wez/wezterm.git
synced 2024-12-24 13:52:55 +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::Char(c) => KC::Char(*c),
|
||||||
WK::Composed(ref s) => {
|
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());
|
return Key::Composed(s.to_owned());
|
||||||
}
|
}
|
||||||
WK::Function(f) => KC::Function(*f),
|
WK::Function(f) => KC::Function(*f),
|
||||||
|
@ -834,17 +834,70 @@ impl TerminalState {
|
|||||||
// Normalize the modifier state for Char's that are uppercase; remove
|
// Normalize the modifier state for Char's that are uppercase; remove
|
||||||
// the SHIFT modifier so that reduce ambiguity below
|
// the SHIFT modifier so that reduce ambiguity below
|
||||||
let mods = match key {
|
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 & !KeyModifiers::SHIFT
|
||||||
}
|
}
|
||||||
_ => mods,
|
_ => 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();
|
let mut buf = String::new();
|
||||||
|
|
||||||
// TODO: also respect self.application_keypad
|
// 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 {
|
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
|
// 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
|
// ALT is pressed. We do this only for ascii alnum characters because
|
||||||
// eg: on macOS generates altgr style glyphs and keeps the ALT key
|
// eg: on macOS generates altgr style glyphs and keeps the ALT key
|
||||||
@ -858,76 +911,124 @@ impl TerminalState {
|
|||||||
buf.push(c);
|
buf.push(c);
|
||||||
buf.as_str()
|
buf.as_str()
|
||||||
}
|
}
|
||||||
Backspace if mods.contains(KeyModifiers::ALT) => "\x1b\x08",
|
|
||||||
Backspace => "\x08",
|
|
||||||
|
|
||||||
Tab => "\t",
|
Enter | Escape | Backspace | Char('\x08') | Delete | Char('\x7f') => {
|
||||||
Enter => "\r",
|
let c = match key {
|
||||||
Escape => "\x1b",
|
Enter => '\r',
|
||||||
|
Escape => '\x1b',
|
||||||
// Delete
|
Char('\x08') | Backspace => '\x08',
|
||||||
// TODO: application delete key? See https://github.com/wez/wezterm/issues/52
|
Char('\x7f') | Delete => '\x7f',
|
||||||
Char('\x7f') | Delete => "\x7f", // "\x1b[3~"
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
Char(c)
|
if mods.contains(KeyModifiers::SHIFT) || mods.contains(KeyModifiers::CTRL) {
|
||||||
if c <= 0xff as char
|
csi_u_encode(&mut buf, c, mods)?;
|
||||||
&& c > 0x40 as char
|
} else {
|
||||||
&& mods == KeyModifiers::CTRL | KeyModifiers::SHIFT =>
|
if mods.contains(KeyModifiers::ALT) && key != Escape {
|
||||||
{
|
buf.push(0x1b as char);
|
||||||
// If shift is held we have C == 0x43 and want to translate
|
}
|
||||||
// that into 0x03
|
buf.push(c);
|
||||||
buf.push((c as u8 - 0x40) as char);
|
}
|
||||||
buf.as_str()
|
buf.as_str()
|
||||||
}
|
}
|
||||||
Char(c)
|
|
||||||
if c <= 0xff as char && c > 0x60 as char && mods.contains(KeyModifiers::CTRL) =>
|
Tab => {
|
||||||
{
|
if mods.contains(KeyModifiers::ALT) {
|
||||||
// If shift is not held we have C == 0x63 and want to translate
|
buf.push(0x1b as char);
|
||||||
// that into 0x03
|
}
|
||||||
buf.push((c as u8 - 0x60) 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()
|
buf.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
Char(c) => {
|
Char(c) => {
|
||||||
buf.push(c);
|
if mods.is_empty() {
|
||||||
|
buf.push(c);
|
||||||
|
} else {
|
||||||
|
csi_u_encode(&mut buf, c, mods)?;
|
||||||
|
}
|
||||||
buf.as_str()
|
buf.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
UpArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[A",
|
Home
|
||||||
DownArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[B",
|
| End
|
||||||
RightArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[C",
|
| UpArrow
|
||||||
LeftArrow if mods.contains(KeyModifiers::ALT) => "\x1b\x1b[D",
|
| DownArrow
|
||||||
UpArrow if self.application_cursor_keys => "\x1bOA",
|
| RightArrow
|
||||||
DownArrow if self.application_cursor_keys => "\x1bOB",
|
| LeftArrow
|
||||||
RightArrow if self.application_cursor_keys => "\x1bOC",
|
| ApplicationUpArrow
|
||||||
LeftArrow if self.application_cursor_keys => "\x1bOD",
|
| ApplicationDownArrow
|
||||||
UpArrow => "\x1b[A",
|
| ApplicationRightArrow
|
||||||
DownArrow => "\x1b[B",
|
| ApplicationLeftArrow => {
|
||||||
RightArrow => "\x1b[C",
|
let (force_app, c) = match key {
|
||||||
LeftArrow => "\x1b[D",
|
UpArrow => (false, 'A'),
|
||||||
ApplicationUpArrow => "\x1bOA",
|
DownArrow => (false, 'B'),
|
||||||
ApplicationDownArrow => "\x1bOB",
|
RightArrow => (false, 'C'),
|
||||||
ApplicationRightArrow => "\x1bOC",
|
LeftArrow => (false, 'D'),
|
||||||
ApplicationLeftArrow => "\x1bOD",
|
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;
|
let rows = self.screen().physical_rows as i64;
|
||||||
self.scroll_viewport(-rows);
|
self.scroll_viewport(-rows);
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
PageDown if mods.contains(KeyModifiers::SHIFT) => {
|
PageDown if mods == KeyModifiers::SHIFT => {
|
||||||
let rows = self.screen().physical_rows as i64;
|
let rows = self.screen().physical_rows as i64;
|
||||||
self.scroll_viewport(rows);
|
self.scroll_viewport(rows);
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
PageUp => "\x1b[5~",
|
|
||||||
PageDown => "\x1b[6~",
|
|
||||||
|
|
||||||
Home if self.application_cursor_keys => "\x1bOH",
|
PageUp | PageDown | Insert => {
|
||||||
End if self.application_cursor_keys => "\x1bOF",
|
let c = match key {
|
||||||
Home => "\x1b[H",
|
Insert => 2,
|
||||||
End => "\x1b[F",
|
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) => {
|
Function(n) => {
|
||||||
if mods.is_empty() && n < 5 {
|
if mods.is_empty() && n < 5 {
|
||||||
@ -957,7 +1058,7 @@ impl TerminalState {
|
|||||||
12 => "\x1b[24",
|
12 => "\x1b[24",
|
||||||
_ => bail!("unhandled fkey number {}", n),
|
_ => bail!("unhandled fkey number {}", n),
|
||||||
};
|
};
|
||||||
write!(buf, "{};{}~", intro, 1 + mods.bits())?;
|
write!(buf, "{};{}~", intro, 1 + encode_modifiers(mods))?;
|
||||||
buf.as_str()
|
buf.as_str()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,9 +458,21 @@ impl InputParser {
|
|||||||
fn build_basic_key_map() -> KeyMap<InputEvent> {
|
fn build_basic_key_map() -> KeyMap<InputEvent> {
|
||||||
let mut map = KeyMap::new();
|
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' {
|
for alpha in b'A'..=b'Z' {
|
||||||
// Ctrl-[A..=Z] are sent as 1..=26
|
// Ctrl-[A..=Z] are sent as 1..=26
|
||||||
let ctrl = [alpha - 0x40];
|
let ctrl = [alpha & 0x1f];
|
||||||
map.insert(
|
map.insert(
|
||||||
&ctrl,
|
&ctrl,
|
||||||
InputEvent::Key(KeyEvent {
|
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
|
// Common arrow keys
|
||||||
for (keycode, dir) in &[
|
for (keycode, dir) in &[
|
||||||
(KeyCode::UpArrow, b'A'),
|
(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 modifier_combos {
|
||||||
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),
|
|
||||||
] {
|
|
||||||
let key = format!("\x1b[1{}{}", suffix, *dir as char);
|
let key = format!("\x1b[1{}{}", suffix, *dir as char);
|
||||||
map.insert(
|
map.insert(
|
||||||
key,
|
key,
|
||||||
@ -535,6 +553,16 @@ impl InputParser {
|
|||||||
modifiers: Modifiers::NONE,
|
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
|
// Function keys 1-4 with no modifiers encoded using SS3
|
||||||
@ -556,16 +584,7 @@ impl InputParser {
|
|||||||
|
|
||||||
// Function keys with modifiers encoded using CSI
|
// Function keys with modifiers encoded using CSI
|
||||||
for n in 1..=12 {
|
for n in 1..=12 {
|
||||||
for (suffix, modifiers) in &[
|
for (suffix, modifiers) in modifier_combos {
|
||||||
("", 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),
|
|
||||||
] {
|
|
||||||
let key = format!("\x1b[{code}{suffix}~", code = n + 10, suffix = suffix);
|
let key = format!("\x1b[{code}{suffix}~", code = n + 10, suffix = suffix);
|
||||||
map.insert(
|
map.insert(
|
||||||
key,
|
key,
|
||||||
|
@ -625,9 +625,16 @@ impl WindowView {
|
|||||||
// sequences
|
// sequences
|
||||||
extern "C" fn do_command_by_selector(this: &mut Object, _sel: Sel, a_selector: Sel) {
|
extern "C" fn do_command_by_selector(this: &mut Object, _sel: Sel, a_selector: Sel) {
|
||||||
let selector = format!("{:?}", a_selector);
|
let selector = format!("{:?}", a_selector);
|
||||||
|
let mut modifiers = Modifiers::default();
|
||||||
let key = match selector.as_ref() {
|
let key = match selector.as_ref() {
|
||||||
"deleteBackward:" => KeyCode::Char('\x08'),
|
"deleteBackward:" => KeyCode::Char('\x08'),
|
||||||
"deleteForward:" => KeyCode::Char('\x7f'),
|
"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'),
|
"cancelOperation:" => KeyCode::Char('\x1b'),
|
||||||
"insertNewline:" => KeyCode::Char('\r'),
|
"insertNewline:" => KeyCode::Char('\r'),
|
||||||
"insertTab:" => KeyCode::Char('\t'),
|
"insertTab:" => KeyCode::Char('\t'),
|
||||||
@ -648,7 +655,7 @@ impl WindowView {
|
|||||||
let event = KeyEvent {
|
let event = KeyEvent {
|
||||||
key,
|
key,
|
||||||
raw_key: None,
|
raw_key: None,
|
||||||
modifiers: Modifiers::default(),
|
modifiers,
|
||||||
repeat_count: 1,
|
repeat_count: 1,
|
||||||
key_is_down: true,
|
key_is_down: true,
|
||||||
};
|
};
|
||||||
@ -888,6 +895,8 @@ impl WindowView {
|
|||||||
let unmod =
|
let unmod =
|
||||||
if virtual_key == super::keycodes::kVK_Delete && modifiers.contains(Modifiers::ALT) {
|
if virtual_key == super::keycodes::kVK_Delete && modifiers.contains(Modifiers::ALT) {
|
||||||
"\x08"
|
"\x08"
|
||||||
|
} else if virtual_key == super::keycodes::kVK_Tab {
|
||||||
|
"\t"
|
||||||
} else {
|
} else {
|
||||||
unmod
|
unmod
|
||||||
};
|
};
|
||||||
@ -974,11 +983,16 @@ impl WindowView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(key) = key_string_to_key_code(chars) {
|
if let Some(key) = key_string_to_key_code(chars) {
|
||||||
let raw_key = if chars == unmod {
|
let (key, raw_key) = if chars == unmod {
|
||||||
None
|
(key, None)
|
||||||
} else {
|
} else {
|
||||||
key_string_to_key_code(unmod)
|
let raw = key_string_to_key_code(unmod)
|
||||||
.map(|kc| normalize_shifted_unmodified_key(kc, virtual_key))
|
.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 {
|
let event = KeyEvent {
|
||||||
|
Loading…
Reference in New Issue
Block a user