#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum CursorStyle {
#[default]
Default,
BlinkingBlock,
SteadyBlock,
BlinkingUnderline,
SteadyUnderline,
BlinkingBar,
SteadyBar,
}
impl CursorStyle {
pub(crate) const fn param(self) -> u8 {
match self {
CursorStyle::Default => 0,
CursorStyle::BlinkingBlock => 1,
CursorStyle::SteadyBlock => 2,
CursorStyle::BlinkingUnderline => 3,
CursorStyle::SteadyUnderline => 4,
CursorStyle::BlinkingBar => 5,
CursorStyle::SteadyBar => 6,
}
}
}
pub(crate) fn cursor_style_bytes(style: CursorStyle) -> Vec<u8> {
format!("\x1b[{} q", style.param()).into_bytes()
}
pub(crate) const CURSOR_STYLE_RESET: &[u8] = b"\x1b[0 q";
fn sanitize_osc_text(text: &str) -> String {
text.chars().filter(|c| !c.is_control()).collect()
}
pub(crate) fn set_title_bytes(title: &str) -> Vec<u8> {
format!("\x1b]0;{}\x1b\\", sanitize_osc_text(title)).into_bytes()
}
pub(crate) const TITLE_PUSH: &[u8] = b"\x1b[22;0t";
pub(crate) const TITLE_POP: &[u8] = b"\x1b[23;0t";
pub(crate) fn clipboard_copy_bytes(text: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(16 + text.len() * 4 / 3 + 4);
out.extend_from_slice(b"\x1b]52;c;");
base64_into(text.as_bytes(), &mut out);
out.extend_from_slice(b"\x1b\\");
out
}
fn base64_into(data: &[u8], out: &mut Vec<u8>) {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for chunk in data.chunks(3) {
let n = (u32::from(chunk[0]) << 16)
| (u32::from(*chunk.get(1).unwrap_or(&0)) << 8)
| u32::from(*chunk.get(2).unwrap_or(&0));
out.push(TABLE[(n >> 18) as usize & 63]);
out.push(TABLE[(n >> 12) as usize & 63]);
out.push(if chunk.len() > 1 {
TABLE[(n >> 6) as usize & 63]
} else {
b'='
});
out.push(if chunk.len() > 2 {
TABLE[n as usize & 63]
} else {
b'='
});
}
}
pub(crate) const PIXEL_MOUSE_ON: &[u8] = b"\x1b[?1016h";
pub(crate) const PIXEL_MOUSE_OFF: &[u8] = b"\x1b[?1016l";
pub(crate) const BELL: &[u8] = b"\x07";
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NotifyChannel {
Osc9,
Osc99,
BellOnly,
}
pub(crate) fn notify_bytes(message: &str) -> Vec<u8> {
format!("\x1b]9;{}\x1b\\", sanitize_osc_text(message)).into_bytes()
}
pub(crate) fn notify_bytes_osc99(message: &str) -> Vec<u8> {
format!("\x1b]99;;{}\x1b\\", sanitize_osc_text(message)).into_bytes()
}
pub fn tmux_wrap(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len() + payload.len() / 8 + 16);
out.extend_from_slice(b"\x1bPtmux;");
for &b in payload {
if b == 0x1b {
out.push(0x1b);
}
out.push(b);
}
out.extend_from_slice(b"\x1b\\");
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_style_sequences() {
assert_eq!(cursor_style_bytes(CursorStyle::Default), b"\x1b[0 q");
assert_eq!(cursor_style_bytes(CursorStyle::BlinkingBlock), b"\x1b[1 q");
assert_eq!(cursor_style_bytes(CursorStyle::SteadyBlock), b"\x1b[2 q");
assert_eq!(
cursor_style_bytes(CursorStyle::BlinkingUnderline),
b"\x1b[3 q"
);
assert_eq!(
cursor_style_bytes(CursorStyle::SteadyUnderline),
b"\x1b[4 q"
);
assert_eq!(cursor_style_bytes(CursorStyle::BlinkingBar), b"\x1b[5 q");
assert_eq!(cursor_style_bytes(CursorStyle::SteadyBar), b"\x1b[6 q");
assert_eq!(CURSOR_STYLE_RESET, b"\x1b[0 q");
}
#[test]
fn title_bytes_and_injection_defense() {
assert_eq!(set_title_bytes("hello"), b"\x1b]0;hello\x1b\\");
assert_eq!(
set_title_bytes("evil\x1b]710;x\x07name"),
b"\x1b]0;evil]710;xname\x1b\\".to_vec()
);
assert_eq!(
set_title_bytes("héllo — ✓"),
"\x1b]0;héllo — ✓\x1b\\".as_bytes()
);
}
#[test]
fn base64_rfc4648_vectors() {
fn enc(s: &str) -> String {
let mut v = Vec::new();
base64_into(s.as_bytes(), &mut v);
String::from_utf8(v).unwrap()
}
assert_eq!(enc(""), "");
assert_eq!(enc("f"), "Zg==");
assert_eq!(enc("fo"), "Zm8=");
assert_eq!(enc("foo"), "Zm9v");
assert_eq!(enc("foob"), "Zm9vYg==");
assert_eq!(enc("fooba"), "Zm9vYmE=");
assert_eq!(enc("foobar"), "Zm9vYmFy");
}
#[test]
fn clipboard_frame_shape() {
assert_eq!(clipboard_copy_bytes("hello"), b"\x1b]52;c;aGVsbG8=\x1b\\");
assert_eq!(clipboard_copy_bytes(""), b"\x1b]52;c;\x1b\\");
let bytes = clipboard_copy_bytes("\x1b]52;c;evil\x07");
let inner = &bytes[b"\x1b]52;c;".len()..bytes.len() - 2];
assert!(inner
.iter()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')));
}
#[test]
fn notify_and_bell() {
assert_eq!(notify_bytes("build done"), b"\x1b]9;build done\x1b\\");
assert_eq!(notify_bytes("a\x1bb"), b"\x1b]9;ab\x1b\\".to_vec());
assert_eq!(BELL, b"\x07");
assert_eq!(
notify_bytes_osc99("build done"),
b"\x1b]99;;build done\x1b\\"
);
assert_eq!(
notify_bytes_osc99("a\x1b]99;evil\x07b"),
b"\x1b]99;;a]99;evilb\x1b\\".to_vec()
);
}
#[test]
fn tmux_wrap_doubles_escapes() {
let wrapped = tmux_wrap(b"\x1b_Ga=q;AAAA\x1b\\");
assert_eq!(
wrapped,
b"\x1bPtmux;\x1b\x1b_Ga=q;AAAA\x1b\x1b\\\x1b\\".to_vec()
);
assert_eq!(tmux_wrap(b"plain"), b"\x1bPtmux;plain\x1b\\".to_vec());
}
}