367 lines
11 KiB
Rust
367 lines
11 KiB
Rust
use crate::cell::CellCoord;
|
|
use crate::protocol::{Key, Modifiers, MouseButton, MouseKind};
|
|
|
|
use super::screen::{MouseTrackingMode, TerminalModes};
|
|
|
|
/// Encode one normalized key press for the child terminal.
|
|
///
|
|
/// Lock/media/unknown keys return `None`. Application-keypad mode is
|
|
/// intentionally not applied to `Key::Char` digits because the normalized
|
|
/// protocol cannot distinguish number-row and keypad input.
|
|
#[must_use]
|
|
pub fn encode_key(key: Key, mods: Modifiers, modes: TerminalModes) -> Option<Vec<u8>> {
|
|
if mods.contains(Modifiers::META) || mods.contains(Modifiers::HYPER) {
|
|
return None;
|
|
}
|
|
let alt = mods.contains(Modifiers::ALT);
|
|
let ctrl = mods.contains(Modifiers::CTRL);
|
|
let mut out = match key {
|
|
Key::Char(ch) => {
|
|
let mut bytes = Vec::with_capacity(4);
|
|
if ctrl {
|
|
bytes.push(control_byte(ch)?);
|
|
} else {
|
|
let mut encoded = [0; 4];
|
|
bytes.extend_from_slice(ch.encode_utf8(&mut encoded).as_bytes());
|
|
}
|
|
bytes
|
|
}
|
|
Key::Enter => vec![b'\r'],
|
|
Key::Tab => vec![b'\t'],
|
|
Key::Backspace => vec![0x7f],
|
|
Key::Escape => vec![0x1b],
|
|
Key::BackTab if mods == Modifiers::NONE || mods == Modifiers::SHIFT => b"\x1b[Z".to_vec(),
|
|
Key::BackTab => modified_csi(b'Z', mods, None),
|
|
Key::Up => navigation(b'A', mods, modes.application_cursor),
|
|
Key::Down => navigation(b'B', mods, modes.application_cursor),
|
|
Key::Right => navigation(b'C', mods, modes.application_cursor),
|
|
Key::Left => navigation(b'D', mods, modes.application_cursor),
|
|
Key::Home => navigation(b'H', mods, modes.application_cursor),
|
|
Key::End => navigation(b'F', mods, modes.application_cursor),
|
|
Key::Insert => tilde_key(2, mods),
|
|
Key::Delete => tilde_key(3, mods),
|
|
Key::PageUp => tilde_key(5, mods),
|
|
Key::PageDown => tilde_key(6, mods),
|
|
Key::F(n @ 1..=4) => function_1_to_4(n, mods),
|
|
Key::F(n @ 5..=12) => {
|
|
let code = [15, 17, 18, 19, 20, 21, 23, 24][usize::from(n - 5)];
|
|
tilde_key(code, mods)
|
|
}
|
|
Key::Null if ctrl => vec![0],
|
|
Key::F(_)
|
|
| Key::CapsLock
|
|
| Key::ScrollLock
|
|
| Key::NumLock
|
|
| Key::PrintScreen
|
|
| Key::Pause
|
|
| Key::Menu
|
|
| Key::KeypadBegin
|
|
| Key::Null
|
|
| Key::Unknown(_) => return None,
|
|
};
|
|
// Character/control/basic keys use the traditional ESC prefix for Alt.
|
|
// Named CSI keys encode Alt in their xterm modifier parameter already.
|
|
if alt
|
|
&& matches!(
|
|
key,
|
|
Key::Char(_) | Key::Enter | Key::Tab | Key::Backspace | Key::Escape
|
|
)
|
|
{
|
|
out.insert(0, 0x1b);
|
|
}
|
|
Some(out)
|
|
}
|
|
|
|
/// Encode pasted bytes, optionally framing them with bracketed-paste markers.
|
|
#[must_use]
|
|
pub fn encode_paste(bytes: &[u8], bracketed_paste: bool) -> Vec<u8> {
|
|
if !bracketed_paste {
|
|
return bytes.to_vec();
|
|
}
|
|
let mut out = Vec::with_capacity(bytes.len() + 12);
|
|
out.extend_from_slice(b"\x1b[200~");
|
|
out.extend_from_slice(bytes);
|
|
out.extend_from_slice(b"\x1b[201~");
|
|
out
|
|
}
|
|
|
|
/// Encode a focus transition when focus reporting is enabled.
|
|
#[must_use]
|
|
pub fn encode_focus(focused: bool, focus_reporting: bool) -> Option<Vec<u8>> {
|
|
focus_reporting.then(|| {
|
|
if focused {
|
|
b"\x1b[I".to_vec()
|
|
} else {
|
|
b"\x1b[O".to_vec()
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Encode an xterm SGR mouse report using zero-based terminal coordinates.
|
|
#[must_use]
|
|
pub fn encode_mouse(
|
|
kind: MouseKind,
|
|
coord: CellCoord,
|
|
mods: Modifiers,
|
|
modes: TerminalModes,
|
|
) -> Option<Vec<u8>> {
|
|
if mods.contains(Modifiers::META) || mods.contains(Modifiers::HYPER) {
|
|
return None;
|
|
}
|
|
if !modes.mouse_sgr || modes.mouse_tracking == MouseTrackingMode::Off {
|
|
return None;
|
|
}
|
|
let allowed = match modes.mouse_tracking {
|
|
MouseTrackingMode::Off => false,
|
|
MouseTrackingMode::X10 => matches!(kind, MouseKind::Down(_)),
|
|
MouseTrackingMode::Button => !matches!(kind, MouseKind::Move),
|
|
MouseTrackingMode::Any => true,
|
|
};
|
|
if !allowed {
|
|
return None;
|
|
}
|
|
let (mut code, release) = match kind {
|
|
MouseKind::Down(button) => (button_code(button), false),
|
|
MouseKind::Up(button) => (button_code(button), true),
|
|
MouseKind::Drag(button) => (button_code(button) + 32, false),
|
|
MouseKind::Move => (35, false),
|
|
MouseKind::ScrollUp => (64, false),
|
|
MouseKind::ScrollDown => (65, false),
|
|
MouseKind::ScrollLeft => (66, false),
|
|
MouseKind::ScrollRight => (67, false),
|
|
};
|
|
if mods.contains(Modifiers::SHIFT) {
|
|
code += 4;
|
|
}
|
|
if mods.contains(Modifiers::ALT) {
|
|
code += 8;
|
|
}
|
|
if mods.contains(Modifiers::CTRL) {
|
|
code += 16;
|
|
}
|
|
let final_byte = if release { 'm' } else { 'M' };
|
|
Some(
|
|
format!(
|
|
"\x1b[<{code};{};{}{final_byte}",
|
|
coord.col.saturating_add(1),
|
|
coord.row.saturating_add(1)
|
|
)
|
|
.into_bytes(),
|
|
)
|
|
}
|
|
|
|
fn control_byte(ch: char) -> Option<u8> {
|
|
match ch {
|
|
'@' | ' ' | '`' => Some(0),
|
|
'a'..='z' => Some(ch as u8 - b'a' + 1),
|
|
'A'..='Z' => Some(ch as u8 - b'A' + 1),
|
|
'[' | '{' => Some(0x1b),
|
|
'\\' | '|' => Some(0x1c),
|
|
']' | '}' => Some(0x1d),
|
|
'^' | '~' => Some(0x1e),
|
|
'_' => Some(0x1f),
|
|
'?' => Some(0x7f),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn navigation(final_byte: u8, mods: Modifiers, application: bool) -> Vec<u8> {
|
|
let parameter = modifier_parameter(mods);
|
|
if parameter == 1 {
|
|
vec![0x1b, if application { b'O' } else { b'[' }, final_byte]
|
|
} else {
|
|
modified_csi(final_byte, mods, None)
|
|
}
|
|
}
|
|
|
|
fn function_1_to_4(n: u8, mods: Modifiers) -> Vec<u8> {
|
|
let final_byte = b'P' + n - 1;
|
|
if modifier_parameter(mods) == 1 {
|
|
vec![0x1b, b'O', final_byte]
|
|
} else {
|
|
modified_csi(final_byte, mods, None)
|
|
}
|
|
}
|
|
|
|
fn tilde_key(code: u8, mods: Modifiers) -> Vec<u8> {
|
|
let modifier = modifier_parameter(mods);
|
|
if modifier == 1 {
|
|
format!("\x1b[{code}~").into_bytes()
|
|
} else {
|
|
format!("\x1b[{code};{modifier}~").into_bytes()
|
|
}
|
|
}
|
|
|
|
fn modified_csi(final_byte: u8, mods: Modifiers, first: Option<u8>) -> Vec<u8> {
|
|
let modifier = modifier_parameter(mods);
|
|
if modifier == 1 && first.is_none() {
|
|
return vec![0x1b, b'[', final_byte];
|
|
}
|
|
let first = first.unwrap_or(1);
|
|
format!("\x1b[{first};{modifier}{}", final_byte as char).into_bytes()
|
|
}
|
|
|
|
fn modifier_parameter(mods: Modifiers) -> u8 {
|
|
1 + u8::from(mods.contains(Modifiers::SHIFT))
|
|
+ 2 * u8::from(mods.contains(Modifiers::ALT))
|
|
+ 4 * u8::from(mods.contains(Modifiers::CTRL))
|
|
}
|
|
|
|
fn button_code(button: MouseButton) -> u8 {
|
|
match button {
|
|
MouseButton::Left => 0,
|
|
MouseButton::Middle => 1,
|
|
MouseButton::Right => 2,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn modes() -> TerminalModes {
|
|
TerminalModes::default()
|
|
}
|
|
|
|
#[test]
|
|
fn utf8_ctrl_and_alt_boundaries() {
|
|
assert_eq!(
|
|
encode_key(Key::Char('é'), Modifiers::NONE, modes()),
|
|
Some("é".as_bytes().to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::Char('c'), Modifiers::CTRL, modes()),
|
|
Some(vec![3])
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::Char('?'), Modifiers::CTRL | Modifiers::ALT, modes()),
|
|
Some(vec![0x1b, 0x7f])
|
|
);
|
|
assert_eq!(encode_key(Key::Char('é'), Modifiers::CTRL, modes()), None);
|
|
}
|
|
|
|
#[test]
|
|
fn application_cursor_and_xterm_modifiers() {
|
|
let mut app = modes();
|
|
app.application_cursor = true;
|
|
assert_eq!(
|
|
encode_key(Key::Up, Modifiers::NONE, app),
|
|
Some(b"\x1bOA".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::Up, Modifiers::CTRL | Modifiers::SHIFT, app),
|
|
Some(b"\x1b[1;6A".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::Delete, Modifiers::ALT, modes()),
|
|
Some(b"\x1b[3;3~".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::F(1), Modifiers::NONE, modes()),
|
|
Some(b"\x1bOP".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::F(12), Modifiers::CTRL, modes()),
|
|
Some(b"\x1b[24;5~".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_key(Key::BackTab, Modifiers::SHIFT, modes()),
|
|
Some(b"\x1b[Z".to_vec())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ambiguous_digits_ignore_application_keypad() {
|
|
let mut app = modes();
|
|
app.application_keypad = true;
|
|
assert_eq!(
|
|
encode_key(Key::Char('7'), Modifiers::NONE, app),
|
|
Some(b"7".to_vec())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn paste_and_focus_are_exact() {
|
|
assert_eq!(encode_paste(b"a\0b", false), b"a\0b".to_vec());
|
|
assert_eq!(
|
|
encode_paste(b"a\0b", true),
|
|
b"\x1b[200~a\0b\x1b[201~".to_vec()
|
|
);
|
|
assert_eq!(encode_focus(true, true), Some(b"\x1b[I".to_vec()));
|
|
assert_eq!(encode_focus(false, true), Some(b"\x1b[O".to_vec()));
|
|
assert_eq!(encode_focus(true, false), None);
|
|
}
|
|
|
|
#[test]
|
|
fn sgr_mouse_modes_modifiers_and_coordinates() {
|
|
let mut m = modes();
|
|
m.mouse_sgr = true;
|
|
m.mouse_tracking = MouseTrackingMode::Any;
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::Down(MouseButton::Left),
|
|
CellCoord::new(0, 0),
|
|
Modifiers::NONE,
|
|
m
|
|
),
|
|
Some(b"\x1b[<0;1;1M".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::Drag(MouseButton::Right),
|
|
CellCoord::new(511, 511),
|
|
Modifiers::CTRL | Modifiers::ALT,
|
|
m
|
|
),
|
|
Some(b"\x1b[<58;512;512M".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::Up(MouseButton::Right),
|
|
CellCoord::new(4, 9),
|
|
Modifiers::NONE,
|
|
m
|
|
),
|
|
Some(b"\x1b[<2;10;5m".to_vec())
|
|
);
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::ScrollDown,
|
|
CellCoord::new(1, 2),
|
|
Modifiers::SHIFT,
|
|
m
|
|
),
|
|
Some(b"\x1b[<69;3;2M".to_vec())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_keys_are_invisible() {
|
|
assert_eq!(encode_key(Key::Unknown(7), Modifiers::NONE, modes()), None);
|
|
assert_eq!(encode_key(Key::F(13), Modifiers::NONE, modes()), None);
|
|
assert_eq!(encode_key(Key::Char('c'), Modifiers::META, modes()), None);
|
|
assert_eq!(encode_key(Key::Up, Modifiers::HYPER, modes()), None);
|
|
let mut mouse_modes = modes();
|
|
mouse_modes.mouse_sgr = true;
|
|
mouse_modes.mouse_tracking = MouseTrackingMode::Any;
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::Down(MouseButton::Left),
|
|
CellCoord::new(0, 0),
|
|
Modifiers::META,
|
|
mouse_modes,
|
|
),
|
|
None,
|
|
);
|
|
assert_eq!(
|
|
encode_mouse(
|
|
MouseKind::Move,
|
|
CellCoord::new(4, 9),
|
|
Modifiers::HYPER,
|
|
mouse_modes,
|
|
),
|
|
None,
|
|
);
|
|
}
|
|
}
|