#![forbid(unsafe_code)]
use std::io::{self, Write};
use crate::cell::{PackedRgba, StyleFlags};
const MAX_OSC8_FIELD_BYTES: usize = 4096;
#[inline]
fn osc8_field_is_safe(value: &str) -> bool {
value.len() <= MAX_OSC8_FIELD_BYTES && !value.chars().any(char::is_control)
}
pub const SGR_RESET: &[u8] = b"\x1b[0m";
#[inline]
pub fn sgr_reset<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(SGR_RESET)
}
#[derive(Debug, Clone, Copy)]
pub struct SgrCodes {
pub on: u8,
pub off: u8,
}
pub const SGR_BOLD: SgrCodes = SgrCodes { on: 1, off: 22 };
pub const SGR_DIM: SgrCodes = SgrCodes { on: 2, off: 22 };
pub const SGR_ITALIC: SgrCodes = SgrCodes { on: 3, off: 23 };
pub const SGR_UNDERLINE: SgrCodes = SgrCodes { on: 4, off: 24 };
pub const SGR_BLINK: SgrCodes = SgrCodes { on: 5, off: 25 };
pub const SGR_REVERSE: SgrCodes = SgrCodes { on: 7, off: 27 };
pub const SGR_HIDDEN: SgrCodes = SgrCodes { on: 8, off: 28 };
pub const SGR_STRIKETHROUGH: SgrCodes = SgrCodes { on: 9, off: 29 };
#[must_use]
pub const fn sgr_codes_for_flag(flag: StyleFlags) -> Option<SgrCodes> {
match flag.bits() {
0b0000_0001 => Some(SGR_BOLD),
0b0000_0010 => Some(SGR_DIM),
0b0000_0100 => Some(SGR_ITALIC),
0b0000_1000 => Some(SGR_UNDERLINE),
0b0001_0000 => Some(SGR_BLINK),
0b0010_0000 => Some(SGR_REVERSE),
0b1000_0000 => Some(SGR_HIDDEN),
0b0100_0000 => Some(SGR_STRIKETHROUGH),
_ => None,
}
}
#[inline]
fn write_u8_dec(buf: &mut [u8], n: u8) -> usize {
if n >= 100 {
let hundreds = n / 100;
let tens = (n / 10) % 10;
let ones = n % 10;
buf[0] = b'0' + hundreds;
buf[1] = b'0' + tens;
buf[2] = b'0' + ones;
3
} else if n >= 10 {
let tens = n / 10;
let ones = n % 10;
buf[0] = b'0' + tens;
buf[1] = b'0' + ones;
2
} else {
buf[0] = b'0' + n;
1
}
}
#[inline]
fn write_u32_dec(buf: &mut [u8], mut n: u32) -> usize {
let mut rev = [0u8; 10];
let mut len = 0usize;
loop {
rev[len] = (n % 10) as u8;
len += 1;
n /= 10;
if n == 0 {
break;
}
}
for i in 0..len {
buf[i] = b'0' + rev[len - 1 - i];
}
len
}
#[inline]
fn write_sgr_code<W: Write>(w: &mut W, code: u8) -> io::Result<()> {
let mut buf = [0u8; 6];
buf[0] = 0x1b;
buf[1] = b'[';
let len = write_u8_dec(&mut buf[2..], code);
buf[2 + len] = b'm';
w.write_all(&buf[..2 + len + 1])
}
pub fn sgr_flags<W: Write>(w: &mut W, flags: StyleFlags) -> io::Result<()> {
if flags.is_empty() {
return Ok(());
}
let bits = flags.bits();
if bits.is_power_of_two()
&& let Some(seq) = sgr_single_flag_seq(bits)
{
return w.write_all(seq);
}
let mut buf = [0u8; 32];
let mut idx = 0usize;
buf[idx] = 0x1b;
buf[idx + 1] = b'[';
idx += 2;
let mut first = true;
for (flag, codes) in FLAG_TABLE {
if flags.contains(flag) {
if !first {
buf[idx] = b';';
idx += 1;
}
idx += write_u8_dec(&mut buf[idx..], codes.on);
first = false;
}
}
buf[idx] = b'm';
idx += 1;
w.write_all(&buf[..idx])
}
pub const FLAG_TABLE: [(StyleFlags, SgrCodes); 8] = [
(StyleFlags::BOLD, SGR_BOLD),
(StyleFlags::DIM, SGR_DIM),
(StyleFlags::ITALIC, SGR_ITALIC),
(StyleFlags::UNDERLINE, SGR_UNDERLINE),
(StyleFlags::BLINK, SGR_BLINK),
(StyleFlags::REVERSE, SGR_REVERSE),
(StyleFlags::HIDDEN, SGR_HIDDEN),
(StyleFlags::STRIKETHROUGH, SGR_STRIKETHROUGH),
];
#[inline]
fn sgr_single_flag_seq(bits: u8) -> Option<&'static [u8]> {
match bits {
0b0000_0001 => Some(b"\x1b[1m"), 0b0000_0010 => Some(b"\x1b[2m"), 0b0000_0100 => Some(b"\x1b[3m"), 0b0000_1000 => Some(b"\x1b[4m"), 0b0001_0000 => Some(b"\x1b[5m"), 0b0010_0000 => Some(b"\x1b[7m"), 0b0100_0000 => Some(b"\x1b[9m"), 0b1000_0000 => Some(b"\x1b[8m"), _ => None,
}
}
#[inline]
fn sgr_single_flag_off_seq(bits: u8) -> Option<&'static [u8]> {
match bits {
0b0000_0001 => Some(b"\x1b[22m"), 0b0000_0010 => Some(b"\x1b[22m"), 0b0000_0100 => Some(b"\x1b[23m"), 0b0000_1000 => Some(b"\x1b[24m"), 0b0001_0000 => Some(b"\x1b[25m"), 0b0010_0000 => Some(b"\x1b[27m"), 0b0100_0000 => Some(b"\x1b[29m"), 0b1000_0000 => Some(b"\x1b[28m"), _ => None,
}
}
pub fn sgr_flags_off<W: Write>(
w: &mut W,
flags_to_disable: StyleFlags,
flags_to_keep: StyleFlags,
) -> io::Result<StyleFlags> {
if flags_to_disable.is_empty() {
return Ok(StyleFlags::empty());
}
let disable_bits = flags_to_disable.bits();
if disable_bits.is_power_of_two()
&& let Some(seq) = sgr_single_flag_off_seq(disable_bits)
{
w.write_all(seq)?;
if disable_bits == StyleFlags::BOLD.bits() && flags_to_keep.contains(StyleFlags::DIM) {
return Ok(StyleFlags::DIM);
}
if disable_bits == StyleFlags::DIM.bits() && flags_to_keep.contains(StyleFlags::BOLD) {
return Ok(StyleFlags::BOLD);
}
return Ok(StyleFlags::empty());
}
let mut collateral = StyleFlags::empty();
for (flag, codes) in FLAG_TABLE {
if !flags_to_disable.contains(flag) {
continue;
}
write_sgr_code(w, codes.off)?;
if codes.off == 22 {
let other = if flag == StyleFlags::BOLD {
StyleFlags::DIM
} else {
StyleFlags::BOLD
};
if flags_to_keep.contains(other) && !flags_to_disable.contains(other) {
collateral |= other;
}
}
}
Ok(collateral)
}
const SGR_FG_RGB_PREFIX: &[u8] = b"\x1b[38;2;";
const SGR_BG_RGB_PREFIX: &[u8] = b"\x1b[48;2;";
#[inline]
fn write_sgr_rgb_seq<W: Write>(w: &mut W, prefix: &[u8], r: u8, g: u8, b: u8) -> io::Result<()> {
let mut buf = [0u8; 20];
let mut idx = 0usize;
buf[..prefix.len()].copy_from_slice(prefix);
idx += prefix.len();
idx += write_u8_dec(&mut buf[idx..], r);
buf[idx] = b';';
idx += 1;
idx += write_u8_dec(&mut buf[idx..], g);
buf[idx] = b';';
idx += 1;
idx += write_u8_dec(&mut buf[idx..], b);
buf[idx] = b'm';
idx += 1;
w.write_all(&buf[..idx])
}
#[inline]
fn write_csi_u32_suffix<W: Write>(w: &mut W, value: u32, suffix: u8) -> io::Result<()> {
let mut buf = [0u8; 16];
buf[0] = 0x1b;
buf[1] = b'[';
let len = write_u32_dec(&mut buf[2..], value);
buf[2 + len] = suffix;
w.write_all(&buf[..3 + len])
}
pub fn sgr_fg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
write_sgr_rgb_seq(w, SGR_FG_RGB_PREFIX, r, g, b)
}
pub fn sgr_bg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
write_sgr_rgb_seq(w, SGR_BG_RGB_PREFIX, r, g, b)
}
pub fn sgr_fg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
write!(w, "\x1b[38;5;{index}m")
}
pub fn sgr_bg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
write!(w, "\x1b[48;5;{index}m")
}
pub fn sgr_fg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
let code = if index < 8 {
30 + index
} else {
90 + index - 8
};
write!(w, "\x1b[{code}m")
}
pub fn sgr_bg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
let code = if index < 8 {
40 + index
} else {
100 + index - 8
};
write!(w, "\x1b[{code}m")
}
pub fn sgr_fg_default<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(b"\x1b[39m")
}
pub fn sgr_bg_default<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(b"\x1b[49m")
}
pub fn sgr_fg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
if color.a() == 0 {
return sgr_fg_default(w);
}
sgr_fg_rgb(w, color.r(), color.g(), color.b())
}
pub fn sgr_bg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
if color.a() == 0 {
return sgr_bg_default(w);
}
sgr_bg_rgb(w, color.r(), color.g(), color.b())
}
pub fn cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
let mut buf = [0u8; 16];
let mut idx = 0usize;
buf[idx] = 0x1b;
buf[idx + 1] = b'[';
idx += 2;
idx += write_u32_dec(&mut buf[idx..], (row as u32) + 1);
buf[idx] = b';';
idx += 1;
idx += write_u32_dec(&mut buf[idx..], (col as u32) + 1);
buf[idx] = b'H';
idx += 1;
w.write_all(&buf[..idx])
}
pub fn cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
write_csi_u32_suffix(w, (col as u32) + 1, b'G')
}
pub fn cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
if n == 0 {
return Ok(());
}
if n == 1 {
w.write_all(b"\x1b[A")
} else {
write_csi_u32_suffix(w, n as u32, b'A')
}
}
pub fn cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
if n == 0 {
return Ok(());
}
if n == 1 {
w.write_all(b"\x1b[B")
} else {
write_csi_u32_suffix(w, n as u32, b'B')
}
}
pub fn cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
if n == 0 {
return Ok(());
}
if n == 1 {
w.write_all(b"\x1b[C")
} else {
write_csi_u32_suffix(w, n as u32, b'C')
}
}
pub fn cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
if n == 0 {
return Ok(());
}
if n == 1 {
w.write_all(b"\x1b[D")
} else {
write_csi_u32_suffix(w, n as u32, b'D')
}
}
#[inline]
pub fn cr<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(b"\r")
}
#[inline]
pub fn lf<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(b"\n")
}
pub const CURSOR_SAVE: &[u8] = b"\x1b7";
pub const CURSOR_RESTORE: &[u8] = b"\x1b8";
#[inline]
pub fn cursor_save<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(CURSOR_SAVE)
}
#[inline]
pub fn cursor_restore<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(CURSOR_RESTORE)
}
pub const CURSOR_HIDE: &[u8] = b"\x1b[?25l";
pub const CURSOR_SHOW: &[u8] = b"\x1b[?25h";
#[inline]
pub fn cursor_hide<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(CURSOR_HIDE)
}
#[inline]
pub fn cursor_show<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(CURSOR_SHOW)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EraseLineMode {
ToEnd = 0,
ToStart = 1,
All = 2,
}
pub fn erase_line<W: Write>(w: &mut W, mode: EraseLineMode) -> io::Result<()> {
match mode {
EraseLineMode::ToEnd => w.write_all(b"\x1b[K"),
EraseLineMode::ToStart => w.write_all(b"\x1b[1K"),
EraseLineMode::All => w.write_all(b"\x1b[2K"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EraseDisplayMode {
ToEnd = 0,
ToStart = 1,
All = 2,
Scrollback = 3,
}
pub fn erase_display<W: Write>(w: &mut W, mode: EraseDisplayMode) -> io::Result<()> {
match mode {
EraseDisplayMode::ToEnd => w.write_all(b"\x1b[J"),
EraseDisplayMode::ToStart => w.write_all(b"\x1b[1J"),
EraseDisplayMode::All => w.write_all(b"\x1b[2J"),
EraseDisplayMode::Scrollback => w.write_all(b"\x1b[3J"),
}
}
pub fn set_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
write!(w, "\x1b[{};{}r", (top as u32) + 1, (bottom as u32) + 1)
}
pub const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
#[inline]
pub fn reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(RESET_SCROLL_REGION)
}
pub const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
pub const SYNC_END: &[u8] = b"\x1b[?2026l";
#[inline]
pub fn sync_begin<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(SYNC_BEGIN)
}
#[inline]
pub fn sync_end<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(SYNC_END)
}
pub fn hyperlink_start<W: Write>(w: &mut W, url: &str) -> io::Result<()> {
if !osc8_field_is_safe(url) {
return Ok(());
}
write!(w, "\x1b]8;;{url}\x07")
}
pub fn hyperlink_end<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(b"\x1b]8;;\x07")
}
pub fn hyperlink_start_with_id<W: Write>(w: &mut W, id: &str, url: &str) -> io::Result<()> {
if !osc8_field_is_safe(url) || !osc8_field_is_safe(id) || id.contains(';') {
return Ok(());
}
write!(w, "\x1b]8;id={id};{url}\x07")
}
pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
pub const ALT_SCREEN_LEAVE: &[u8] = b"\x1b[?1049l";
pub const BRACKETED_PASTE_ENABLE: &[u8] = b"\x1b[?2004h";
pub const BRACKETED_PASTE_DISABLE: &[u8] = b"\x1b[?2004l";
pub const MOUSE_ENABLE: &[u8] = b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1006;1000;1002h\x1b[?1006h\x1b[?1000h\x1b[?1002h";
pub const MOUSE_DISABLE: &[u8] = b"\x1b[?1000;1002;1006l\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l";
pub const FOCUS_ENABLE: &[u8] = b"\x1b[?1004h";
pub const FOCUS_DISABLE: &[u8] = b"\x1b[?1004l";
#[inline]
pub fn alt_screen_enter<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(ALT_SCREEN_ENTER)
}
#[inline]
pub fn alt_screen_leave<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(ALT_SCREEN_LEAVE)
}
#[inline]
pub fn bracketed_paste_enable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(BRACKETED_PASTE_ENABLE)
}
#[inline]
pub fn bracketed_paste_disable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(BRACKETED_PASTE_DISABLE)
}
#[inline]
pub fn mouse_enable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(MOUSE_ENABLE)
}
#[inline]
pub fn mouse_disable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(MOUSE_DISABLE)
}
#[inline]
pub fn focus_enable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(FOCUS_ENABLE)
}
#[inline]
pub fn focus_disable<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(FOCUS_DISABLE)
}
#[cfg(test)]
mod tests {
use super::*;
fn to_bytes<F: FnOnce(&mut Vec<u8>) -> io::Result<()>>(f: F) -> Vec<u8> {
let mut buf = Vec::new();
f(&mut buf).unwrap();
buf
}
#[test]
fn sgr_reset_bytes() {
assert_eq!(to_bytes(sgr_reset), b"\x1b[0m");
}
#[test]
fn sgr_flags_bold() {
assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)), b"\x1b[1m");
}
#[test]
fn sgr_flags_multiple() {
let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
assert_eq!(to_bytes(|w| sgr_flags(w, flags)), b"\x1b[1;3;4m");
}
#[test]
fn sgr_flags_empty() {
assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::empty())), b"");
}
#[test]
fn sgr_fg_rgb_bytes() {
assert_eq!(
to_bytes(|w| sgr_fg_rgb(w, 255, 128, 0)),
b"\x1b[38;2;255;128;0m"
);
}
#[test]
fn sgr_bg_rgb_bytes() {
assert_eq!(to_bytes(|w| sgr_bg_rgb(w, 0, 0, 0)), b"\x1b[48;2;0;0;0m");
}
#[test]
fn dynamic_sgr_rgb_matches_reference_formatting() {
for (r, g, b) in [(0, 0, 0), (1, 2, 3), (9, 10, 99), (100, 200, 255)] {
assert_eq!(
to_bytes(|w| sgr_fg_rgb(w, r, g, b)),
format!("\x1b[38;2;{r};{g};{b}m").into_bytes()
);
assert_eq!(
to_bytes(|w| sgr_bg_rgb(w, r, g, b)),
format!("\x1b[48;2;{r};{g};{b}m").into_bytes()
);
}
}
#[test]
fn sgr_fg_256_bytes() {
assert_eq!(to_bytes(|w| sgr_fg_256(w, 196)), b"\x1b[38;5;196m");
}
#[test]
fn sgr_bg_256_bytes() {
assert_eq!(to_bytes(|w| sgr_bg_256(w, 232)), b"\x1b[48;5;232m");
}
#[test]
fn sgr_fg_16_normal() {
assert_eq!(to_bytes(|w| sgr_fg_16(w, 1)), b"\x1b[31m"); assert_eq!(to_bytes(|w| sgr_fg_16(w, 7)), b"\x1b[37m"); }
#[test]
fn sgr_fg_16_bright() {
assert_eq!(to_bytes(|w| sgr_fg_16(w, 9)), b"\x1b[91m"); assert_eq!(to_bytes(|w| sgr_fg_16(w, 15)), b"\x1b[97m"); }
#[test]
fn sgr_bg_16_normal() {
assert_eq!(to_bytes(|w| sgr_bg_16(w, 0)), b"\x1b[40m"); assert_eq!(to_bytes(|w| sgr_bg_16(w, 4)), b"\x1b[44m"); }
#[test]
fn sgr_bg_16_bright() {
assert_eq!(to_bytes(|w| sgr_bg_16(w, 8)), b"\x1b[100m"); assert_eq!(to_bytes(|w| sgr_bg_16(w, 12)), b"\x1b[104m"); }
#[test]
fn sgr_default_colors() {
assert_eq!(to_bytes(sgr_fg_default), b"\x1b[39m");
assert_eq!(to_bytes(sgr_bg_default), b"\x1b[49m");
}
#[test]
fn sgr_packed_transparent_uses_default() {
assert_eq!(
to_bytes(|w| sgr_fg_packed(w, PackedRgba::TRANSPARENT)),
b"\x1b[39m"
);
assert_eq!(
to_bytes(|w| sgr_bg_packed(w, PackedRgba::TRANSPARENT)),
b"\x1b[49m"
);
}
#[test]
fn sgr_packed_opaque() {
let color = PackedRgba::rgb(10, 20, 30);
assert_eq!(
to_bytes(|w| sgr_fg_packed(w, color)),
b"\x1b[38;2;10;20;30m"
);
}
#[test]
fn cup_1_indexed() {
assert_eq!(to_bytes(|w| cup(w, 0, 0)), b"\x1b[1;1H");
assert_eq!(to_bytes(|w| cup(w, 23, 79)), b"\x1b[24;80H");
}
#[test]
fn cha_1_indexed() {
assert_eq!(to_bytes(|w| cha(w, 0)), b"\x1b[1G");
assert_eq!(to_bytes(|w| cha(w, 79)), b"\x1b[80G");
}
#[test]
fn cursor_relative_moves() {
assert_eq!(to_bytes(|w| cuu(w, 1)), b"\x1b[A");
assert_eq!(to_bytes(|w| cuu(w, 5)), b"\x1b[5A");
assert_eq!(to_bytes(|w| cud(w, 1)), b"\x1b[B");
assert_eq!(to_bytes(|w| cud(w, 3)), b"\x1b[3B");
assert_eq!(to_bytes(|w| cuf(w, 1)), b"\x1b[C");
assert_eq!(to_bytes(|w| cuf(w, 10)), b"\x1b[10C");
assert_eq!(to_bytes(|w| cub(w, 1)), b"\x1b[D");
assert_eq!(to_bytes(|w| cub(w, 2)), b"\x1b[2D");
}
#[test]
fn cursor_relative_zero_is_noop() {
assert_eq!(to_bytes(|w| cuu(w, 0)), b"");
assert_eq!(to_bytes(|w| cud(w, 0)), b"");
assert_eq!(to_bytes(|w| cuf(w, 0)), b"");
assert_eq!(to_bytes(|w| cub(w, 0)), b"");
}
#[test]
fn dynamic_cursor_sequences_match_reference_formatting() {
for (row, col) in [(0, 0), (23, 79), (999, 999), (u16::MAX, u16::MAX)] {
assert_eq!(
to_bytes(|w| cup(w, row, col)),
format!("\x1b[{};{}H", (row as u32) + 1, (col as u32) + 1).into_bytes()
);
}
for col in [0, 79, 999, u16::MAX] {
assert_eq!(
to_bytes(|w| cha(w, col)),
format!("\x1b[{}G", (col as u32) + 1).into_bytes()
);
}
for n in [0, 1, 2, 10, 999, u16::MAX] {
let expected_up = if n == 0 {
Vec::new()
} else if n == 1 {
b"\x1b[A".to_vec()
} else {
format!("\x1b[{n}A").into_bytes()
};
let expected_down = if n == 0 {
Vec::new()
} else if n == 1 {
b"\x1b[B".to_vec()
} else {
format!("\x1b[{n}B").into_bytes()
};
let expected_forward = if n == 0 {
Vec::new()
} else if n == 1 {
b"\x1b[C".to_vec()
} else {
format!("\x1b[{n}C").into_bytes()
};
let expected_back = if n == 0 {
Vec::new()
} else if n == 1 {
b"\x1b[D".to_vec()
} else {
format!("\x1b[{n}D").into_bytes()
};
assert_eq!(to_bytes(|w| cuu(w, n)), expected_up);
assert_eq!(to_bytes(|w| cud(w, n)), expected_down);
assert_eq!(to_bytes(|w| cuf(w, n)), expected_forward);
assert_eq!(to_bytes(|w| cub(w, n)), expected_back);
}
}
#[test]
fn cursor_save_restore() {
assert_eq!(to_bytes(cursor_save), b"\x1b7");
assert_eq!(to_bytes(cursor_restore), b"\x1b8");
}
#[test]
fn cursor_visibility() {
assert_eq!(to_bytes(cursor_hide), b"\x1b[?25l");
assert_eq!(to_bytes(cursor_show), b"\x1b[?25h");
}
#[test]
fn erase_line_modes() {
assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::ToEnd)), b"\x1b[K");
assert_eq!(
to_bytes(|w| erase_line(w, EraseLineMode::ToStart)),
b"\x1b[1K"
);
assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::All)), b"\x1b[2K");
}
#[test]
fn erase_display_modes() {
assert_eq!(
to_bytes(|w| erase_display(w, EraseDisplayMode::ToEnd)),
b"\x1b[J"
);
assert_eq!(
to_bytes(|w| erase_display(w, EraseDisplayMode::ToStart)),
b"\x1b[1J"
);
assert_eq!(
to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
b"\x1b[2J"
);
assert_eq!(
to_bytes(|w| erase_display(w, EraseDisplayMode::Scrollback)),
b"\x1b[3J"
);
}
#[test]
fn scroll_region_1_indexed() {
assert_eq!(to_bytes(|w| set_scroll_region(w, 0, 23)), b"\x1b[1;24r");
assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 20)), b"\x1b[6;21r");
}
#[test]
fn scroll_region_reset() {
assert_eq!(to_bytes(reset_scroll_region), b"\x1b[r");
}
#[test]
fn sync_output() {
assert_eq!(to_bytes(sync_begin), b"\x1b[?2026h");
assert_eq!(to_bytes(sync_end), b"\x1b[?2026l");
}
#[test]
fn hyperlink_basic() {
assert_eq!(
to_bytes(|w| hyperlink_start(w, "https://example.com")),
b"\x1b]8;;https://example.com\x07"
);
assert_eq!(to_bytes(hyperlink_end), b"\x1b]8;;\x07");
}
#[test]
fn hyperlink_with_id() {
assert_eq!(
to_bytes(|w| hyperlink_start_with_id(w, "link1", "https://example.com")),
b"\x1b]8;id=link1;https://example.com\x07"
);
}
#[test]
fn hyperlink_rejects_control_chars() {
assert_eq!(
to_bytes(|w| hyperlink_start(w, "https://exa\x1bmple.com")),
b""
);
assert_eq!(
to_bytes(|w| hyperlink_start_with_id(w, "id", "https://exa\u{009d}mple.com")),
b""
);
}
#[test]
fn hyperlink_with_id_rejects_parameter_breakout() {
assert_eq!(
to_bytes(|w| hyperlink_start_with_id(w, "id;malicious=1", "https://example.com")),
b""
);
}
#[test]
fn hyperlink_rejects_overlong_fields() {
let long_url = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
assert_eq!(to_bytes(|w| hyperlink_start(w, &long_url)), b"");
let long_id = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
assert_eq!(
to_bytes(|w| hyperlink_start_with_id(w, &long_id, "https://example.com")),
b""
);
}
#[test]
fn alt_screen() {
assert_eq!(to_bytes(alt_screen_enter), b"\x1b[?1049h");
assert_eq!(to_bytes(alt_screen_leave), b"\x1b[?1049l");
}
#[test]
fn bracketed_paste() {
assert_eq!(to_bytes(bracketed_paste_enable), b"\x1b[?2004h");
assert_eq!(to_bytes(bracketed_paste_disable), b"\x1b[?2004l");
}
#[test]
fn mouse_mode() {
assert_eq!(
to_bytes(mouse_enable),
b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1006;1000;1002h\x1b[?1006h\x1b[?1000h\x1b[?1002h"
);
assert_eq!(
to_bytes(mouse_disable),
b"\x1b[?1000;1002;1006l\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l"
);
let enabled = to_bytes(mouse_enable);
assert!(
!enabled.ends_with(b"\x1b[?1016l"),
"mouse enable should not end with 1016l (can force X10 fallback)"
);
let pos_1016l = enabled
.windows(b"\x1b[?1016l".len())
.position(|w| w == b"\x1b[?1016l")
.expect("mouse enable should clear 1016 before enabling SGR");
let pos_1006h = enabled
.windows(b"\x1b[?1006h".len())
.position(|w| w == b"\x1b[?1006h")
.expect("mouse enable should include 1006h");
assert!(
pos_1016l < pos_1006h,
"1016l must be emitted before 1006h to preserve SGR mode"
);
}
#[test]
fn focus_mode() {
assert_eq!(to_bytes(focus_enable), b"\x1b[?1004h");
assert_eq!(to_bytes(focus_disable), b"\x1b[?1004l");
}
#[test]
fn all_sequences_are_ascii() {
for seq in [
SGR_RESET,
CURSOR_SAVE,
CURSOR_RESTORE,
CURSOR_HIDE,
CURSOR_SHOW,
RESET_SCROLL_REGION,
SYNC_BEGIN,
SYNC_END,
ALT_SCREEN_ENTER,
ALT_SCREEN_LEAVE,
BRACKETED_PASTE_ENABLE,
BRACKETED_PASTE_DISABLE,
MOUSE_ENABLE,
MOUSE_DISABLE,
FOCUS_ENABLE,
FOCUS_DISABLE,
] {
for &byte in seq {
assert!(byte < 128, "Non-ASCII byte {byte:#x} in sequence");
}
}
}
#[test]
fn osc_sequences_are_terminated() {
let link_start = to_bytes(|w| hyperlink_start(w, "test"));
assert!(
link_start.ends_with(b"\x07"),
"hyperlink_start not terminated with BEL"
);
let link_end = to_bytes(hyperlink_end);
assert!(
link_end.ends_with(b"\x07"),
"hyperlink_end not terminated with BEL"
);
let link_id = to_bytes(|w| hyperlink_start_with_id(w, "id", "url"));
assert!(
link_id.ends_with(b"\x07"),
"hyperlink_start_with_id not terminated with BEL"
);
}
#[test]
fn sgr_flags_off_empty_is_noop() {
let bytes = to_bytes(|w| {
sgr_flags_off(w, StyleFlags::empty(), StyleFlags::empty()).unwrap();
Ok(())
});
assert!(bytes.is_empty(), "disabling no flags should emit nothing");
}
#[test]
fn sgr_flags_off_single_bold() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[22m");
assert!(collateral.is_empty(), "no collateral when DIM is not kept");
}
#[test]
fn sgr_flags_off_single_dim() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[22m");
assert!(collateral.is_empty(), "no collateral when BOLD is not kept");
}
#[test]
fn sgr_flags_off_bold_collateral_dim() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::DIM).unwrap();
assert_eq!(buf, b"\x1b[22m");
assert_eq!(collateral, StyleFlags::DIM);
}
#[test]
fn sgr_flags_off_dim_collateral_bold() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::BOLD).unwrap();
assert_eq!(buf, b"\x1b[22m");
assert_eq!(collateral, StyleFlags::BOLD);
}
#[test]
fn sgr_flags_off_italic() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::ITALIC, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[23m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_underline() {
let mut buf = Vec::new();
let collateral =
sgr_flags_off(&mut buf, StyleFlags::UNDERLINE, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[24m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_blink() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::BLINK, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[25m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_reverse() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::REVERSE, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[27m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_hidden() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, StyleFlags::HIDDEN, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[28m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_strikethrough() {
let mut buf = Vec::new();
let collateral =
sgr_flags_off(&mut buf, StyleFlags::STRIKETHROUGH, StyleFlags::empty()).unwrap();
assert_eq!(buf, b"\x1b[29m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_multi_no_bold_dim_overlap() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(
&mut buf,
StyleFlags::ITALIC | StyleFlags::UNDERLINE,
StyleFlags::empty(),
)
.unwrap();
assert_eq!(buf, b"\x1b[23m\x1b[24m");
assert!(collateral.is_empty());
}
#[test]
fn sgr_flags_off_bold_and_dim_together() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(
&mut buf,
StyleFlags::BOLD | StyleFlags::DIM,
StyleFlags::empty(),
)
.unwrap();
assert_eq!(buf, b"\x1b[22m\x1b[22m");
assert!(
collateral.is_empty(),
"no collateral when both are disabled"
);
}
#[test]
fn sgr_flags_off_overlap_keep_and_disable_does_not_report_collateral() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(
&mut buf,
StyleFlags::BOLD | StyleFlags::DIM,
StyleFlags::DIM,
)
.unwrap();
assert_eq!(buf, b"\x1b[22m\x1b[22m");
assert!(
collateral.is_empty(),
"DIM is explicitly disabled, so it must not be reported as collateral"
);
}
#[test]
fn sgr_flags_off_bold_dim_with_dim_kept() {
let mut buf = Vec::new();
let collateral = sgr_flags_off(
&mut buf,
StyleFlags::BOLD | StyleFlags::ITALIC,
StyleFlags::DIM,
)
.unwrap();
assert_eq!(
collateral,
StyleFlags::DIM,
"DIM should be collateral damage from BOLD off (code 22)"
);
}
#[test]
fn sgr_codes_for_all_single_flags() {
let cases = [
(StyleFlags::BOLD, 1, 22),
(StyleFlags::DIM, 2, 22),
(StyleFlags::ITALIC, 3, 23),
(StyleFlags::UNDERLINE, 4, 24),
(StyleFlags::BLINK, 5, 25),
(StyleFlags::REVERSE, 7, 27),
(StyleFlags::HIDDEN, 8, 28),
(StyleFlags::STRIKETHROUGH, 9, 29),
];
for (flag, expected_on, expected_off) in cases {
let codes = sgr_codes_for_flag(flag)
.unwrap_or_else(|| panic!("should return codes for {flag:?}"));
assert_eq!(codes.on, expected_on, "on code for {flag:?}");
assert_eq!(codes.off, expected_off, "off code for {flag:?}");
}
}
#[test]
fn sgr_codes_for_composite_flag_returns_none() {
let composite = StyleFlags::BOLD | StyleFlags::ITALIC;
assert!(
sgr_codes_for_flag(composite).is_none(),
"composite flags should return None"
);
}
#[test]
fn sgr_codes_for_empty_flag_returns_none() {
assert!(
sgr_codes_for_flag(StyleFlags::empty()).is_none(),
"empty flags should return None"
);
}
#[test]
fn sgr_codes_for_flag_matches_flag_table_entries() {
for (flag, expected) in FLAG_TABLE {
let actual = sgr_codes_for_flag(flag).expect("single-bit FLAG_TABLE entry");
assert_eq!(actual.on, expected.on, "{flag:?} on code");
assert_eq!(actual.off, expected.off, "{flag:?} off code");
}
}
#[test]
fn cr_emits_carriage_return() {
assert_eq!(to_bytes(cr), b"\r");
}
#[test]
fn lf_emits_line_feed() {
assert_eq!(to_bytes(lf), b"\n");
}
#[test]
fn sgr_flags_each_single_flag_fast_path() {
let cases: &[(StyleFlags, &[u8])] = &[
(StyleFlags::BOLD, b"\x1b[1m"),
(StyleFlags::DIM, b"\x1b[2m"),
(StyleFlags::ITALIC, b"\x1b[3m"),
(StyleFlags::UNDERLINE, b"\x1b[4m"),
(StyleFlags::BLINK, b"\x1b[5m"),
(StyleFlags::REVERSE, b"\x1b[7m"),
(StyleFlags::STRIKETHROUGH, b"\x1b[9m"),
(StyleFlags::HIDDEN, b"\x1b[8m"),
];
for &(flag, expected) in cases {
assert_eq!(
to_bytes(|w| sgr_flags(w, flag)),
expected,
"single-flag fast path for {flag:?}"
);
}
}
#[test]
fn sgr_flags_all_eight() {
let all = StyleFlags::BOLD
| StyleFlags::DIM
| StyleFlags::ITALIC
| StyleFlags::UNDERLINE
| StyleFlags::BLINK
| StyleFlags::REVERSE
| StyleFlags::HIDDEN
| StyleFlags::STRIKETHROUGH;
let bytes = to_bytes(|w| sgr_flags(w, all));
assert_eq!(bytes, b"\x1b[1;2;3;4;5;7;8;9m");
}
#[test]
fn sgr_code_single_digit() {
let mut buf = Vec::new();
write_sgr_code(&mut buf, 1).unwrap();
assert_eq!(buf, b"\x1b[1m");
}
#[test]
fn sgr_code_two_digits() {
let mut buf = Vec::new();
write_sgr_code(&mut buf, 22).unwrap();
assert_eq!(buf, b"\x1b[22m");
}
#[test]
fn sgr_code_three_digits() {
let mut buf = Vec::new();
write_sgr_code(&mut buf, 100).unwrap();
assert_eq!(buf, b"\x1b[100m");
}
#[test]
fn sgr_code_max_u8() {
let mut buf = Vec::new();
write_sgr_code(&mut buf, 255).unwrap();
assert_eq!(buf, b"\x1b[255m");
}
#[test]
fn sgr_code_zero() {
let mut buf = Vec::new();
write_sgr_code(&mut buf, 0).unwrap();
assert_eq!(buf, b"\x1b[0m");
}
#[test]
fn sgr_fg_16_boundary_7_to_8() {
assert_eq!(to_bytes(|w| sgr_fg_16(w, 7)), b"\x1b[37m");
assert_eq!(to_bytes(|w| sgr_fg_16(w, 8)), b"\x1b[90m");
}
#[test]
fn sgr_bg_16_boundary_7_to_8() {
assert_eq!(to_bytes(|w| sgr_bg_16(w, 7)), b"\x1b[47m");
assert_eq!(to_bytes(|w| sgr_bg_16(w, 8)), b"\x1b[100m");
}
#[test]
fn sgr_fg_16_first_color() {
assert_eq!(to_bytes(|w| sgr_fg_16(w, 0)), b"\x1b[30m"); }
#[test]
fn sgr_bg_16_last_bright() {
assert_eq!(to_bytes(|w| sgr_bg_16(w, 15)), b"\x1b[107m"); }
#[test]
fn sgr_fg_256_zero() {
assert_eq!(to_bytes(|w| sgr_fg_256(w, 0)), b"\x1b[38;5;0m");
}
#[test]
fn sgr_fg_256_max() {
assert_eq!(to_bytes(|w| sgr_fg_256(w, 255)), b"\x1b[38;5;255m");
}
#[test]
fn sgr_bg_256_zero() {
assert_eq!(to_bytes(|w| sgr_bg_256(w, 0)), b"\x1b[48;5;0m");
}
#[test]
fn sgr_bg_256_max() {
assert_eq!(to_bytes(|w| sgr_bg_256(w, 255)), b"\x1b[48;5;255m");
}
#[test]
fn cup_max_u16() {
let bytes = to_bytes(|w| cup(w, u16::MAX, u16::MAX));
let s = String::from_utf8(bytes).unwrap();
assert!(s.starts_with("\x1b["));
assert!(s.ends_with("H"));
}
#[test]
fn cha_max_u16() {
let bytes = to_bytes(|w| cha(w, u16::MAX));
let s = String::from_utf8(bytes).unwrap();
assert!(s.starts_with("\x1b["));
assert!(s.ends_with("G"));
}
#[test]
fn cursor_up_max() {
let bytes = to_bytes(|w| cuu(w, u16::MAX));
let s = String::from_utf8(bytes).unwrap();
assert!(s.contains("65535"));
assert!(s.ends_with("A"));
}
#[test]
fn scroll_region_same_top_bottom() {
assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 5)), b"\x1b[6;6r");
}
#[test]
fn sgr_flags_off_each_single_flag_fast_path() {
let cases: &[(StyleFlags, &[u8])] = &[
(StyleFlags::BOLD, b"\x1b[22m"),
(StyleFlags::DIM, b"\x1b[22m"),
(StyleFlags::ITALIC, b"\x1b[23m"),
(StyleFlags::UNDERLINE, b"\x1b[24m"),
(StyleFlags::BLINK, b"\x1b[25m"),
(StyleFlags::REVERSE, b"\x1b[27m"),
(StyleFlags::STRIKETHROUGH, b"\x1b[29m"),
(StyleFlags::HIDDEN, b"\x1b[28m"),
];
for &(flag, expected) in cases {
let mut buf = Vec::new();
let collateral = sgr_flags_off(&mut buf, flag, StyleFlags::empty()).unwrap();
assert_eq!(buf, expected, "off sequence for {flag:?}");
assert!(collateral.is_empty(), "no collateral for {flag:?}");
}
}
#[test]
fn sgr_bg_packed_opaque() {
let color = PackedRgba::rgb(100, 200, 50);
assert_eq!(
to_bytes(|w| sgr_bg_packed(w, color)),
b"\x1b[48;2;100;200;50m"
);
}
#[test]
fn hyperlink_empty_url() {
assert_eq!(to_bytes(|w| hyperlink_start(w, "")), b"\x1b]8;;\x07");
}
#[test]
fn hyperlink_with_empty_id() {
assert_eq!(
to_bytes(|w| hyperlink_start_with_id(w, "", "https://x.com")),
b"\x1b]8;id=;https://x.com\x07"
);
}
#[test]
fn all_dynamic_sequences_start_with_esc() {
let sequences: Vec<Vec<u8>> = vec![
to_bytes(sgr_reset),
to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)),
to_bytes(|w| sgr_fg_rgb(w, 1, 2, 3)),
to_bytes(|w| sgr_bg_rgb(w, 1, 2, 3)),
to_bytes(|w| sgr_fg_256(w, 42)),
to_bytes(|w| sgr_bg_256(w, 42)),
to_bytes(|w| sgr_fg_16(w, 5)),
to_bytes(|w| sgr_bg_16(w, 5)),
to_bytes(sgr_fg_default),
to_bytes(sgr_bg_default),
to_bytes(|w| cup(w, 0, 0)),
to_bytes(|w| cha(w, 0)),
to_bytes(|w| cuu(w, 1)),
to_bytes(|w| cud(w, 1)),
to_bytes(|w| cuf(w, 1)),
to_bytes(|w| cub(w, 1)),
to_bytes(cursor_save),
to_bytes(cursor_restore),
to_bytes(cursor_hide),
to_bytes(cursor_show),
to_bytes(|w| erase_line(w, EraseLineMode::All)),
to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
to_bytes(|w| set_scroll_region(w, 0, 23)),
to_bytes(reset_scroll_region),
to_bytes(sync_begin),
to_bytes(sync_end),
to_bytes(|w| hyperlink_start(w, "test")),
to_bytes(hyperlink_end),
to_bytes(alt_screen_enter),
to_bytes(alt_screen_leave),
to_bytes(bracketed_paste_enable),
to_bytes(bracketed_paste_disable),
to_bytes(mouse_enable),
to_bytes(mouse_disable),
to_bytes(focus_enable),
to_bytes(focus_disable),
];
for (i, seq) in sequences.iter().enumerate() {
assert!(
seq.starts_with(b"\x1b"),
"sequence {i} should start with ESC, got {seq:?}"
);
}
}
}