use crate::Screen;
use crate::input::{KeyEvent, MouseEvent, MouseEventKind};
use crate::keys::{CoordEncoding, key_to_bytes, key_to_bytes_kitty, mouse_to_bytes};
use crate::screen::{CellPixelSize, TerminalMode};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum MouseReportLevel {
#[default]
None,
X10,
Click,
Drag,
Any,
}
impl MouseReportLevel {
fn from_screen(screen: &Screen) -> Self {
if screen.mode(TerminalMode::MouseReportAllMotion) {
Self::Any
} else if screen.mode(TerminalMode::MouseReportCellMotion) {
Self::Drag
} else if screen.mode(TerminalMode::MouseReportClick) {
Self::Click
} else if screen.mode(TerminalMode::MouseReportX10) {
Self::X10
} else {
Self::None
}
}
fn allows(self, kind: MouseEventKind) -> bool {
match self {
Self::None => false,
Self::X10 => matches!(kind, MouseEventKind::Down(_)),
Self::Click => !matches!(kind, MouseEventKind::Drag(_) | MouseEventKind::Moved),
Self::Drag => !matches!(kind, MouseEventKind::Moved),
Self::Any => true,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct KeyScreenState {
pub kitty_keyboard_flags: u8,
pub application_cursor: bool,
pub backspace_bs: bool,
pub line_feed_new_line: bool,
}
#[derive(Clone, Debug, Default)]
pub struct KeyEncoder {
state: KeyScreenState,
}
impl KeyEncoder {
pub fn new() -> Self {
Self::default()
}
pub fn sync(&mut self, screen: &Screen) {
self.state = KeyScreenState {
kitty_keyboard_flags: screen.kitty_keyboard_flags(),
application_cursor: screen.mode(TerminalMode::ApplicationCursor),
backspace_bs: screen.mode(TerminalMode::BackspaceBs),
line_feed_new_line: screen.mode(TerminalMode::LineFeedNewLine),
};
}
#[must_use]
pub fn screen_state(&self) -> &KeyScreenState {
&self.state
}
pub fn encode_key(&self, key: &KeyEvent) -> Option<Vec<u8>> {
if self.state.kitty_keyboard_flags != 0 {
key_to_bytes_kitty(key, self.state.kitty_keyboard_flags)
} else {
key_to_bytes(
key,
self.state.application_cursor,
self.state.backspace_bs,
self.state.line_feed_new_line,
)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct MouseEncoder {
level: MouseReportLevel,
sgr_mouse: bool,
sgr_pixel_mouse: bool,
cell_pixel_size: CellPixelSize,
alternate_scroll: bool,
alternate_screen: bool,
application_cursor: bool,
}
impl MouseEncoder {
pub fn new() -> Self {
Self::default()
}
pub fn sync(&mut self, screen: &Screen) {
self.level = MouseReportLevel::from_screen(screen);
self.sgr_mouse = screen.mode(TerminalMode::SgrMouse);
self.sgr_pixel_mouse = screen.mode(TerminalMode::SgrPixelMouse);
self.cell_pixel_size = screen.pixel_cell_size();
self.alternate_scroll = screen.mode(TerminalMode::AlternateScroll);
self.alternate_screen = screen.mode(TerminalMode::AlternateScreen);
self.application_cursor = screen.mode(TerminalMode::ApplicationCursor);
}
#[must_use]
pub fn mouse_enabled(&self) -> bool {
!matches!(self.level, MouseReportLevel::None)
}
#[must_use]
pub fn cell_pixel_size(&self) -> CellPixelSize {
self.cell_pixel_size
}
pub fn encode_mouse(&self, event: &MouseEvent) -> Option<Vec<u8>> {
if let Some(bytes) = self.alt_scroll_translation(event.kind) {
return Some(bytes);
}
if !self.level.allows(event.kind) {
return None;
}
mouse_to_bytes(event, self.coord_encoding())
}
fn alt_scroll_translation(&self, kind: MouseEventKind) -> Option<Vec<u8>> {
if !(self.alternate_scroll
&& self.alternate_screen
&& matches!(self.level, MouseReportLevel::None))
{
return None;
}
match (kind, self.application_cursor) {
(MouseEventKind::ScrollUp, true) => Some(b"\x1bOA".to_vec()),
(MouseEventKind::ScrollUp, false) => Some(b"\x1b[A".to_vec()),
(MouseEventKind::ScrollDown, true) => Some(b"\x1bOB".to_vec()),
(MouseEventKind::ScrollDown, false) => Some(b"\x1b[B".to_vec()),
_ => None,
}
}
fn coord_encoding(&self) -> CoordEncoding {
if self.sgr_pixel_mouse
&& self.cell_pixel_size.width != 0
&& self.cell_pixel_size.height != 0
{
CoordEncoding::SgrPixel(self.cell_pixel_size)
} else if self.sgr_mouse || self.sgr_pixel_mouse {
CoordEncoding::Sgr
} else {
CoordEncoding::X10
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{KeyCode, KeyModifiers, MouseButton, MouseEventKind};
use crate::{Parser, TerminalSize};
fn encoder_after(setup: &[u8]) -> MouseEncoder {
let mut parser = Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
parser.process(setup);
let mut enc = MouseEncoder::new();
enc.sync(parser.screen());
enc
}
fn encoder_with_cell_size(setup: &[u8], cell: crate::screen::CellPixelSize) -> MouseEncoder {
let mut parser = Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
parser.screen_mut().set_pixel_cell_size(cell);
parser.process(setup);
let mut enc = MouseEncoder::new();
enc.sync(parser.screen());
enc
}
fn at(kind: MouseEventKind) -> MouseEvent {
MouseEvent {
kind,
row: 0,
col: 0,
modifiers: KeyModifiers::NONE,
}
}
#[test]
fn key_encoder_legacy_char() {
let enc = KeyEncoder::new();
let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert_eq!(enc.encode_key(&key), Some(b"a".to_vec()));
}
#[test]
fn key_encoder_application_cursor() {
let mut enc = KeyEncoder::new();
enc.state.application_cursor = true;
let key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
assert_eq!(enc.encode_key(&key), Some(b"\x1bOA".to_vec()));
}
#[test]
fn key_encoder_kitty_mode() {
let mut enc = KeyEncoder::new();
enc.state.kitty_keyboard_flags = 1; let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(enc.encode_key(&key), Some(b"\x1b[27u".to_vec()));
}
#[test]
fn key_encoder_sync_picks_up_runtime_decbkm_flip() {
let mut parser = Parser::new(TerminalSize { rows: 24, cols: 80 }, 0);
let mut enc = KeyEncoder::new();
enc.sync(parser.screen());
assert!(!enc.screen_state().backspace_bs);
let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(enc.encode_key(&key), Some(vec![0x7f]));
parser.process(b"\x1b[?67h");
assert!(parser.screen().mode(TerminalMode::BackspaceBs));
assert_eq!(
enc.encode_key(&key),
Some(vec![0x7f]),
"encoder must keep emitting the stale byte until sync() runs",
);
enc.sync(parser.screen());
assert!(enc.screen_state().backspace_bs);
assert_eq!(
enc.encode_key(&key),
Some(vec![0x08]),
"after sync(), encoder must honor the runtime DECBKM flip",
);
}
#[test]
fn key_encoder_kitty_path_ignores_decbkm() {
let mut enc = KeyEncoder::new();
enc.state.kitty_keyboard_flags = 1; let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
let baseline = enc.encode_key(&key);
assert_eq!(baseline, Some(b"\x1b[127u".to_vec()));
enc.state.backspace_bs = true;
assert_eq!(
enc.encode_key(&key),
baseline,
"kitty path must not honor DECBKM",
);
}
#[test]
fn mouse_encoder_disabled() {
let enc = MouseEncoder::new();
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 0,
col: 0,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), None);
}
#[test]
fn mouse_encoder_sgr() {
let enc = encoder_after(b"\x1b[?1000h\x1b[?1006h");
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 4,
col: 9,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;10;5M".to_vec()));
}
#[test]
fn mouse_encoder_click_level_drops_motion() {
let enc = encoder_after(b"\x1b[?1000h\x1b[?1006h");
assert!(enc.mouse_enabled());
assert!(
enc.encode_mouse(&at(MouseEventKind::Down(MouseButton::Left)))
.is_some(),
"press must be emitted at click level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Up(MouseButton::Left)))
.is_some(),
"release must be emitted at click level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_none(),
"drag motion must be dropped at click level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_none(),
"pure motion must be dropped at click level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::ScrollUp)).is_some(),
"scroll wheel must be emitted at click level"
);
}
#[test]
fn mouse_encoder_drag_level_drops_pure_motion() {
let enc = encoder_after(b"\x1b[?1002h\x1b[?1006h");
assert!(enc.mouse_enabled());
assert!(
enc.encode_mouse(&at(MouseEventKind::Down(MouseButton::Left)))
.is_some()
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Up(MouseButton::Left)))
.is_some()
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_some(),
"drag motion must be emitted at drag level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_none(),
"pure motion must be dropped at drag level"
);
}
#[test]
fn mouse_encoder_any_level_emits_pure_motion() {
let enc = encoder_after(b"\x1b[?1003h\x1b[?1006h");
assert!(enc.mouse_enabled());
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_some()
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_some(),
"pure motion must be emitted at any level"
);
}
#[test]
fn mouse_encoder_any_level_overrides_lower_modes() {
let enc = encoder_after(b"\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h");
assert!(enc.encode_mouse(&at(MouseEventKind::Moved)).is_some());
}
#[test]
fn mouse_encoder_disabled_after_reset() {
let enc = encoder_after(b"\x1b[?1003h\x1b[?1003l");
assert!(!enc.mouse_enabled());
assert!(
enc.encode_mouse(&at(MouseEventKind::Down(MouseButton::Left)))
.is_none()
);
}
#[test]
fn key_encoder_unshifted_codepoint() {
let mut enc = KeyEncoder::new();
enc.state.kitty_keyboard_flags = 1 | 4; let key =
KeyEvent::new(KeyCode::Char('!'), KeyModifiers::SHIFT).with_unshifted_codepoint('1');
let bytes = enc.encode_key(&key).unwrap();
let s = String::from_utf8(bytes).unwrap();
assert_eq!(s, "\x1b[49:33;2u");
}
#[test]
fn key_encoder_consumed_modifiers() {
let mut enc = KeyEncoder::new();
enc.state.kitty_keyboard_flags = 1; let key = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT)
.with_unshifted_codepoint('a')
.with_consumed_modifiers(KeyModifiers::SHIFT);
let bytes = enc.encode_key(&key).unwrap();
let s = String::from_utf8(bytes).unwrap();
assert_eq!(s, "A");
}
#[test]
fn mouse_encoder_sgr_pixel_basic() {
let cell = crate::screen::CellPixelSize {
width: 10,
height: 20,
};
let enc = encoder_with_cell_size(b"\x1b[?1000h\x1b[?1006h\x1b[?1016h", cell);
assert_eq!(enc.cell_pixel_size(), cell);
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 3,
col: 5,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;51;61M".to_vec()));
}
#[test]
fn mouse_encoder_sgr_pixel_implies_sgr_without_1006() {
let cell = crate::screen::CellPixelSize {
width: 8,
height: 16,
};
let enc = encoder_with_cell_size(b"\x1b[?1000h\x1b[?1016h", cell);
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 0,
col: 0,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;1;1M".to_vec()));
}
#[test]
fn mouse_encoder_sgr_pixel_falls_back_when_cell_size_unknown() {
let enc = encoder_after(b"\x1b[?1000h\x1b[?1006h\x1b[?1016h");
assert_eq!(
enc.cell_pixel_size(),
crate::screen::CellPixelSize::default()
);
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 4,
col: 9,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;10;5M".to_vec()));
}
#[test]
fn mouse_encoder_sgr_pixel_release_uses_lowercase_m() {
let cell = crate::screen::CellPixelSize {
width: 10,
height: 20,
};
let enc = encoder_with_cell_size(b"\x1b[?1000h\x1b[?1016h", cell);
let event = MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
row: 1,
col: 2,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;21;21m".to_vec()));
}
#[test]
fn mouse_encoder_alt_scroll_translates_wheel_to_arrows_on_alt_screen() {
let enc = encoder_after(b"\x1b[?1049h\x1b[?1007h");
assert!(!enc.mouse_enabled());
assert_eq!(
enc.encode_mouse(&at(MouseEventKind::ScrollUp)),
Some(b"\x1b[A".to_vec()),
);
assert_eq!(
enc.encode_mouse(&at(MouseEventKind::ScrollDown)),
Some(b"\x1b[B".to_vec()),
);
}
#[test]
fn mouse_encoder_alt_scroll_honors_application_cursor() {
let enc = encoder_after(b"\x1b[?1h\x1b[?1049h\x1b[?1007h");
assert!(!enc.mouse_enabled());
assert_eq!(
enc.encode_mouse(&at(MouseEventKind::ScrollUp)),
Some(b"\x1bOA".to_vec()),
);
assert_eq!(
enc.encode_mouse(&at(MouseEventKind::ScrollDown)),
Some(b"\x1bOB".to_vec()),
);
}
#[test]
fn mouse_encoder_alt_scroll_inactive_off_alt_screen() {
let enc = encoder_after(b"\x1b[?1007h");
assert!(!enc.mouse_enabled());
assert_eq!(enc.encode_mouse(&at(MouseEventKind::ScrollUp)), None);
assert_eq!(enc.encode_mouse(&at(MouseEventKind::ScrollDown)), None);
}
#[test]
fn mouse_encoder_alt_scroll_inactive_when_mouse_reporting_active() {
let enc = encoder_after(b"\x1b[?1049h\x1b[?1007h\x1b[?1000h\x1b[?1006h");
let event = MouseEvent {
kind: MouseEventKind::ScrollUp,
row: 4,
col: 9,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<64;10;5M".to_vec()));
}
#[test]
fn mouse_encoder_alt_scroll_horizontal_wheel_passes_through() {
let enc = encoder_after(b"\x1b[?1049h\x1b[?1007h");
assert_eq!(enc.encode_mouse(&at(MouseEventKind::ScrollLeft)), None);
assert_eq!(enc.encode_mouse(&at(MouseEventKind::ScrollRight)), None);
}
#[test]
fn mouse_encoder_x10_level_press_only() {
let enc = encoder_after(b"\x1b[?9h");
assert!(enc.mouse_enabled());
assert!(
enc.encode_mouse(&at(MouseEventKind::Down(MouseButton::Left)))
.is_some(),
"press must be emitted at X10 level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Up(MouseButton::Left)))
.is_none(),
"release must be dropped at X10 level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_none(),
"drag must be dropped at X10 level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_none(),
"pure motion must be dropped at X10 level"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::ScrollUp)).is_none(),
"wheel must be dropped at X10 level"
);
}
#[test]
fn mouse_encoder_x10_dominated_by_click() {
let enc = encoder_after(b"\x1b[?9h\x1b[?1000h");
assert!(
enc.encode_mouse(&at(MouseEventKind::Up(MouseButton::Left)))
.is_some(),
"Click level must surface release events even with X10 also set"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_none(),
"Click level must drop drag even with X10 also set"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::ScrollUp)).is_some(),
"Click level must surface wheel events even with X10 also set"
);
}
#[test]
fn mouse_encoder_x10_dominated_by_drag() {
let enc = encoder_after(b"\x1b[?9h\x1b[?1002h");
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_some(),
"Drag level must surface drag motion even with X10 also set"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_none(),
"Drag level must drop pure motion even with X10 also set"
);
}
#[test]
fn mouse_encoder_x10_dominated_by_any() {
let enc = encoder_after(b"\x1b[?9h\x1b[?1003h");
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_some(),
"Any level must surface pure motion even with X10 also set"
);
}
#[test]
fn mouse_encoder_x10_uses_x10_wire_format_by_default() {
let enc = encoder_after(b"\x1b[?9h");
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 4,
col: 9,
modifiers: KeyModifiers::NONE,
};
let bytes = enc.encode_mouse(&event).expect("press must encode");
assert!(
bytes.starts_with(b"\x1b[M"),
"expected legacy X10 wire prefix, got {bytes:?}"
);
}
#[test]
fn mouse_encoder_x10_with_sgr_uses_sgr_wire_format() {
let enc = encoder_after(b"\x1b[?9h\x1b[?1006h");
let event = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
row: 4,
col: 9,
modifiers: KeyModifiers::NONE,
};
assert_eq!(enc.encode_mouse(&event), Some(b"\x1b[<0;10;5M".to_vec()));
}
#[test]
fn mouse_encoder_sgr_pixel_respects_level() {
let cell = crate::screen::CellPixelSize {
width: 10,
height: 20,
};
let enc = encoder_with_cell_size(b"\x1b[?1000h\x1b[?1016h", cell);
assert!(
enc.encode_mouse(&at(MouseEventKind::Down(MouseButton::Left)))
.is_some()
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Drag(MouseButton::Left)))
.is_none(),
"drag must be dropped at click level even with ?1016"
);
assert!(
enc.encode_mouse(&at(MouseEventKind::Moved)).is_none(),
"pure motion must be dropped at click level even with ?1016"
);
}
}