#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum MouseMode {
#[default]
Off,
ButtonDrag,
AnyMotion,
}
impl MouseMode {
pub(crate) const fn arm_bytes(self) -> &'static [u8] {
match self {
MouseMode::Off => b"",
MouseMode::ButtonDrag => b"\x1b[?1002h\x1b[?1006h",
MouseMode::AnyMotion => b"\x1b[?1003h\x1b[?1006h",
}
}
pub(crate) const fn disarm_bytes(self) -> &'static [u8] {
match self {
MouseMode::Off => b"",
MouseMode::ButtonDrag => b"\x1b[?1006l\x1b[?1002l",
MouseMode::AnyMotion => b"\x1b[?1006l\x1b[?1003l",
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct KittyFlags(pub u8);
impl KittyFlags {
pub const DISAMBIGUATE: u8 = 1;
pub const REPORT_EVENT_TYPES: u8 = 2;
pub const REPORT_ALTERNATE: u8 = 4;
pub const REPORT_ALL_AS_ESCAPES: u8 = 8;
pub const REPORT_TEXT: u8 = 16;
pub const fn standard() -> Self {
KittyFlags(Self::DISAMBIGUATE | Self::REPORT_EVENT_TYPES)
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub(crate) fn push_bytes(self) -> Vec<u8> {
format!("\x1b[>{}u", self.0).into_bytes()
}
pub(crate) const POP_BYTES: &'static [u8] = b"\x1b[<u";
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct EnterOptions {
pub alternate_screen: bool,
pub hide_cursor: bool,
pub mouse: MouseMode,
pub bracketed_paste: bool,
pub focus_events: bool,
pub kitty_keyboard: KittyFlags,
}
impl Default for EnterOptions {
fn default() -> Self {
EnterOptions {
alternate_screen: true,
hide_cursor: true,
mouse: MouseMode::ButtonDrag,
bracketed_paste: true,
focus_events: true,
kitty_keyboard: KittyFlags(0),
}
}
}
impl EnterOptions {
pub fn enter_bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(64);
if self.alternate_screen {
b.extend_from_slice(b"\x1b[?1049h");
}
if self.hide_cursor {
b.extend_from_slice(b"\x1b[?25l");
}
b.extend_from_slice(self.mouse.arm_bytes());
if self.bracketed_paste {
b.extend_from_slice(b"\x1b[?2004h");
}
if self.focus_events {
b.extend_from_slice(b"\x1b[?1004h");
}
if !self.kitty_keyboard.is_empty() {
b.extend_from_slice(&self.kitty_keyboard.push_bytes());
}
b
}
pub fn leave_bytes(&self) -> Vec<u8> {
let mut b = Vec::with_capacity(64);
if !self.kitty_keyboard.is_empty() {
b.extend_from_slice(KittyFlags::POP_BYTES);
}
if self.focus_events {
b.extend_from_slice(b"\x1b[?1004l");
}
if self.bracketed_paste {
b.extend_from_slice(b"\x1b[?2004l");
}
b.extend_from_slice(self.mouse.disarm_bytes());
b.extend_from_slice(b"\x1b[?2026l\x1b[0m");
if self.hide_cursor {
b.extend_from_slice(b"\x1b[?25h");
}
if self.alternate_screen {
b.extend_from_slice(b"\x1b[?1049l");
}
b
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enter_and_leave_mirror_each_other() {
let opts = EnterOptions {
kitty_keyboard: KittyFlags::standard(),
..EnterOptions::default()
};
let enter = String::from_utf8(opts.enter_bytes()).unwrap();
let leave = String::from_utf8(opts.leave_bytes()).unwrap();
assert!(enter.starts_with("\x1b[?1049h"));
assert!(enter.ends_with("\x1b[>3u"));
assert!(leave.starts_with("\x1b[<u"));
assert!(leave.ends_with("\x1b[?1049l"));
for mode in ["1002", "1006", "2004", "1004", "25", "1049"] {
assert!(enter.contains(&format!("[?{mode}h")) || mode == "25");
assert!(leave.contains(&format!("[?{mode}l")) || mode == "25");
}
assert!(enter.contains("[?25l") && leave.contains("[?25h"));
}
#[test]
fn minimal_options_emit_minimal_bytes() {
let opts = EnterOptions {
alternate_screen: false,
hide_cursor: false,
mouse: MouseMode::Off,
bracketed_paste: false,
focus_events: false,
kitty_keyboard: KittyFlags(0),
};
assert!(opts.enter_bytes().is_empty());
assert_eq!(opts.leave_bytes(), b"\x1b[?2026l\x1b[0m".to_vec());
}
#[test]
fn any_motion_uses_1003() {
let opts = EnterOptions {
mouse: MouseMode::AnyMotion,
..EnterOptions::default()
};
let enter = String::from_utf8(opts.enter_bytes()).unwrap();
assert!(enter.contains("[?1003h"));
assert!(!enter.contains("[?1002h"));
}
}