1#![forbid(unsafe_code)]
2
3use std::io::{self, Write};
28
29use crate::cell::{PackedRgba, StyleFlags};
30
31const MAX_OSC8_FIELD_BYTES: usize = 4096;
32
33#[inline]
34fn osc8_field_is_safe(value: &str) -> bool {
35 value.len() <= MAX_OSC8_FIELD_BYTES && !value.chars().any(char::is_control)
36}
37
38pub const SGR_RESET: &[u8] = b"\x1b[0m";
44
45#[inline]
47pub fn sgr_reset<W: Write>(w: &mut W) -> io::Result<()> {
48 w.write_all(SGR_RESET)
49}
50
51#[derive(Debug, Clone, Copy)]
53pub struct SgrCodes {
54 pub on: u8,
56 pub off: u8,
58}
59
60pub const SGR_BOLD: SgrCodes = SgrCodes { on: 1, off: 22 };
62pub const SGR_DIM: SgrCodes = SgrCodes { on: 2, off: 22 };
64pub const SGR_ITALIC: SgrCodes = SgrCodes { on: 3, off: 23 };
66pub const SGR_UNDERLINE: SgrCodes = SgrCodes { on: 4, off: 24 };
68pub const SGR_BLINK: SgrCodes = SgrCodes { on: 5, off: 25 };
70pub const SGR_REVERSE: SgrCodes = SgrCodes { on: 7, off: 27 };
72pub const SGR_HIDDEN: SgrCodes = SgrCodes { on: 8, off: 28 };
74pub const SGR_STRIKETHROUGH: SgrCodes = SgrCodes { on: 9, off: 29 };
76
77#[must_use]
79pub const fn sgr_codes_for_flag(flag: StyleFlags) -> Option<SgrCodes> {
80 match flag.bits() {
81 0b0000_0001 => Some(SGR_BOLD),
82 0b0000_0010 => Some(SGR_DIM),
83 0b0000_0100 => Some(SGR_ITALIC),
84 0b0000_1000 => Some(SGR_UNDERLINE),
85 0b0001_0000 => Some(SGR_BLINK),
86 0b0010_0000 => Some(SGR_REVERSE),
87 0b1000_0000 => Some(SGR_HIDDEN),
88 0b0100_0000 => Some(SGR_STRIKETHROUGH),
89 _ => None,
90 }
91}
92
93#[inline]
94fn write_u8_dec(buf: &mut [u8], n: u8) -> usize {
95 if n >= 100 {
96 let hundreds = n / 100;
97 let tens = (n / 10) % 10;
98 let ones = n % 10;
99 buf[0] = b'0' + hundreds;
100 buf[1] = b'0' + tens;
101 buf[2] = b'0' + ones;
102 3
103 } else if n >= 10 {
104 let tens = n / 10;
105 let ones = n % 10;
106 buf[0] = b'0' + tens;
107 buf[1] = b'0' + ones;
108 2
109 } else {
110 buf[0] = b'0' + n;
111 1
112 }
113}
114
115#[inline]
116fn write_u32_dec(buf: &mut [u8], mut n: u32) -> usize {
117 let mut rev = [0u8; 10];
118 let mut len = 0usize;
119
120 loop {
121 rev[len] = (n % 10) as u8;
122 len += 1;
123 n /= 10;
124 if n == 0 {
125 break;
126 }
127 }
128
129 for i in 0..len {
130 buf[i] = b'0' + rev[len - 1 - i];
131 }
132
133 len
134}
135
136#[inline]
137fn write_sgr_code<W: Write>(w: &mut W, code: u8) -> io::Result<()> {
138 let mut buf = [0u8; 6];
139 buf[0] = 0x1b;
140 buf[1] = b'[';
141 let len = write_u8_dec(&mut buf[2..], code);
142 buf[2 + len] = b'm';
143 w.write_all(&buf[..2 + len + 1])
144}
145
146pub fn sgr_flags<W: Write>(w: &mut W, flags: StyleFlags) -> io::Result<()> {
151 if flags.is_empty() {
152 return Ok(());
153 }
154
155 let bits = flags.bits();
156 if bits.is_power_of_two()
157 && let Some(seq) = sgr_single_flag_seq(bits)
158 {
159 return w.write_all(seq);
160 }
161
162 let mut buf = [0u8; 32];
163 let mut idx = 0usize;
164 buf[idx] = 0x1b;
165 buf[idx + 1] = b'[';
166 idx += 2;
167 let mut first = true;
168
169 for (flag, codes) in FLAG_TABLE {
170 if flags.contains(flag) {
171 if !first {
172 buf[idx] = b';';
173 idx += 1;
174 }
175 idx += write_u8_dec(&mut buf[idx..], codes.on);
176 first = false;
177 }
178 }
179
180 buf[idx] = b'm';
181 idx += 1;
182 w.write_all(&buf[..idx])
183}
184
185pub const FLAG_TABLE: [(StyleFlags, SgrCodes); 8] = [
187 (StyleFlags::BOLD, SGR_BOLD),
188 (StyleFlags::DIM, SGR_DIM),
189 (StyleFlags::ITALIC, SGR_ITALIC),
190 (StyleFlags::UNDERLINE, SGR_UNDERLINE),
191 (StyleFlags::BLINK, SGR_BLINK),
192 (StyleFlags::REVERSE, SGR_REVERSE),
193 (StyleFlags::HIDDEN, SGR_HIDDEN),
194 (StyleFlags::STRIKETHROUGH, SGR_STRIKETHROUGH),
195];
196
197#[inline]
198fn sgr_single_flag_seq(bits: u8) -> Option<&'static [u8]> {
199 match bits {
200 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,
209 }
210}
211
212#[inline]
213fn sgr_single_flag_off_seq(bits: u8) -> Option<&'static [u8]> {
214 match bits {
215 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,
224 }
225}
226
227pub fn sgr_flags_off<W: Write>(
245 w: &mut W,
246 flags_to_disable: StyleFlags,
247 flags_to_keep: StyleFlags,
248) -> io::Result<StyleFlags> {
249 if flags_to_disable.is_empty() {
250 return Ok(StyleFlags::empty());
251 }
252
253 let disable_bits = flags_to_disable.bits();
254 if disable_bits.is_power_of_two()
255 && let Some(seq) = sgr_single_flag_off_seq(disable_bits)
256 {
257 w.write_all(seq)?;
258 if disable_bits == StyleFlags::BOLD.bits() && flags_to_keep.contains(StyleFlags::DIM) {
259 return Ok(StyleFlags::DIM);
260 }
261 if disable_bits == StyleFlags::DIM.bits() && flags_to_keep.contains(StyleFlags::BOLD) {
262 return Ok(StyleFlags::BOLD);
263 }
264 return Ok(StyleFlags::empty());
265 }
266
267 let mut collateral = StyleFlags::empty();
268
269 for (flag, codes) in FLAG_TABLE {
270 if !flags_to_disable.contains(flag) {
271 continue;
272 }
273 write_sgr_code(w, codes.off)?;
275 if codes.off == 22 {
277 let other = if flag == StyleFlags::BOLD {
279 StyleFlags::DIM
280 } else {
281 StyleFlags::BOLD
282 };
283 if flags_to_keep.contains(other) && !flags_to_disable.contains(other) {
284 collateral |= other;
285 }
286 }
287 }
288
289 Ok(collateral)
290}
291
292const SGR_FG_RGB_PREFIX: &[u8] = b"\x1b[38;2;";
293const SGR_BG_RGB_PREFIX: &[u8] = b"\x1b[48;2;";
294
295#[inline]
296fn write_sgr_rgb_seq<W: Write>(w: &mut W, prefix: &[u8], r: u8, g: u8, b: u8) -> io::Result<()> {
297 let mut buf = [0u8; 20];
298 let mut idx = 0usize;
299
300 buf[..prefix.len()].copy_from_slice(prefix);
301 idx += prefix.len();
302
303 idx += write_u8_dec(&mut buf[idx..], r);
304 buf[idx] = b';';
305 idx += 1;
306 idx += write_u8_dec(&mut buf[idx..], g);
307 buf[idx] = b';';
308 idx += 1;
309 idx += write_u8_dec(&mut buf[idx..], b);
310 buf[idx] = b'm';
311 idx += 1;
312
313 w.write_all(&buf[..idx])
314}
315
316#[inline]
317fn write_csi_u32_suffix<W: Write>(w: &mut W, value: u32, suffix: u8) -> io::Result<()> {
318 let mut buf = [0u8; 16];
319 buf[0] = 0x1b;
320 buf[1] = b'[';
321 let len = write_u32_dec(&mut buf[2..], value);
322 buf[2 + len] = suffix;
323 w.write_all(&buf[..3 + len])
324}
325
326const ANSI16_RGB: [(u8, u8, u8); 16] = [
327 (0, 0, 0),
328 (205, 0, 0),
329 (0, 205, 0),
330 (205, 205, 0),
331 (0, 0, 238),
332 (205, 0, 205),
333 (0, 205, 205),
334 (229, 229, 229),
335 (127, 127, 127),
336 (255, 0, 0),
337 (0, 255, 0),
338 (255, 255, 0),
339 (92, 92, 255),
340 (255, 0, 255),
341 (0, 255, 255),
342 (255, 255, 255),
343];
344
345#[inline]
346const fn ansi_cube_index(value: u8) -> u8 {
347 if value < 48 {
348 0
349 } else if value < 115 {
350 1
351 } else {
352 (value - 35) / 40
353 }
354}
355
356#[inline]
357const fn ansi256_rgb(index: u8) -> (u8, u8, u8) {
358 if index < 16 {
359 return ANSI16_RGB[index as usize];
360 }
361 if index >= 232 {
362 let gray = 8 + 10 * (index - 232);
363 return (gray, gray, gray);
364 }
365
366 let index = index - 16;
367 let r = index / 36;
368 let g = (index / 6) % 6;
369 let b = index % 6;
370 const LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
371 (LEVELS[r as usize], LEVELS[g as usize], LEVELS[b as usize])
372}
373
374#[inline]
375fn color_distance(a: (u8, u8, u8), b: (u8, u8, u8)) -> u64 {
376 let dr = i32::from(a.0) - i32::from(b.0);
377 let dg = i32::from(a.1) - i32::from(b.1);
378 let db = i32::from(a.2) - i32::from(b.2);
379 2126 * (dr * dr) as u64 + 7152 * (dg * dg) as u64 + 722 * (db * db) as u64
380}
381
382#[must_use]
388pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
389 let cube = 16 + 36 * ansi_cube_index(r) + 6 * ansi_cube_index(g) + ansi_cube_index(b);
390
391 if r != g || g != b {
392 return cube;
393 }
394 if r < 8 {
395 return if r <= 4 { 16 } else { 232 };
396 }
397 if r > 246 {
398 return 231;
399 }
400
401 let gray = 232 + ((r - 8 + 5) / 10).min(23);
402 let target = (r, g, b);
403 if color_distance(target, ansi256_rgb(cube)) <= color_distance(target, ansi256_rgb(gray)) {
404 cube
405 } else {
406 gray
407 }
408}
409
410#[must_use]
412pub fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> u8 {
413 let target = (r, g, b);
414 let mut best_index = 0;
415 let mut best_distance = u64::MAX;
416 for (index, candidate) in ANSI16_RGB.iter().copied().enumerate() {
417 let distance = color_distance(target, candidate);
418 if distance < best_distance {
419 best_index = index as u8;
420 best_distance = distance;
421 }
422 }
423 best_index
424}
425
426pub fn sgr_fg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
428 write_sgr_rgb_seq(w, SGR_FG_RGB_PREFIX, r, g, b)
429}
430
431pub fn sgr_bg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
433 write_sgr_rgb_seq(w, SGR_BG_RGB_PREFIX, r, g, b)
434}
435
436pub fn sgr_fg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
438 write!(w, "\x1b[38;5;{index}m")
439}
440
441pub fn sgr_bg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
443 write!(w, "\x1b[48;5;{index}m")
444}
445
446pub fn sgr_fg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
450 let code = if index < 8 {
451 30 + index
452 } else {
453 90 + index - 8
454 };
455 write!(w, "\x1b[{code}m")
456}
457
458pub fn sgr_bg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
462 let code = if index < 8 {
463 40 + index
464 } else {
465 100 + index - 8
466 };
467 write!(w, "\x1b[{code}m")
468}
469
470pub fn sgr_fg_default<W: Write>(w: &mut W) -> io::Result<()> {
472 w.write_all(b"\x1b[39m")
473}
474
475pub fn sgr_bg_default<W: Write>(w: &mut W) -> io::Result<()> {
477 w.write_all(b"\x1b[49m")
478}
479
480pub fn sgr_fg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
484 if color.a() == 0 {
485 return sgr_fg_default(w);
486 }
487 sgr_fg_rgb(w, color.r(), color.g(), color.b())
488}
489
490pub fn sgr_bg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
494 if color.a() == 0 {
495 return sgr_bg_default(w);
496 }
497 sgr_bg_rgb(w, color.r(), color.g(), color.b())
498}
499
500pub fn cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
509 let mut buf = [0u8; 16];
510 let mut idx = 0usize;
511
512 buf[idx] = 0x1b;
513 buf[idx + 1] = b'[';
514 idx += 2;
515 idx += write_u32_dec(&mut buf[idx..], (row as u32) + 1);
516 buf[idx] = b';';
517 idx += 1;
518 idx += write_u32_dec(&mut buf[idx..], (col as u32) + 1);
519 buf[idx] = b'H';
520 idx += 1;
521
522 w.write_all(&buf[..idx])
523}
524
525pub fn cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
529 write_csi_u32_suffix(w, (col as u32) + 1, b'G')
530}
531
532pub fn cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
534 if n == 0 {
535 return Ok(());
536 }
537 if n == 1 {
538 w.write_all(b"\x1b[A")
539 } else {
540 write_csi_u32_suffix(w, n as u32, b'A')
541 }
542}
543
544pub fn cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
546 if n == 0 {
547 return Ok(());
548 }
549 if n == 1 {
550 w.write_all(b"\x1b[B")
551 } else {
552 write_csi_u32_suffix(w, n as u32, b'B')
553 }
554}
555
556pub fn cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
558 if n == 0 {
559 return Ok(());
560 }
561 if n == 1 {
562 w.write_all(b"\x1b[C")
563 } else {
564 write_csi_u32_suffix(w, n as u32, b'C')
565 }
566}
567
568pub fn cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
570 if n == 0 {
571 return Ok(());
572 }
573 if n == 1 {
574 w.write_all(b"\x1b[D")
575 } else {
576 write_csi_u32_suffix(w, n as u32, b'D')
577 }
578}
579
580#[inline]
582pub fn cr<W: Write>(w: &mut W) -> io::Result<()> {
583 w.write_all(b"\r")
584}
585
586#[inline]
590pub fn lf<W: Write>(w: &mut W) -> io::Result<()> {
591 w.write_all(b"\n")
592}
593
594pub const CURSOR_SAVE: &[u8] = b"\x1b7";
596
597pub const CURSOR_RESTORE: &[u8] = b"\x1b8";
599
600#[inline]
602pub fn cursor_save<W: Write>(w: &mut W) -> io::Result<()> {
603 w.write_all(CURSOR_SAVE)
604}
605
606#[inline]
608pub fn cursor_restore<W: Write>(w: &mut W) -> io::Result<()> {
609 w.write_all(CURSOR_RESTORE)
610}
611
612pub const CURSOR_HIDE: &[u8] = b"\x1b[?25l";
614
615pub const CURSOR_SHOW: &[u8] = b"\x1b[?25h";
617
618#[inline]
620pub fn cursor_hide<W: Write>(w: &mut W) -> io::Result<()> {
621 w.write_all(CURSOR_HIDE)
622}
623
624#[inline]
626pub fn cursor_show<W: Write>(w: &mut W) -> io::Result<()> {
627 w.write_all(CURSOR_SHOW)
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
636pub enum EraseLineMode {
637 ToEnd = 0,
639 ToStart = 1,
641 All = 2,
643}
644
645pub fn erase_line<W: Write>(w: &mut W, mode: EraseLineMode) -> io::Result<()> {
647 match mode {
648 EraseLineMode::ToEnd => w.write_all(b"\x1b[K"),
649 EraseLineMode::ToStart => w.write_all(b"\x1b[1K"),
650 EraseLineMode::All => w.write_all(b"\x1b[2K"),
651 }
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
656pub enum EraseDisplayMode {
657 ToEnd = 0,
659 ToStart = 1,
661 All = 2,
663 Scrollback = 3,
665}
666
667pub fn erase_display<W: Write>(w: &mut W, mode: EraseDisplayMode) -> io::Result<()> {
669 match mode {
670 EraseDisplayMode::ToEnd => w.write_all(b"\x1b[J"),
671 EraseDisplayMode::ToStart => w.write_all(b"\x1b[1J"),
672 EraseDisplayMode::All => w.write_all(b"\x1b[2J"),
673 EraseDisplayMode::Scrollback => w.write_all(b"\x1b[3J"),
674 }
675}
676
677pub fn set_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
685 write!(w, "\x1b[{};{}r", (top as u32) + 1, (bottom as u32) + 1)
686}
687
688pub const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
690
691#[inline]
693pub fn reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
694 w.write_all(RESET_SCROLL_REGION)
695}
696
697pub const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
703
704pub const SYNC_END: &[u8] = b"\x1b[?2026l";
706
707#[inline]
709pub fn sync_begin<W: Write>(w: &mut W) -> io::Result<()> {
710 w.write_all(SYNC_BEGIN)
711}
712
713#[inline]
715pub fn sync_end<W: Write>(w: &mut W) -> io::Result<()> {
716 w.write_all(SYNC_END)
717}
718
719pub fn hyperlink_start<W: Write>(w: &mut W, url: &str) -> io::Result<()> {
729 if !osc8_field_is_safe(url) {
730 return Ok(());
731 }
732 write!(w, "\x1b]8;;{url}\x07")
733}
734
735pub fn hyperlink_end<W: Write>(w: &mut W) -> io::Result<()> {
739 w.write_all(b"\x1b]8;;\x07")
740}
741
742pub fn hyperlink_start_with_id<W: Write>(w: &mut W, id: &str, url: &str) -> io::Result<()> {
747 if !osc8_field_is_safe(url)
753 || !osc8_field_is_safe(id)
754 || id.contains(';')
755 || id.contains(':')
756 || id.contains('=')
757 {
758 return Ok(());
759 }
760 write!(w, "\x1b]8;id={id};{url}\x07")
761}
762
763pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
769
770pub const ALT_SCREEN_LEAVE: &[u8] = b"\x1b[?1049l";
772
773pub const BRACKETED_PASTE_ENABLE: &[u8] = b"\x1b[?2004h";
775
776pub const BRACKETED_PASTE_DISABLE: &[u8] = b"\x1b[?2004l";
778
779pub 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";
792
793pub 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";
795
796pub const FOCUS_ENABLE: &[u8] = b"\x1b[?1004h";
798
799pub const FOCUS_DISABLE: &[u8] = b"\x1b[?1004l";
801
802#[inline]
804pub fn alt_screen_enter<W: Write>(w: &mut W) -> io::Result<()> {
805 w.write_all(ALT_SCREEN_ENTER)
806}
807
808#[inline]
810pub fn alt_screen_leave<W: Write>(w: &mut W) -> io::Result<()> {
811 w.write_all(ALT_SCREEN_LEAVE)
812}
813
814#[inline]
816pub fn bracketed_paste_enable<W: Write>(w: &mut W) -> io::Result<()> {
817 w.write_all(BRACKETED_PASTE_ENABLE)
818}
819
820#[inline]
822pub fn bracketed_paste_disable<W: Write>(w: &mut W) -> io::Result<()> {
823 w.write_all(BRACKETED_PASTE_DISABLE)
824}
825
826#[inline]
828pub fn mouse_enable<W: Write>(w: &mut W) -> io::Result<()> {
829 w.write_all(MOUSE_ENABLE)
830}
831
832#[inline]
834pub fn mouse_disable<W: Write>(w: &mut W) -> io::Result<()> {
835 w.write_all(MOUSE_DISABLE)
836}
837
838#[inline]
840pub fn focus_enable<W: Write>(w: &mut W) -> io::Result<()> {
841 w.write_all(FOCUS_ENABLE)
842}
843
844#[inline]
846pub fn focus_disable<W: Write>(w: &mut W) -> io::Result<()> {
847 w.write_all(FOCUS_DISABLE)
848}
849
850#[cfg(test)]
855mod tests {
856 use super::*;
857
858 fn to_bytes<F: FnOnce(&mut Vec<u8>) -> io::Result<()>>(f: F) -> Vec<u8> {
859 let mut buf = Vec::new();
860 f(&mut buf).unwrap();
861 buf
862 }
863
864 #[test]
867 fn sgr_reset_bytes() {
868 assert_eq!(to_bytes(sgr_reset), b"\x1b[0m");
869 }
870
871 #[test]
872 fn sgr_flags_bold() {
873 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)), b"\x1b[1m");
874 }
875
876 #[test]
877 fn sgr_flags_multiple() {
878 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
879 assert_eq!(to_bytes(|w| sgr_flags(w, flags)), b"\x1b[1;3;4m");
880 }
881
882 #[test]
883 fn sgr_flags_empty() {
884 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::empty())), b"");
885 }
886
887 #[test]
888 fn sgr_fg_rgb_bytes() {
889 assert_eq!(
890 to_bytes(|w| sgr_fg_rgb(w, 255, 128, 0)),
891 b"\x1b[38;2;255;128;0m"
892 );
893 }
894
895 #[test]
896 fn sgr_bg_rgb_bytes() {
897 assert_eq!(to_bytes(|w| sgr_bg_rgb(w, 0, 0, 0)), b"\x1b[48;2;0;0;0m");
898 }
899
900 #[test]
901 fn dynamic_sgr_rgb_matches_reference_formatting() {
902 for (r, g, b) in [(0, 0, 0), (1, 2, 3), (9, 10, 99), (100, 200, 255)] {
903 assert_eq!(
904 to_bytes(|w| sgr_fg_rgb(w, r, g, b)),
905 format!("\x1b[38;2;{r};{g};{b}m").into_bytes()
906 );
907 assert_eq!(
908 to_bytes(|w| sgr_bg_rgb(w, r, g, b)),
909 format!("\x1b[48;2;{r};{g};{b}m").into_bytes()
910 );
911 }
912 }
913
914 #[test]
915 fn sgr_fg_256_bytes() {
916 assert_eq!(to_bytes(|w| sgr_fg_256(w, 196)), b"\x1b[38;5;196m");
917 }
918
919 #[test]
920 fn sgr_bg_256_bytes() {
921 assert_eq!(to_bytes(|w| sgr_bg_256(w, 232)), b"\x1b[48;5;232m");
922 }
923
924 #[test]
925 fn rgb_palette_downgrade_is_deterministic() {
926 assert_eq!(rgb_to_ansi256(255, 0, 0), 196);
927 assert_eq!(rgb_to_ansi256(0, 0, 255), 21);
928 assert_eq!(rgb_to_ansi256(128, 128, 128), 244);
929 assert_eq!(rgb_to_ansi256(17, 17, 17), 233);
930 assert_eq!(rgb_to_ansi16(255, 0, 0), 9);
931 assert_eq!(rgb_to_ansi16(0, 0, 255), 4);
932 }
933
934 #[test]
935 fn grayscale_downgrade_is_nearest_extended_palette_entry() {
936 for value in 0..=u8::MAX {
937 let target = (value, value, value);
938 let selected = rgb_to_ansi256(value, value, value);
939 let selected_distance = color_distance(target, ansi256_rgb(selected));
940 let nearest_distance = (16..=u8::MAX)
941 .map(|index| color_distance(target, ansi256_rgb(index)))
942 .min()
943 .unwrap();
944
945 assert_eq!(
946 selected_distance, nearest_distance,
947 "gray {value} mapped to index {selected}"
948 );
949 }
950 }
951
952 #[test]
953 fn sgr_fg_16_normal() {
954 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"); }
957
958 #[test]
959 fn sgr_fg_16_bright() {
960 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"); }
963
964 #[test]
965 fn sgr_bg_16_normal() {
966 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"); }
969
970 #[test]
971 fn sgr_bg_16_bright() {
972 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"); }
975
976 #[test]
977 fn sgr_default_colors() {
978 assert_eq!(to_bytes(sgr_fg_default), b"\x1b[39m");
979 assert_eq!(to_bytes(sgr_bg_default), b"\x1b[49m");
980 }
981
982 #[test]
983 fn sgr_packed_transparent_uses_default() {
984 assert_eq!(
985 to_bytes(|w| sgr_fg_packed(w, PackedRgba::TRANSPARENT)),
986 b"\x1b[39m"
987 );
988 assert_eq!(
989 to_bytes(|w| sgr_bg_packed(w, PackedRgba::TRANSPARENT)),
990 b"\x1b[49m"
991 );
992 }
993
994 #[test]
995 fn sgr_packed_opaque() {
996 let color = PackedRgba::rgb(10, 20, 30);
997 assert_eq!(
998 to_bytes(|w| sgr_fg_packed(w, color)),
999 b"\x1b[38;2;10;20;30m"
1000 );
1001 }
1002
1003 #[test]
1006 fn cup_1_indexed() {
1007 assert_eq!(to_bytes(|w| cup(w, 0, 0)), b"\x1b[1;1H");
1008 assert_eq!(to_bytes(|w| cup(w, 23, 79)), b"\x1b[24;80H");
1009 }
1010
1011 #[test]
1012 fn cha_1_indexed() {
1013 assert_eq!(to_bytes(|w| cha(w, 0)), b"\x1b[1G");
1014 assert_eq!(to_bytes(|w| cha(w, 79)), b"\x1b[80G");
1015 }
1016
1017 #[test]
1018 fn cursor_relative_moves() {
1019 assert_eq!(to_bytes(|w| cuu(w, 1)), b"\x1b[A");
1020 assert_eq!(to_bytes(|w| cuu(w, 5)), b"\x1b[5A");
1021 assert_eq!(to_bytes(|w| cud(w, 1)), b"\x1b[B");
1022 assert_eq!(to_bytes(|w| cud(w, 3)), b"\x1b[3B");
1023 assert_eq!(to_bytes(|w| cuf(w, 1)), b"\x1b[C");
1024 assert_eq!(to_bytes(|w| cuf(w, 10)), b"\x1b[10C");
1025 assert_eq!(to_bytes(|w| cub(w, 1)), b"\x1b[D");
1026 assert_eq!(to_bytes(|w| cub(w, 2)), b"\x1b[2D");
1027 }
1028
1029 #[test]
1030 fn cursor_relative_zero_is_noop() {
1031 assert_eq!(to_bytes(|w| cuu(w, 0)), b"");
1032 assert_eq!(to_bytes(|w| cud(w, 0)), b"");
1033 assert_eq!(to_bytes(|w| cuf(w, 0)), b"");
1034 assert_eq!(to_bytes(|w| cub(w, 0)), b"");
1035 }
1036
1037 #[test]
1038 fn dynamic_cursor_sequences_match_reference_formatting() {
1039 for (row, col) in [(0, 0), (23, 79), (999, 999), (u16::MAX, u16::MAX)] {
1040 assert_eq!(
1041 to_bytes(|w| cup(w, row, col)),
1042 format!("\x1b[{};{}H", (row as u32) + 1, (col as u32) + 1).into_bytes()
1043 );
1044 }
1045
1046 for col in [0, 79, 999, u16::MAX] {
1047 assert_eq!(
1048 to_bytes(|w| cha(w, col)),
1049 format!("\x1b[{}G", (col as u32) + 1).into_bytes()
1050 );
1051 }
1052
1053 for n in [0, 1, 2, 10, 999, u16::MAX] {
1054 let expected_up = if n == 0 {
1055 Vec::new()
1056 } else if n == 1 {
1057 b"\x1b[A".to_vec()
1058 } else {
1059 format!("\x1b[{n}A").into_bytes()
1060 };
1061 let expected_down = if n == 0 {
1062 Vec::new()
1063 } else if n == 1 {
1064 b"\x1b[B".to_vec()
1065 } else {
1066 format!("\x1b[{n}B").into_bytes()
1067 };
1068 let expected_forward = if n == 0 {
1069 Vec::new()
1070 } else if n == 1 {
1071 b"\x1b[C".to_vec()
1072 } else {
1073 format!("\x1b[{n}C").into_bytes()
1074 };
1075 let expected_back = if n == 0 {
1076 Vec::new()
1077 } else if n == 1 {
1078 b"\x1b[D".to_vec()
1079 } else {
1080 format!("\x1b[{n}D").into_bytes()
1081 };
1082
1083 assert_eq!(to_bytes(|w| cuu(w, n)), expected_up);
1084 assert_eq!(to_bytes(|w| cud(w, n)), expected_down);
1085 assert_eq!(to_bytes(|w| cuf(w, n)), expected_forward);
1086 assert_eq!(to_bytes(|w| cub(w, n)), expected_back);
1087 }
1088 }
1089
1090 #[test]
1091 fn cursor_save_restore() {
1092 assert_eq!(to_bytes(cursor_save), b"\x1b7");
1093 assert_eq!(to_bytes(cursor_restore), b"\x1b8");
1094 }
1095
1096 #[test]
1097 fn cursor_visibility() {
1098 assert_eq!(to_bytes(cursor_hide), b"\x1b[?25l");
1099 assert_eq!(to_bytes(cursor_show), b"\x1b[?25h");
1100 }
1101
1102 #[test]
1105 fn erase_line_modes() {
1106 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::ToEnd)), b"\x1b[K");
1107 assert_eq!(
1108 to_bytes(|w| erase_line(w, EraseLineMode::ToStart)),
1109 b"\x1b[1K"
1110 );
1111 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::All)), b"\x1b[2K");
1112 }
1113
1114 #[test]
1115 fn erase_display_modes() {
1116 assert_eq!(
1117 to_bytes(|w| erase_display(w, EraseDisplayMode::ToEnd)),
1118 b"\x1b[J"
1119 );
1120 assert_eq!(
1121 to_bytes(|w| erase_display(w, EraseDisplayMode::ToStart)),
1122 b"\x1b[1J"
1123 );
1124 assert_eq!(
1125 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
1126 b"\x1b[2J"
1127 );
1128 assert_eq!(
1129 to_bytes(|w| erase_display(w, EraseDisplayMode::Scrollback)),
1130 b"\x1b[3J"
1131 );
1132 }
1133
1134 #[test]
1137 fn scroll_region_1_indexed() {
1138 assert_eq!(to_bytes(|w| set_scroll_region(w, 0, 23)), b"\x1b[1;24r");
1139 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 20)), b"\x1b[6;21r");
1140 }
1141
1142 #[test]
1143 fn scroll_region_reset() {
1144 assert_eq!(to_bytes(reset_scroll_region), b"\x1b[r");
1145 }
1146
1147 #[test]
1150 fn sync_output() {
1151 assert_eq!(to_bytes(sync_begin), b"\x1b[?2026h");
1152 assert_eq!(to_bytes(sync_end), b"\x1b[?2026l");
1153 }
1154
1155 #[test]
1158 fn hyperlink_basic() {
1159 assert_eq!(
1160 to_bytes(|w| hyperlink_start(w, "https://example.com")),
1161 b"\x1b]8;;https://example.com\x07"
1162 );
1163 assert_eq!(to_bytes(hyperlink_end), b"\x1b]8;;\x07");
1164 }
1165
1166 #[test]
1167 fn hyperlink_with_id() {
1168 assert_eq!(
1169 to_bytes(|w| hyperlink_start_with_id(w, "link1", "https://example.com")),
1170 b"\x1b]8;id=link1;https://example.com\x07"
1171 );
1172 }
1173
1174 #[test]
1175 fn hyperlink_rejects_control_chars() {
1176 assert_eq!(
1177 to_bytes(|w| hyperlink_start(w, "https://exa\x1bmple.com")),
1178 b""
1179 );
1180 assert_eq!(
1181 to_bytes(|w| hyperlink_start_with_id(w, "id", "https://exa\u{009d}mple.com")),
1182 b""
1183 );
1184 }
1185
1186 #[test]
1187 fn hyperlink_with_id_rejects_parameter_breakout() {
1188 assert_eq!(
1189 to_bytes(|w| hyperlink_start_with_id(w, "id;malicious=1", "https://example.com")),
1190 b""
1191 );
1192 }
1193
1194 #[test]
1195 fn hyperlink_rejects_overlong_fields() {
1196 let long_url = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
1197 assert_eq!(to_bytes(|w| hyperlink_start(w, &long_url)), b"");
1198
1199 let long_id = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
1200 assert_eq!(
1201 to_bytes(|w| hyperlink_start_with_id(w, &long_id, "https://example.com")),
1202 b""
1203 );
1204 }
1205
1206 #[test]
1209 fn alt_screen() {
1210 assert_eq!(to_bytes(alt_screen_enter), b"\x1b[?1049h");
1211 assert_eq!(to_bytes(alt_screen_leave), b"\x1b[?1049l");
1212 }
1213
1214 #[test]
1215 fn bracketed_paste() {
1216 assert_eq!(to_bytes(bracketed_paste_enable), b"\x1b[?2004h");
1217 assert_eq!(to_bytes(bracketed_paste_disable), b"\x1b[?2004l");
1218 }
1219
1220 #[test]
1221 fn mouse_mode() {
1222 assert_eq!(
1223 to_bytes(mouse_enable),
1224 b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1006;1000;1002h\x1b[?1006h\x1b[?1000h\x1b[?1002h"
1225 );
1226 assert_eq!(
1227 to_bytes(mouse_disable),
1228 b"\x1b[?1000;1002;1006l\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l"
1229 );
1230
1231 let enabled = to_bytes(mouse_enable);
1232 assert!(
1233 !enabled.ends_with(b"\x1b[?1016l"),
1234 "mouse enable should not end with 1016l (can force X10 fallback)"
1235 );
1236 let pos_1016l = enabled
1237 .windows(b"\x1b[?1016l".len())
1238 .position(|w| w == b"\x1b[?1016l")
1239 .expect("mouse enable should clear 1016 before enabling SGR");
1240 let pos_1006h = enabled
1241 .windows(b"\x1b[?1006h".len())
1242 .position(|w| w == b"\x1b[?1006h")
1243 .expect("mouse enable should include 1006h");
1244 assert!(
1245 pos_1016l < pos_1006h,
1246 "1016l must be emitted before 1006h to preserve SGR mode"
1247 );
1248 }
1249
1250 #[test]
1251 fn focus_mode() {
1252 assert_eq!(to_bytes(focus_enable), b"\x1b[?1004h");
1253 assert_eq!(to_bytes(focus_disable), b"\x1b[?1004l");
1254 }
1255
1256 #[test]
1259 fn all_sequences_are_ascii() {
1260 for seq in [
1262 SGR_RESET,
1263 CURSOR_SAVE,
1264 CURSOR_RESTORE,
1265 CURSOR_HIDE,
1266 CURSOR_SHOW,
1267 RESET_SCROLL_REGION,
1268 SYNC_BEGIN,
1269 SYNC_END,
1270 ALT_SCREEN_ENTER,
1271 ALT_SCREEN_LEAVE,
1272 BRACKETED_PASTE_ENABLE,
1273 BRACKETED_PASTE_DISABLE,
1274 MOUSE_ENABLE,
1275 MOUSE_DISABLE,
1276 FOCUS_ENABLE,
1277 FOCUS_DISABLE,
1278 ] {
1279 for &byte in seq {
1280 assert!(byte < 128, "Non-ASCII byte {byte:#x} in sequence");
1281 }
1282 }
1283 }
1284
1285 #[test]
1286 fn osc_sequences_are_terminated() {
1287 let link_start = to_bytes(|w| hyperlink_start(w, "test"));
1289 assert!(
1290 link_start.ends_with(b"\x07"),
1291 "hyperlink_start not terminated with BEL"
1292 );
1293
1294 let link_end = to_bytes(hyperlink_end);
1295 assert!(
1296 link_end.ends_with(b"\x07"),
1297 "hyperlink_end not terminated with BEL"
1298 );
1299
1300 let link_id = to_bytes(|w| hyperlink_start_with_id(w, "id", "url"));
1301 assert!(
1302 link_id.ends_with(b"\x07"),
1303 "hyperlink_start_with_id not terminated with BEL"
1304 );
1305 }
1306
1307 #[test]
1308 fn hyperlink_id_with_param_separators_is_suppressed() {
1309 for bad_id in ["a:hover=1", "x:y", "k=v"] {
1314 let mut buf = Vec::new();
1315 hyperlink_start_with_id(&mut buf, bad_id, "https://example.com").unwrap();
1316 assert!(buf.is_empty(), "id {bad_id:?} must be suppressed");
1317 }
1318 let mut buf = Vec::new();
1320 hyperlink_start_with_id(&mut buf, "group-1", "https://example.com").unwrap();
1321 assert_eq!(buf, b"\x1b]8;id=group-1;https://example.com\x07");
1322 }
1323
1324 #[test]
1327 fn sgr_flags_off_empty_is_noop() {
1328 let bytes = to_bytes(|w| {
1329 sgr_flags_off(w, StyleFlags::empty(), StyleFlags::empty()).unwrap();
1330 Ok(())
1331 });
1332 assert!(bytes.is_empty(), "disabling no flags should emit nothing");
1333 }
1334
1335 #[test]
1336 fn sgr_flags_off_single_bold() {
1337 let mut buf = Vec::new();
1338 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::empty()).unwrap();
1339 assert_eq!(buf, b"\x1b[22m");
1340 assert!(collateral.is_empty(), "no collateral when DIM is not kept");
1341 }
1342
1343 #[test]
1344 fn sgr_flags_off_single_dim() {
1345 let mut buf = Vec::new();
1346 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::empty()).unwrap();
1347 assert_eq!(buf, b"\x1b[22m");
1348 assert!(collateral.is_empty(), "no collateral when BOLD is not kept");
1349 }
1350
1351 #[test]
1352 fn sgr_flags_off_bold_collateral_dim() {
1353 let mut buf = Vec::new();
1355 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::DIM).unwrap();
1356 assert_eq!(buf, b"\x1b[22m");
1357 assert_eq!(collateral, StyleFlags::DIM);
1358 }
1359
1360 #[test]
1361 fn sgr_flags_off_dim_collateral_bold() {
1362 let mut buf = Vec::new();
1364 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::BOLD).unwrap();
1365 assert_eq!(buf, b"\x1b[22m");
1366 assert_eq!(collateral, StyleFlags::BOLD);
1367 }
1368
1369 #[test]
1370 fn sgr_flags_off_italic() {
1371 let mut buf = Vec::new();
1372 let collateral = sgr_flags_off(&mut buf, StyleFlags::ITALIC, StyleFlags::empty()).unwrap();
1373 assert_eq!(buf, b"\x1b[23m");
1374 assert!(collateral.is_empty());
1375 }
1376
1377 #[test]
1378 fn sgr_flags_off_underline() {
1379 let mut buf = Vec::new();
1380 let collateral =
1381 sgr_flags_off(&mut buf, StyleFlags::UNDERLINE, StyleFlags::empty()).unwrap();
1382 assert_eq!(buf, b"\x1b[24m");
1383 assert!(collateral.is_empty());
1384 }
1385
1386 #[test]
1387 fn sgr_flags_off_blink() {
1388 let mut buf = Vec::new();
1389 let collateral = sgr_flags_off(&mut buf, StyleFlags::BLINK, StyleFlags::empty()).unwrap();
1390 assert_eq!(buf, b"\x1b[25m");
1391 assert!(collateral.is_empty());
1392 }
1393
1394 #[test]
1395 fn sgr_flags_off_reverse() {
1396 let mut buf = Vec::new();
1397 let collateral = sgr_flags_off(&mut buf, StyleFlags::REVERSE, StyleFlags::empty()).unwrap();
1398 assert_eq!(buf, b"\x1b[27m");
1399 assert!(collateral.is_empty());
1400 }
1401
1402 #[test]
1403 fn sgr_flags_off_hidden() {
1404 let mut buf = Vec::new();
1405 let collateral = sgr_flags_off(&mut buf, StyleFlags::HIDDEN, StyleFlags::empty()).unwrap();
1406 assert_eq!(buf, b"\x1b[28m");
1407 assert!(collateral.is_empty());
1408 }
1409
1410 #[test]
1411 fn sgr_flags_off_strikethrough() {
1412 let mut buf = Vec::new();
1413 let collateral =
1414 sgr_flags_off(&mut buf, StyleFlags::STRIKETHROUGH, StyleFlags::empty()).unwrap();
1415 assert_eq!(buf, b"\x1b[29m");
1416 assert!(collateral.is_empty());
1417 }
1418
1419 #[test]
1420 fn sgr_flags_off_multi_no_bold_dim_overlap() {
1421 let mut buf = Vec::new();
1423 let collateral = sgr_flags_off(
1424 &mut buf,
1425 StyleFlags::ITALIC | StyleFlags::UNDERLINE,
1426 StyleFlags::empty(),
1427 )
1428 .unwrap();
1429 assert_eq!(buf, b"\x1b[23m\x1b[24m");
1431 assert!(collateral.is_empty());
1432 }
1433
1434 #[test]
1435 fn sgr_flags_off_bold_and_dim_together() {
1436 let mut buf = Vec::new();
1439 let collateral = sgr_flags_off(
1440 &mut buf,
1441 StyleFlags::BOLD | StyleFlags::DIM,
1442 StyleFlags::empty(),
1443 )
1444 .unwrap();
1445 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1446 assert!(
1447 collateral.is_empty(),
1448 "no collateral when both are disabled"
1449 );
1450 }
1451
1452 #[test]
1453 fn sgr_flags_off_overlap_keep_and_disable_does_not_report_collateral() {
1454 let mut buf = Vec::new();
1456 let collateral = sgr_flags_off(
1457 &mut buf,
1458 StyleFlags::BOLD | StyleFlags::DIM,
1459 StyleFlags::DIM,
1460 )
1461 .unwrap();
1462 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1463 assert!(
1464 collateral.is_empty(),
1465 "DIM is explicitly disabled, so it must not be reported as collateral"
1466 );
1467 }
1468
1469 #[test]
1470 fn sgr_flags_off_bold_dim_with_dim_kept() {
1471 let mut buf = Vec::new();
1473 let collateral = sgr_flags_off(
1474 &mut buf,
1475 StyleFlags::BOLD | StyleFlags::ITALIC,
1476 StyleFlags::DIM,
1477 )
1478 .unwrap();
1479 assert_eq!(
1480 collateral,
1481 StyleFlags::DIM,
1482 "DIM should be collateral damage from BOLD off (code 22)"
1483 );
1484 }
1485
1486 #[test]
1489 fn sgr_codes_for_all_single_flags() {
1490 let cases = [
1491 (StyleFlags::BOLD, 1, 22),
1492 (StyleFlags::DIM, 2, 22),
1493 (StyleFlags::ITALIC, 3, 23),
1494 (StyleFlags::UNDERLINE, 4, 24),
1495 (StyleFlags::BLINK, 5, 25),
1496 (StyleFlags::REVERSE, 7, 27),
1497 (StyleFlags::HIDDEN, 8, 28),
1498 (StyleFlags::STRIKETHROUGH, 9, 29),
1499 ];
1500 for (flag, expected_on, expected_off) in cases {
1501 let codes = sgr_codes_for_flag(flag)
1502 .unwrap_or_else(|| panic!("should return codes for {flag:?}"));
1503 assert_eq!(codes.on, expected_on, "on code for {flag:?}");
1504 assert_eq!(codes.off, expected_off, "off code for {flag:?}");
1505 }
1506 }
1507
1508 #[test]
1509 fn sgr_codes_for_composite_flag_returns_none() {
1510 let composite = StyleFlags::BOLD | StyleFlags::ITALIC;
1511 assert!(
1512 sgr_codes_for_flag(composite).is_none(),
1513 "composite flags should return None"
1514 );
1515 }
1516
1517 #[test]
1518 fn sgr_codes_for_empty_flag_returns_none() {
1519 assert!(
1520 sgr_codes_for_flag(StyleFlags::empty()).is_none(),
1521 "empty flags should return None"
1522 );
1523 }
1524
1525 #[test]
1526 fn sgr_codes_for_flag_matches_flag_table_entries() {
1527 for (flag, expected) in FLAG_TABLE {
1528 let actual = sgr_codes_for_flag(flag).expect("single-bit FLAG_TABLE entry");
1529 assert_eq!(actual.on, expected.on, "{flag:?} on code");
1530 assert_eq!(actual.off, expected.off, "{flag:?} off code");
1531 }
1532 }
1533
1534 #[test]
1537 fn cr_emits_carriage_return() {
1538 assert_eq!(to_bytes(cr), b"\r");
1539 }
1540
1541 #[test]
1542 fn lf_emits_line_feed() {
1543 assert_eq!(to_bytes(lf), b"\n");
1544 }
1545
1546 #[test]
1549 fn sgr_flags_each_single_flag_fast_path() {
1550 let cases: &[(StyleFlags, &[u8])] = &[
1551 (StyleFlags::BOLD, b"\x1b[1m"),
1552 (StyleFlags::DIM, b"\x1b[2m"),
1553 (StyleFlags::ITALIC, b"\x1b[3m"),
1554 (StyleFlags::UNDERLINE, b"\x1b[4m"),
1555 (StyleFlags::BLINK, b"\x1b[5m"),
1556 (StyleFlags::REVERSE, b"\x1b[7m"),
1557 (StyleFlags::STRIKETHROUGH, b"\x1b[9m"),
1558 (StyleFlags::HIDDEN, b"\x1b[8m"),
1559 ];
1560 for &(flag, expected) in cases {
1561 assert_eq!(
1562 to_bytes(|w| sgr_flags(w, flag)),
1563 expected,
1564 "single-flag fast path for {flag:?}"
1565 );
1566 }
1567 }
1568
1569 #[test]
1570 fn sgr_flags_all_eight() {
1571 let all = StyleFlags::BOLD
1572 | StyleFlags::DIM
1573 | StyleFlags::ITALIC
1574 | StyleFlags::UNDERLINE
1575 | StyleFlags::BLINK
1576 | StyleFlags::REVERSE
1577 | StyleFlags::HIDDEN
1578 | StyleFlags::STRIKETHROUGH;
1579 let bytes = to_bytes(|w| sgr_flags(w, all));
1580 assert_eq!(bytes, b"\x1b[1;2;3;4;5;7;8;9m");
1582 }
1583
1584 #[test]
1587 fn sgr_code_single_digit() {
1588 let mut buf = Vec::new();
1590 write_sgr_code(&mut buf, 1).unwrap();
1591 assert_eq!(buf, b"\x1b[1m");
1592 }
1593
1594 #[test]
1595 fn sgr_code_two_digits() {
1596 let mut buf = Vec::new();
1598 write_sgr_code(&mut buf, 22).unwrap();
1599 assert_eq!(buf, b"\x1b[22m");
1600 }
1601
1602 #[test]
1603 fn sgr_code_three_digits() {
1604 let mut buf = Vec::new();
1606 write_sgr_code(&mut buf, 100).unwrap();
1607 assert_eq!(buf, b"\x1b[100m");
1608 }
1609
1610 #[test]
1611 fn sgr_code_max_u8() {
1612 let mut buf = Vec::new();
1614 write_sgr_code(&mut buf, 255).unwrap();
1615 assert_eq!(buf, b"\x1b[255m");
1616 }
1617
1618 #[test]
1619 fn sgr_code_zero() {
1620 let mut buf = Vec::new();
1621 write_sgr_code(&mut buf, 0).unwrap();
1622 assert_eq!(buf, b"\x1b[0m");
1623 }
1624
1625 #[test]
1628 fn sgr_fg_16_boundary_7_to_8() {
1629 assert_eq!(to_bytes(|w| sgr_fg_16(w, 7)), b"\x1b[37m");
1631 assert_eq!(to_bytes(|w| sgr_fg_16(w, 8)), b"\x1b[90m");
1632 }
1633
1634 #[test]
1635 fn sgr_bg_16_boundary_7_to_8() {
1636 assert_eq!(to_bytes(|w| sgr_bg_16(w, 7)), b"\x1b[47m");
1637 assert_eq!(to_bytes(|w| sgr_bg_16(w, 8)), b"\x1b[100m");
1638 }
1639
1640 #[test]
1641 fn sgr_fg_16_first_color() {
1642 assert_eq!(to_bytes(|w| sgr_fg_16(w, 0)), b"\x1b[30m"); }
1644
1645 #[test]
1646 fn sgr_bg_16_last_bright() {
1647 assert_eq!(to_bytes(|w| sgr_bg_16(w, 15)), b"\x1b[107m"); }
1649
1650 #[test]
1653 fn sgr_fg_256_zero() {
1654 assert_eq!(to_bytes(|w| sgr_fg_256(w, 0)), b"\x1b[38;5;0m");
1655 }
1656
1657 #[test]
1658 fn sgr_fg_256_max() {
1659 assert_eq!(to_bytes(|w| sgr_fg_256(w, 255)), b"\x1b[38;5;255m");
1660 }
1661
1662 #[test]
1663 fn sgr_bg_256_zero() {
1664 assert_eq!(to_bytes(|w| sgr_bg_256(w, 0)), b"\x1b[48;5;0m");
1665 }
1666
1667 #[test]
1668 fn sgr_bg_256_max() {
1669 assert_eq!(to_bytes(|w| sgr_bg_256(w, 255)), b"\x1b[48;5;255m");
1670 }
1671
1672 #[test]
1675 fn cup_max_u16() {
1676 let bytes = to_bytes(|w| cup(w, u16::MAX, u16::MAX));
1678 let s = String::from_utf8(bytes).unwrap();
1679 assert!(s.starts_with("\x1b["));
1680 assert!(s.ends_with("H"));
1681 }
1682
1683 #[test]
1684 fn cha_max_u16() {
1685 let bytes = to_bytes(|w| cha(w, u16::MAX));
1686 let s = String::from_utf8(bytes).unwrap();
1687 assert!(s.starts_with("\x1b["));
1688 assert!(s.ends_with("G"));
1689 }
1690
1691 #[test]
1692 fn cursor_up_max() {
1693 let bytes = to_bytes(|w| cuu(w, u16::MAX));
1694 let s = String::from_utf8(bytes).unwrap();
1695 assert!(s.contains("65535"));
1696 assert!(s.ends_with("A"));
1697 }
1698
1699 #[test]
1702 fn scroll_region_same_top_bottom() {
1703 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 5)), b"\x1b[6;6r");
1704 }
1705
1706 #[test]
1709 fn sgr_flags_off_each_single_flag_fast_path() {
1710 let cases: &[(StyleFlags, &[u8])] = &[
1711 (StyleFlags::BOLD, b"\x1b[22m"),
1712 (StyleFlags::DIM, b"\x1b[22m"),
1713 (StyleFlags::ITALIC, b"\x1b[23m"),
1714 (StyleFlags::UNDERLINE, b"\x1b[24m"),
1715 (StyleFlags::BLINK, b"\x1b[25m"),
1716 (StyleFlags::REVERSE, b"\x1b[27m"),
1717 (StyleFlags::STRIKETHROUGH, b"\x1b[29m"),
1718 (StyleFlags::HIDDEN, b"\x1b[28m"),
1719 ];
1720 for &(flag, expected) in cases {
1721 let mut buf = Vec::new();
1722 let collateral = sgr_flags_off(&mut buf, flag, StyleFlags::empty()).unwrap();
1723 assert_eq!(buf, expected, "off sequence for {flag:?}");
1724 assert!(collateral.is_empty(), "no collateral for {flag:?}");
1725 }
1726 }
1727
1728 #[test]
1731 fn sgr_bg_packed_opaque() {
1732 let color = PackedRgba::rgb(100, 200, 50);
1733 assert_eq!(
1734 to_bytes(|w| sgr_bg_packed(w, color)),
1735 b"\x1b[48;2;100;200;50m"
1736 );
1737 }
1738
1739 #[test]
1742 fn hyperlink_empty_url() {
1743 assert_eq!(to_bytes(|w| hyperlink_start(w, "")), b"\x1b]8;;\x07");
1744 }
1745
1746 #[test]
1747 fn hyperlink_with_empty_id() {
1748 assert_eq!(
1749 to_bytes(|w| hyperlink_start_with_id(w, "", "https://x.com")),
1750 b"\x1b]8;id=;https://x.com\x07"
1751 );
1752 }
1753
1754 #[test]
1757 fn all_dynamic_sequences_start_with_esc() {
1758 let sequences: Vec<Vec<u8>> = vec![
1759 to_bytes(sgr_reset),
1760 to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)),
1761 to_bytes(|w| sgr_fg_rgb(w, 1, 2, 3)),
1762 to_bytes(|w| sgr_bg_rgb(w, 1, 2, 3)),
1763 to_bytes(|w| sgr_fg_256(w, 42)),
1764 to_bytes(|w| sgr_bg_256(w, 42)),
1765 to_bytes(|w| sgr_fg_16(w, 5)),
1766 to_bytes(|w| sgr_bg_16(w, 5)),
1767 to_bytes(sgr_fg_default),
1768 to_bytes(sgr_bg_default),
1769 to_bytes(|w| cup(w, 0, 0)),
1770 to_bytes(|w| cha(w, 0)),
1771 to_bytes(|w| cuu(w, 1)),
1772 to_bytes(|w| cud(w, 1)),
1773 to_bytes(|w| cuf(w, 1)),
1774 to_bytes(|w| cub(w, 1)),
1775 to_bytes(cursor_save),
1776 to_bytes(cursor_restore),
1777 to_bytes(cursor_hide),
1778 to_bytes(cursor_show),
1779 to_bytes(|w| erase_line(w, EraseLineMode::All)),
1780 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
1781 to_bytes(|w| set_scroll_region(w, 0, 23)),
1782 to_bytes(reset_scroll_region),
1783 to_bytes(sync_begin),
1784 to_bytes(sync_end),
1785 to_bytes(|w| hyperlink_start(w, "test")),
1786 to_bytes(hyperlink_end),
1787 to_bytes(alt_screen_enter),
1788 to_bytes(alt_screen_leave),
1789 to_bytes(bracketed_paste_enable),
1790 to_bytes(bracketed_paste_disable),
1791 to_bytes(mouse_enable),
1792 to_bytes(mouse_disable),
1793 to_bytes(focus_enable),
1794 to_bytes(focus_disable),
1795 ];
1796 for (i, seq) in sequences.iter().enumerate() {
1797 assert!(
1798 seq.starts_with(b"\x1b"),
1799 "sequence {i} should start with ESC, got {seq:?}"
1800 );
1801 }
1802 }
1803}