use term_wm_events::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use vt100::{MouseProtocolEncoding, MouseProtocolMode};
pub fn key_to_bytes(key: &KeyEvent, application_cursor_keys: bool) -> Vec<u8> {
match (key.code, key.modifiers) {
(KeyCode::Char(c), m) if m == KeyModifiers::NONE || m.shift => {
let ch = if m.shift { c.to_ascii_uppercase() } else { c };
ch.to_string().into_bytes()
}
(KeyCode::Char(c), m) if m.control => match ctrl_char(c) {
Some(byte) => vec![byte],
None => Vec::new(),
},
(KeyCode::Char(c), m) if m.alt => {
let mut b = vec![0x1b];
b.extend(c.to_string().into_bytes());
b
}
(KeyCode::Enter, _) => vec![b'\r'],
(KeyCode::Backspace, _) => vec![0x7f],
(KeyCode::Esc, _) => vec![0x1b],
(KeyCode::Tab, m) if m.shift => b"\x1b[Z".to_vec(),
(KeyCode::Tab, _) => vec![b'\t'],
(KeyCode::Up, m) if application_cursor_keys && m == KeyModifiers::NONE => {
b"\x1bOA".to_vec()
}
(KeyCode::Down, m) if application_cursor_keys && m == KeyModifiers::NONE => {
b"\x1bOB".to_vec()
}
(KeyCode::Right, m) if application_cursor_keys && m == KeyModifiers::NONE => {
b"\x1bOC".to_vec()
}
(KeyCode::Left, m) if application_cursor_keys && m == KeyModifiers::NONE => {
b"\x1bOD".to_vec()
}
(KeyCode::Up, _) => b"\x1b[A".to_vec(),
(KeyCode::Down, _) => b"\x1b[B".to_vec(),
(KeyCode::Right, _) => b"\x1b[C".to_vec(),
(KeyCode::Left, _) => b"\x1b[D".to_vec(),
(KeyCode::Home, _) => b"\x1b[H".to_vec(),
(KeyCode::End, _) => b"\x1b[F".to_vec(),
(KeyCode::PageUp, _) => b"\x1b[5~".to_vec(),
(KeyCode::PageDown, _) => b"\x1b[6~".to_vec(),
(KeyCode::Delete, _) => b"\x1b[3~".to_vec(),
(KeyCode::Insert, _) => b"\x1b[2~".to_vec(),
(KeyCode::F(1), _) => b"\x1bOP".to_vec(),
(KeyCode::F(2), _) => b"\x1bOQ".to_vec(),
(KeyCode::F(3), _) => b"\x1bOR".to_vec(),
(KeyCode::F(4), _) => b"\x1bOS".to_vec(),
(KeyCode::F(n), _) if (5..=15).contains(&n) => {
let code: &[u8] = match n {
5 => b"15~",
6 => b"17~",
7 => b"18~",
8 => b"19~",
9 => b"20~",
10 => b"21~",
11 => b"23~",
12 => b"24~",
13 => b"25~",
14 => b"26~",
15 => b"28~",
_ => unreachable!(),
};
let mut b = vec![0x1b, b'['];
b.extend_from_slice(code);
b
}
_ => Vec::new(),
}
}
pub fn ctrl_char(c: char) -> Option<u8> {
let c = c.to_ascii_lowercase();
if c.is_ascii_lowercase() {
Some((c as u8) - b'a' + 1)
} else {
None
}
}
pub fn mouse_event_allowed(mode: MouseProtocolMode, kind: MouseEventKind) -> bool {
use MouseEventKind::*;
match mode {
MouseProtocolMode::None => false,
MouseProtocolMode::Press => {
matches!(
kind,
Press(_) | ScrollUp | ScrollDown | ScrollLeft | ScrollRight
)
}
MouseProtocolMode::PressRelease => {
matches!(
kind,
Press(_) | Release(_) | ScrollUp | ScrollDown | ScrollLeft | ScrollRight
)
}
MouseProtocolMode::ButtonMotion => {
matches!(
kind,
Press(_) | Release(_) | Drag(_) | ScrollUp | ScrollDown | ScrollLeft | ScrollRight
)
}
MouseProtocolMode::AnyMotion => true,
}
}
pub fn mouse_event_to_bytes(mouse: &MouseEvent, encoding: MouseProtocolEncoding) -> Vec<u8> {
let (mut code, release): (u8, bool) = match mouse.kind {
MouseEventKind::Press(button) => (
match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
MouseButton::Right => 2,
},
false,
),
MouseEventKind::Release(button) => (
match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
MouseButton::Right => 2,
},
true,
),
MouseEventKind::Drag(button) => (
32 + match button {
MouseButton::Left => 0,
MouseButton::Middle => 1,
MouseButton::Right => 2,
},
false,
),
MouseEventKind::Moved => (35, false),
MouseEventKind::ScrollUp => (64, false),
MouseEventKind::ScrollDown => (65, false),
MouseEventKind::ScrollLeft => (66, false),
MouseEventKind::ScrollRight => (67, false),
};
if mouse.modifiers.shift {
code |= 4;
}
if mouse.modifiers.alt {
code |= 8;
}
if mouse.modifiers.control {
code |= 16;
}
let col = mouse.column.saturating_add(1);
let row = mouse.row.saturating_add(1);
match encoding {
MouseProtocolEncoding::Sgr => {
let action = if release { 'm' } else { 'M' };
format!("\x1b[<{};{};{}{}", code, col, row, action).into_bytes()
}
MouseProtocolEncoding::Default => {
let x11_code = if release {
let mods = code & (4 | 8 | 16);
3 | mods
} else {
code
};
let cb = x11_code.saturating_add(32);
let cx = mouse.column.saturating_add(33);
let cy = mouse.row.saturating_add(33);
if cx > 255 || cy > 255 {
return Vec::new();
}
vec![0x1b, b'[', b'M', cb, cx as u8, cy as u8]
}
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use term_wm_events::KeyKind;
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyKind::Press,
}
}
fn kb(key: &KeyEvent) -> Vec<u8> {
key_to_bytes(key, false)
}
#[test]
fn key_to_bytes_char_and_controls() {
let b = kb(&key(KeyCode::Char('x'), KeyModifiers::NONE));
assert_eq!(b, b"x".to_vec());
let enter = kb(&key(KeyCode::Enter, KeyModifiers::NONE));
assert_eq!(enter, vec![b'\r']);
let back = kb(&key(KeyCode::Backspace, KeyModifiers::NONE));
assert_eq!(back, vec![0x7f]);
let ctrl_a = kb(&key(
KeyCode::Char('a'),
KeyModifiers {
shift: false,
control: true,
alt: false,
},
));
assert_eq!(ctrl_a, vec![1u8]);
}
#[test]
fn key_to_bytes_alt() {
let alt_x = kb(&key(
KeyCode::Char('x'),
KeyModifiers {
shift: false,
control: false,
alt: true,
},
));
assert_eq!(alt_x, vec![0x1b, b'x']);
}
#[test]
fn key_to_bytes_delete_insert() {
let del = kb(&key(KeyCode::Delete, KeyModifiers::NONE));
assert_eq!(del, b"\x1b[3~");
let ins = kb(&key(KeyCode::Insert, KeyModifiers::NONE));
assert_eq!(ins, b"\x1b[2~");
}
#[test]
fn key_to_bytes_fkeys() {
let f1 = kb(&key(KeyCode::F(1), KeyModifiers::NONE));
assert_eq!(f1, b"\x1bOP");
let f5 = kb(&key(KeyCode::F(5), KeyModifiers::NONE));
assert_eq!(f5, b"\x1b[15~");
}
#[test]
fn key_to_bytes_backtab() {
let bt = kb(&key(KeyCode::Tab, KeyModifiers::NONE));
assert_eq!(bt, b"\t");
let shift_tab = kb(&key(
KeyCode::Tab,
KeyModifiers {
shift: true,
control: false,
alt: false,
},
));
assert_eq!(shift_tab, b"\x1b[Z");
}
#[test]
fn key_to_bytes_unicode() {
let u = kb(&key(KeyCode::Char('é'), KeyModifiers::NONE));
assert_eq!(u, "é".as_bytes());
}
#[test]
fn key_to_bytes_shift_uppercase() {
let s = kb(&key(
KeyCode::Char('a'),
KeyModifiers {
shift: true,
control: false,
alt: false,
},
));
assert_eq!(s, b"A");
}
#[test]
fn key_to_bytes_media_keys_return_empty() {
for code in &[
KeyCode::MediaPlayPause,
KeyCode::MediaStop,
KeyCode::MediaTrackNext,
KeyCode::MediaTrackPrevious,
] {
let ev = key(*code, KeyModifiers::NONE);
assert!(
key_to_bytes(&ev, false).is_empty(),
"Media key {:?} must produce empty bytes",
code
);
}
}
#[test]
fn ctrl_char_edges() {
assert_eq!(ctrl_char('a'), Some(1));
assert_eq!(ctrl_char('z'), Some(26));
assert_eq!(ctrl_char('A'), Some(1));
assert_eq!(ctrl_char('1'), None);
}
#[test]
fn mouse_event_allowed_modes() {
use MouseEventKind::*;
assert!(!mouse_event_allowed(
MouseProtocolMode::None,
Press(MouseButton::Left)
));
assert!(mouse_event_allowed(
MouseProtocolMode::Press,
Press(MouseButton::Left)
));
assert!(!mouse_event_allowed(
MouseProtocolMode::Press,
Release(MouseButton::Left)
));
assert!(mouse_event_allowed(
MouseProtocolMode::PressRelease,
Release(MouseButton::Left)
));
assert!(mouse_event_allowed(
MouseProtocolMode::ButtonMotion,
Drag(MouseButton::Left)
));
assert!(mouse_event_allowed(MouseProtocolMode::AnyMotion, Moved));
assert!(
mouse_event_allowed(MouseProtocolMode::Press, ScrollDown),
"ScrollDown should be allowed in Press mode"
);
assert!(
mouse_event_allowed(MouseProtocolMode::PressRelease, ScrollUp),
"ScrollUp should be allowed in PressRelease mode"
);
assert!(
mouse_event_allowed(MouseProtocolMode::ButtonMotion, ScrollLeft),
"ScrollLeft should be allowed in ButtonMotion mode"
);
assert!(
!mouse_event_allowed(MouseProtocolMode::None, ScrollDown),
"ScrollDown should be denied in None mode"
);
}
#[test]
fn mouse_event_to_bytes_format_and_mods() {
let m = MouseEvent {
kind: MouseEventKind::Press(MouseButton::Left),
modifiers: KeyModifiers::NONE,
column: 2,
row: 3,
};
let bytes = mouse_event_to_bytes(&m, MouseProtocolEncoding::Sgr);
let s = String::from_utf8(bytes).unwrap();
assert!(s.starts_with("\x1b[<0;3;4M"));
let bytes_def = mouse_event_to_bytes(&m, MouseProtocolEncoding::Default);
assert_eq!(bytes_def, vec![0x1b, b'[', b'M', 32, 35, 36]);
let m2 = MouseEvent {
kind: MouseEventKind::Release(MouseButton::Right),
modifiers: KeyModifiers {
shift: true,
control: false,
alt: true,
},
column: 0,
row: 0,
};
let s2 = String::from_utf8(mouse_event_to_bytes(&m2, MouseProtocolEncoding::Sgr)).unwrap();
assert!(s2.contains(';'));
assert!(s2.ends_with('m'));
}
#[test]
fn mouse_event_x11_release_and_modifiers() {
let m_up = MouseEvent {
kind: MouseEventKind::Release(MouseButton::Left),
modifiers: KeyModifiers::NONE,
column: 0,
row: 0,
};
let bytes = mouse_event_to_bytes(&m_up, MouseProtocolEncoding::Default);
assert_eq!(bytes, vec![0x1b, b'[', b'M', 35, 33, 33]);
let m_up_shift = MouseEvent {
kind: MouseEventKind::Release(MouseButton::Left),
modifiers: KeyModifiers {
shift: true,
control: false,
alt: false,
},
column: 0,
row: 0,
};
let bytes = mouse_event_to_bytes(&m_up_shift, MouseProtocolEncoding::Default);
assert_eq!(bytes, vec![0x1b, b'[', b'M', 39, 33, 33]);
let m_down_ctrl = MouseEvent {
kind: MouseEventKind::Press(MouseButton::Right),
modifiers: KeyModifiers {
shift: false,
control: true,
alt: false,
},
column: 0,
row: 0,
};
let bytes = mouse_event_to_bytes(&m_down_ctrl, MouseProtocolEncoding::Default);
assert_eq!(bytes, vec![0x1b, b'[', b'M', 50, 33, 33]);
}
#[test]
fn key_to_bytes_application_cursor_arrows() {
let up = key(KeyCode::Up, KeyModifiers::NONE);
assert_eq!(
key_to_bytes(&up, true),
b"\x1bOA",
"DECCKM: Up should emit SS3"
);
let down = key(KeyCode::Down, KeyModifiers::NONE);
assert_eq!(key_to_bytes(&down, true), b"\x1bOB");
let right = key(KeyCode::Right, KeyModifiers::NONE);
assert_eq!(key_to_bytes(&right, true), b"\x1bOC");
let left = key(KeyCode::Left, KeyModifiers::NONE);
assert_eq!(key_to_bytes(&left, true), b"\x1bOD");
}
#[test]
fn key_to_bytes_application_cursor_off_uses_csi() {
let up = key(KeyCode::Up, KeyModifiers::NONE);
assert_eq!(
key_to_bytes(&up, false),
b"\x1b[A",
"Normal mode: Up should emit CSI"
);
}
#[test]
fn key_to_bytes_application_cursor_modified_uses_csi() {
let shift_up = key(
KeyCode::Up,
KeyModifiers {
shift: true,
control: false,
alt: false,
},
);
assert_eq!(
key_to_bytes(&shift_up, true),
b"\x1b[A",
"Modified arrows must use CSI even when DECCKM active"
);
let ctrl_up = key(
KeyCode::Up,
KeyModifiers {
shift: false,
control: true,
alt: false,
},
);
assert_eq!(
key_to_bytes(&ctrl_up, true),
b"\x1b[A",
"Ctrl+Up must use CSI even when DECCKM active"
);
let alt_up = key(
KeyCode::Up,
KeyModifiers {
shift: false,
control: false,
alt: true,
},
);
assert_eq!(
key_to_bytes(&alt_up, true),
b"\x1b[A",
"Alt+Up must use CSI even when DECCKM active"
);
}
}