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>(
238 w: &mut W,
239 flags_to_disable: StyleFlags,
240 flags_to_keep: StyleFlags,
241) -> io::Result<StyleFlags> {
242 if flags_to_disable.is_empty() {
243 return Ok(StyleFlags::empty());
244 }
245
246 let disable_bits = flags_to_disable.bits();
247 if disable_bits.is_power_of_two()
248 && let Some(seq) = sgr_single_flag_off_seq(disable_bits)
249 {
250 w.write_all(seq)?;
251 if disable_bits == StyleFlags::BOLD.bits() && flags_to_keep.contains(StyleFlags::DIM) {
252 return Ok(StyleFlags::DIM);
253 }
254 if disable_bits == StyleFlags::DIM.bits() && flags_to_keep.contains(StyleFlags::BOLD) {
255 return Ok(StyleFlags::BOLD);
256 }
257 return Ok(StyleFlags::empty());
258 }
259
260 let mut collateral = StyleFlags::empty();
261
262 for (flag, codes) in FLAG_TABLE {
263 if !flags_to_disable.contains(flag) {
264 continue;
265 }
266 write_sgr_code(w, codes.off)?;
268 if codes.off == 22 {
270 let other = if flag == StyleFlags::BOLD {
272 StyleFlags::DIM
273 } else {
274 StyleFlags::BOLD
275 };
276 if flags_to_keep.contains(other) && !flags_to_disable.contains(other) {
277 collateral |= other;
278 }
279 }
280 }
281
282 Ok(collateral)
283}
284
285const SGR_FG_RGB_PREFIX: &[u8] = b"\x1b[38;2;";
286const SGR_BG_RGB_PREFIX: &[u8] = b"\x1b[48;2;";
287
288#[inline]
289fn write_sgr_rgb_seq<W: Write>(w: &mut W, prefix: &[u8], r: u8, g: u8, b: u8) -> io::Result<()> {
290 let mut buf = [0u8; 20];
291 let mut idx = 0usize;
292
293 buf[..prefix.len()].copy_from_slice(prefix);
294 idx += prefix.len();
295
296 idx += write_u8_dec(&mut buf[idx..], r);
297 buf[idx] = b';';
298 idx += 1;
299 idx += write_u8_dec(&mut buf[idx..], g);
300 buf[idx] = b';';
301 idx += 1;
302 idx += write_u8_dec(&mut buf[idx..], b);
303 buf[idx] = b'm';
304 idx += 1;
305
306 w.write_all(&buf[..idx])
307}
308
309#[inline]
310fn write_csi_u32_suffix<W: Write>(w: &mut W, value: u32, suffix: u8) -> io::Result<()> {
311 let mut buf = [0u8; 16];
312 buf[0] = 0x1b;
313 buf[1] = b'[';
314 let len = write_u32_dec(&mut buf[2..], value);
315 buf[2 + len] = suffix;
316 w.write_all(&buf[..3 + len])
317}
318
319pub fn sgr_fg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
321 write_sgr_rgb_seq(w, SGR_FG_RGB_PREFIX, r, g, b)
322}
323
324pub fn sgr_bg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
326 write_sgr_rgb_seq(w, SGR_BG_RGB_PREFIX, r, g, b)
327}
328
329pub fn sgr_fg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
331 write!(w, "\x1b[38;5;{index}m")
332}
333
334pub fn sgr_bg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
336 write!(w, "\x1b[48;5;{index}m")
337}
338
339pub fn sgr_fg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
343 let code = if index < 8 {
344 30 + index
345 } else {
346 90 + index - 8
347 };
348 write!(w, "\x1b[{code}m")
349}
350
351pub fn sgr_bg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
355 let code = if index < 8 {
356 40 + index
357 } else {
358 100 + index - 8
359 };
360 write!(w, "\x1b[{code}m")
361}
362
363pub fn sgr_fg_default<W: Write>(w: &mut W) -> io::Result<()> {
365 w.write_all(b"\x1b[39m")
366}
367
368pub fn sgr_bg_default<W: Write>(w: &mut W) -> io::Result<()> {
370 w.write_all(b"\x1b[49m")
371}
372
373pub fn sgr_fg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
377 if color.a() == 0 {
378 return sgr_fg_default(w);
379 }
380 sgr_fg_rgb(w, color.r(), color.g(), color.b())
381}
382
383pub fn sgr_bg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
387 if color.a() == 0 {
388 return sgr_bg_default(w);
389 }
390 sgr_bg_rgb(w, color.r(), color.g(), color.b())
391}
392
393pub fn cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
402 let mut buf = [0u8; 16];
403 let mut idx = 0usize;
404
405 buf[idx] = 0x1b;
406 buf[idx + 1] = b'[';
407 idx += 2;
408 idx += write_u32_dec(&mut buf[idx..], (row as u32) + 1);
409 buf[idx] = b';';
410 idx += 1;
411 idx += write_u32_dec(&mut buf[idx..], (col as u32) + 1);
412 buf[idx] = b'H';
413 idx += 1;
414
415 w.write_all(&buf[..idx])
416}
417
418pub fn cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
422 write_csi_u32_suffix(w, (col as u32) + 1, b'G')
423}
424
425pub fn cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
427 if n == 0 {
428 return Ok(());
429 }
430 if n == 1 {
431 w.write_all(b"\x1b[A")
432 } else {
433 write_csi_u32_suffix(w, n as u32, b'A')
434 }
435}
436
437pub fn cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
439 if n == 0 {
440 return Ok(());
441 }
442 if n == 1 {
443 w.write_all(b"\x1b[B")
444 } else {
445 write_csi_u32_suffix(w, n as u32, b'B')
446 }
447}
448
449pub fn cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
451 if n == 0 {
452 return Ok(());
453 }
454 if n == 1 {
455 w.write_all(b"\x1b[C")
456 } else {
457 write_csi_u32_suffix(w, n as u32, b'C')
458 }
459}
460
461pub fn cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
463 if n == 0 {
464 return Ok(());
465 }
466 if n == 1 {
467 w.write_all(b"\x1b[D")
468 } else {
469 write_csi_u32_suffix(w, n as u32, b'D')
470 }
471}
472
473#[inline]
475pub fn cr<W: Write>(w: &mut W) -> io::Result<()> {
476 w.write_all(b"\r")
477}
478
479#[inline]
483pub fn lf<W: Write>(w: &mut W) -> io::Result<()> {
484 w.write_all(b"\n")
485}
486
487pub const CURSOR_SAVE: &[u8] = b"\x1b7";
489
490pub const CURSOR_RESTORE: &[u8] = b"\x1b8";
492
493#[inline]
495pub fn cursor_save<W: Write>(w: &mut W) -> io::Result<()> {
496 w.write_all(CURSOR_SAVE)
497}
498
499#[inline]
501pub fn cursor_restore<W: Write>(w: &mut W) -> io::Result<()> {
502 w.write_all(CURSOR_RESTORE)
503}
504
505pub const CURSOR_HIDE: &[u8] = b"\x1b[?25l";
507
508pub const CURSOR_SHOW: &[u8] = b"\x1b[?25h";
510
511#[inline]
513pub fn cursor_hide<W: Write>(w: &mut W) -> io::Result<()> {
514 w.write_all(CURSOR_HIDE)
515}
516
517#[inline]
519pub fn cursor_show<W: Write>(w: &mut W) -> io::Result<()> {
520 w.write_all(CURSOR_SHOW)
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum EraseLineMode {
530 ToEnd = 0,
532 ToStart = 1,
534 All = 2,
536}
537
538pub fn erase_line<W: Write>(w: &mut W, mode: EraseLineMode) -> io::Result<()> {
540 match mode {
541 EraseLineMode::ToEnd => w.write_all(b"\x1b[K"),
542 EraseLineMode::ToStart => w.write_all(b"\x1b[1K"),
543 EraseLineMode::All => w.write_all(b"\x1b[2K"),
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
549pub enum EraseDisplayMode {
550 ToEnd = 0,
552 ToStart = 1,
554 All = 2,
556 Scrollback = 3,
558}
559
560pub fn erase_display<W: Write>(w: &mut W, mode: EraseDisplayMode) -> io::Result<()> {
562 match mode {
563 EraseDisplayMode::ToEnd => w.write_all(b"\x1b[J"),
564 EraseDisplayMode::ToStart => w.write_all(b"\x1b[1J"),
565 EraseDisplayMode::All => w.write_all(b"\x1b[2J"),
566 EraseDisplayMode::Scrollback => w.write_all(b"\x1b[3J"),
567 }
568}
569
570pub fn set_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
578 write!(w, "\x1b[{};{}r", (top as u32) + 1, (bottom as u32) + 1)
579}
580
581pub const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
583
584#[inline]
586pub fn reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
587 w.write_all(RESET_SCROLL_REGION)
588}
589
590pub const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
596
597pub const SYNC_END: &[u8] = b"\x1b[?2026l";
599
600#[inline]
602pub fn sync_begin<W: Write>(w: &mut W) -> io::Result<()> {
603 w.write_all(SYNC_BEGIN)
604}
605
606#[inline]
608pub fn sync_end<W: Write>(w: &mut W) -> io::Result<()> {
609 w.write_all(SYNC_END)
610}
611
612pub fn hyperlink_start<W: Write>(w: &mut W, url: &str) -> io::Result<()> {
621 if !osc8_field_is_safe(url) {
622 return Ok(());
623 }
624 write!(w, "\x1b]8;;{url}\x07")
625}
626
627pub fn hyperlink_end<W: Write>(w: &mut W) -> io::Result<()> {
631 w.write_all(b"\x1b]8;;\x07")
632}
633
634pub fn hyperlink_start_with_id<W: Write>(w: &mut W, id: &str, url: &str) -> io::Result<()> {
639 if !osc8_field_is_safe(url) || !osc8_field_is_safe(id) || id.contains(';') {
640 return Ok(());
641 }
642 write!(w, "\x1b]8;id={id};{url}\x07")
643}
644
645pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
651
652pub const ALT_SCREEN_LEAVE: &[u8] = b"\x1b[?1049l";
654
655pub const BRACKETED_PASTE_ENABLE: &[u8] = b"\x1b[?2004h";
657
658pub const BRACKETED_PASTE_DISABLE: &[u8] = b"\x1b[?2004l";
660
661pub 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";
674
675pub 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";
677
678pub const FOCUS_ENABLE: &[u8] = b"\x1b[?1004h";
680
681pub const FOCUS_DISABLE: &[u8] = b"\x1b[?1004l";
683
684#[inline]
686pub fn alt_screen_enter<W: Write>(w: &mut W) -> io::Result<()> {
687 w.write_all(ALT_SCREEN_ENTER)
688}
689
690#[inline]
692pub fn alt_screen_leave<W: Write>(w: &mut W) -> io::Result<()> {
693 w.write_all(ALT_SCREEN_LEAVE)
694}
695
696#[inline]
698pub fn bracketed_paste_enable<W: Write>(w: &mut W) -> io::Result<()> {
699 w.write_all(BRACKETED_PASTE_ENABLE)
700}
701
702#[inline]
704pub fn bracketed_paste_disable<W: Write>(w: &mut W) -> io::Result<()> {
705 w.write_all(BRACKETED_PASTE_DISABLE)
706}
707
708#[inline]
710pub fn mouse_enable<W: Write>(w: &mut W) -> io::Result<()> {
711 w.write_all(MOUSE_ENABLE)
712}
713
714#[inline]
716pub fn mouse_disable<W: Write>(w: &mut W) -> io::Result<()> {
717 w.write_all(MOUSE_DISABLE)
718}
719
720#[inline]
722pub fn focus_enable<W: Write>(w: &mut W) -> io::Result<()> {
723 w.write_all(FOCUS_ENABLE)
724}
725
726#[inline]
728pub fn focus_disable<W: Write>(w: &mut W) -> io::Result<()> {
729 w.write_all(FOCUS_DISABLE)
730}
731
732#[cfg(test)]
737mod tests {
738 use super::*;
739
740 fn to_bytes<F: FnOnce(&mut Vec<u8>) -> io::Result<()>>(f: F) -> Vec<u8> {
741 let mut buf = Vec::new();
742 f(&mut buf).unwrap();
743 buf
744 }
745
746 #[test]
749 fn sgr_reset_bytes() {
750 assert_eq!(to_bytes(sgr_reset), b"\x1b[0m");
751 }
752
753 #[test]
754 fn sgr_flags_bold() {
755 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)), b"\x1b[1m");
756 }
757
758 #[test]
759 fn sgr_flags_multiple() {
760 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
761 assert_eq!(to_bytes(|w| sgr_flags(w, flags)), b"\x1b[1;3;4m");
762 }
763
764 #[test]
765 fn sgr_flags_empty() {
766 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::empty())), b"");
767 }
768
769 #[test]
770 fn sgr_fg_rgb_bytes() {
771 assert_eq!(
772 to_bytes(|w| sgr_fg_rgb(w, 255, 128, 0)),
773 b"\x1b[38;2;255;128;0m"
774 );
775 }
776
777 #[test]
778 fn sgr_bg_rgb_bytes() {
779 assert_eq!(to_bytes(|w| sgr_bg_rgb(w, 0, 0, 0)), b"\x1b[48;2;0;0;0m");
780 }
781
782 #[test]
783 fn dynamic_sgr_rgb_matches_reference_formatting() {
784 for (r, g, b) in [(0, 0, 0), (1, 2, 3), (9, 10, 99), (100, 200, 255)] {
785 assert_eq!(
786 to_bytes(|w| sgr_fg_rgb(w, r, g, b)),
787 format!("\x1b[38;2;{r};{g};{b}m").into_bytes()
788 );
789 assert_eq!(
790 to_bytes(|w| sgr_bg_rgb(w, r, g, b)),
791 format!("\x1b[48;2;{r};{g};{b}m").into_bytes()
792 );
793 }
794 }
795
796 #[test]
797 fn sgr_fg_256_bytes() {
798 assert_eq!(to_bytes(|w| sgr_fg_256(w, 196)), b"\x1b[38;5;196m");
799 }
800
801 #[test]
802 fn sgr_bg_256_bytes() {
803 assert_eq!(to_bytes(|w| sgr_bg_256(w, 232)), b"\x1b[48;5;232m");
804 }
805
806 #[test]
807 fn sgr_fg_16_normal() {
808 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"); }
811
812 #[test]
813 fn sgr_fg_16_bright() {
814 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"); }
817
818 #[test]
819 fn sgr_bg_16_normal() {
820 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"); }
823
824 #[test]
825 fn sgr_bg_16_bright() {
826 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"); }
829
830 #[test]
831 fn sgr_default_colors() {
832 assert_eq!(to_bytes(sgr_fg_default), b"\x1b[39m");
833 assert_eq!(to_bytes(sgr_bg_default), b"\x1b[49m");
834 }
835
836 #[test]
837 fn sgr_packed_transparent_uses_default() {
838 assert_eq!(
839 to_bytes(|w| sgr_fg_packed(w, PackedRgba::TRANSPARENT)),
840 b"\x1b[39m"
841 );
842 assert_eq!(
843 to_bytes(|w| sgr_bg_packed(w, PackedRgba::TRANSPARENT)),
844 b"\x1b[49m"
845 );
846 }
847
848 #[test]
849 fn sgr_packed_opaque() {
850 let color = PackedRgba::rgb(10, 20, 30);
851 assert_eq!(
852 to_bytes(|w| sgr_fg_packed(w, color)),
853 b"\x1b[38;2;10;20;30m"
854 );
855 }
856
857 #[test]
860 fn cup_1_indexed() {
861 assert_eq!(to_bytes(|w| cup(w, 0, 0)), b"\x1b[1;1H");
862 assert_eq!(to_bytes(|w| cup(w, 23, 79)), b"\x1b[24;80H");
863 }
864
865 #[test]
866 fn cha_1_indexed() {
867 assert_eq!(to_bytes(|w| cha(w, 0)), b"\x1b[1G");
868 assert_eq!(to_bytes(|w| cha(w, 79)), b"\x1b[80G");
869 }
870
871 #[test]
872 fn cursor_relative_moves() {
873 assert_eq!(to_bytes(|w| cuu(w, 1)), b"\x1b[A");
874 assert_eq!(to_bytes(|w| cuu(w, 5)), b"\x1b[5A");
875 assert_eq!(to_bytes(|w| cud(w, 1)), b"\x1b[B");
876 assert_eq!(to_bytes(|w| cud(w, 3)), b"\x1b[3B");
877 assert_eq!(to_bytes(|w| cuf(w, 1)), b"\x1b[C");
878 assert_eq!(to_bytes(|w| cuf(w, 10)), b"\x1b[10C");
879 assert_eq!(to_bytes(|w| cub(w, 1)), b"\x1b[D");
880 assert_eq!(to_bytes(|w| cub(w, 2)), b"\x1b[2D");
881 }
882
883 #[test]
884 fn cursor_relative_zero_is_noop() {
885 assert_eq!(to_bytes(|w| cuu(w, 0)), b"");
886 assert_eq!(to_bytes(|w| cud(w, 0)), b"");
887 assert_eq!(to_bytes(|w| cuf(w, 0)), b"");
888 assert_eq!(to_bytes(|w| cub(w, 0)), b"");
889 }
890
891 #[test]
892 fn dynamic_cursor_sequences_match_reference_formatting() {
893 for (row, col) in [(0, 0), (23, 79), (999, 999), (u16::MAX, u16::MAX)] {
894 assert_eq!(
895 to_bytes(|w| cup(w, row, col)),
896 format!("\x1b[{};{}H", (row as u32) + 1, (col as u32) + 1).into_bytes()
897 );
898 }
899
900 for col in [0, 79, 999, u16::MAX] {
901 assert_eq!(
902 to_bytes(|w| cha(w, col)),
903 format!("\x1b[{}G", (col as u32) + 1).into_bytes()
904 );
905 }
906
907 for n in [0, 1, 2, 10, 999, u16::MAX] {
908 let expected_up = if n == 0 {
909 Vec::new()
910 } else if n == 1 {
911 b"\x1b[A".to_vec()
912 } else {
913 format!("\x1b[{n}A").into_bytes()
914 };
915 let expected_down = if n == 0 {
916 Vec::new()
917 } else if n == 1 {
918 b"\x1b[B".to_vec()
919 } else {
920 format!("\x1b[{n}B").into_bytes()
921 };
922 let expected_forward = if n == 0 {
923 Vec::new()
924 } else if n == 1 {
925 b"\x1b[C".to_vec()
926 } else {
927 format!("\x1b[{n}C").into_bytes()
928 };
929 let expected_back = if n == 0 {
930 Vec::new()
931 } else if n == 1 {
932 b"\x1b[D".to_vec()
933 } else {
934 format!("\x1b[{n}D").into_bytes()
935 };
936
937 assert_eq!(to_bytes(|w| cuu(w, n)), expected_up);
938 assert_eq!(to_bytes(|w| cud(w, n)), expected_down);
939 assert_eq!(to_bytes(|w| cuf(w, n)), expected_forward);
940 assert_eq!(to_bytes(|w| cub(w, n)), expected_back);
941 }
942 }
943
944 #[test]
945 fn cursor_save_restore() {
946 assert_eq!(to_bytes(cursor_save), b"\x1b7");
947 assert_eq!(to_bytes(cursor_restore), b"\x1b8");
948 }
949
950 #[test]
951 fn cursor_visibility() {
952 assert_eq!(to_bytes(cursor_hide), b"\x1b[?25l");
953 assert_eq!(to_bytes(cursor_show), b"\x1b[?25h");
954 }
955
956 #[test]
959 fn erase_line_modes() {
960 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::ToEnd)), b"\x1b[K");
961 assert_eq!(
962 to_bytes(|w| erase_line(w, EraseLineMode::ToStart)),
963 b"\x1b[1K"
964 );
965 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::All)), b"\x1b[2K");
966 }
967
968 #[test]
969 fn erase_display_modes() {
970 assert_eq!(
971 to_bytes(|w| erase_display(w, EraseDisplayMode::ToEnd)),
972 b"\x1b[J"
973 );
974 assert_eq!(
975 to_bytes(|w| erase_display(w, EraseDisplayMode::ToStart)),
976 b"\x1b[1J"
977 );
978 assert_eq!(
979 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
980 b"\x1b[2J"
981 );
982 assert_eq!(
983 to_bytes(|w| erase_display(w, EraseDisplayMode::Scrollback)),
984 b"\x1b[3J"
985 );
986 }
987
988 #[test]
991 fn scroll_region_1_indexed() {
992 assert_eq!(to_bytes(|w| set_scroll_region(w, 0, 23)), b"\x1b[1;24r");
993 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 20)), b"\x1b[6;21r");
994 }
995
996 #[test]
997 fn scroll_region_reset() {
998 assert_eq!(to_bytes(reset_scroll_region), b"\x1b[r");
999 }
1000
1001 #[test]
1004 fn sync_output() {
1005 assert_eq!(to_bytes(sync_begin), b"\x1b[?2026h");
1006 assert_eq!(to_bytes(sync_end), b"\x1b[?2026l");
1007 }
1008
1009 #[test]
1012 fn hyperlink_basic() {
1013 assert_eq!(
1014 to_bytes(|w| hyperlink_start(w, "https://example.com")),
1015 b"\x1b]8;;https://example.com\x07"
1016 );
1017 assert_eq!(to_bytes(hyperlink_end), b"\x1b]8;;\x07");
1018 }
1019
1020 #[test]
1021 fn hyperlink_with_id() {
1022 assert_eq!(
1023 to_bytes(|w| hyperlink_start_with_id(w, "link1", "https://example.com")),
1024 b"\x1b]8;id=link1;https://example.com\x07"
1025 );
1026 }
1027
1028 #[test]
1029 fn hyperlink_rejects_control_chars() {
1030 assert_eq!(
1031 to_bytes(|w| hyperlink_start(w, "https://exa\x1bmple.com")),
1032 b""
1033 );
1034 assert_eq!(
1035 to_bytes(|w| hyperlink_start_with_id(w, "id", "https://exa\u{009d}mple.com")),
1036 b""
1037 );
1038 }
1039
1040 #[test]
1041 fn hyperlink_with_id_rejects_parameter_breakout() {
1042 assert_eq!(
1043 to_bytes(|w| hyperlink_start_with_id(w, "id;malicious=1", "https://example.com")),
1044 b""
1045 );
1046 }
1047
1048 #[test]
1049 fn hyperlink_rejects_overlong_fields() {
1050 let long_url = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
1051 assert_eq!(to_bytes(|w| hyperlink_start(w, &long_url)), b"");
1052
1053 let long_id = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
1054 assert_eq!(
1055 to_bytes(|w| hyperlink_start_with_id(w, &long_id, "https://example.com")),
1056 b""
1057 );
1058 }
1059
1060 #[test]
1063 fn alt_screen() {
1064 assert_eq!(to_bytes(alt_screen_enter), b"\x1b[?1049h");
1065 assert_eq!(to_bytes(alt_screen_leave), b"\x1b[?1049l");
1066 }
1067
1068 #[test]
1069 fn bracketed_paste() {
1070 assert_eq!(to_bytes(bracketed_paste_enable), b"\x1b[?2004h");
1071 assert_eq!(to_bytes(bracketed_paste_disable), b"\x1b[?2004l");
1072 }
1073
1074 #[test]
1075 fn mouse_mode() {
1076 assert_eq!(
1077 to_bytes(mouse_enable),
1078 b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1006;1000;1002h\x1b[?1006h\x1b[?1000h\x1b[?1002h"
1079 );
1080 assert_eq!(
1081 to_bytes(mouse_disable),
1082 b"\x1b[?1000;1002;1006l\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l"
1083 );
1084
1085 let enabled = to_bytes(mouse_enable);
1086 assert!(
1087 !enabled.ends_with(b"\x1b[?1016l"),
1088 "mouse enable should not end with 1016l (can force X10 fallback)"
1089 );
1090 let pos_1016l = enabled
1091 .windows(b"\x1b[?1016l".len())
1092 .position(|w| w == b"\x1b[?1016l")
1093 .expect("mouse enable should clear 1016 before enabling SGR");
1094 let pos_1006h = enabled
1095 .windows(b"\x1b[?1006h".len())
1096 .position(|w| w == b"\x1b[?1006h")
1097 .expect("mouse enable should include 1006h");
1098 assert!(
1099 pos_1016l < pos_1006h,
1100 "1016l must be emitted before 1006h to preserve SGR mode"
1101 );
1102 }
1103
1104 #[test]
1105 fn focus_mode() {
1106 assert_eq!(to_bytes(focus_enable), b"\x1b[?1004h");
1107 assert_eq!(to_bytes(focus_disable), b"\x1b[?1004l");
1108 }
1109
1110 #[test]
1113 fn all_sequences_are_ascii() {
1114 for seq in [
1116 SGR_RESET,
1117 CURSOR_SAVE,
1118 CURSOR_RESTORE,
1119 CURSOR_HIDE,
1120 CURSOR_SHOW,
1121 RESET_SCROLL_REGION,
1122 SYNC_BEGIN,
1123 SYNC_END,
1124 ALT_SCREEN_ENTER,
1125 ALT_SCREEN_LEAVE,
1126 BRACKETED_PASTE_ENABLE,
1127 BRACKETED_PASTE_DISABLE,
1128 MOUSE_ENABLE,
1129 MOUSE_DISABLE,
1130 FOCUS_ENABLE,
1131 FOCUS_DISABLE,
1132 ] {
1133 for &byte in seq {
1134 assert!(byte < 128, "Non-ASCII byte {byte:#x} in sequence");
1135 }
1136 }
1137 }
1138
1139 #[test]
1140 fn osc_sequences_are_terminated() {
1141 let link_start = to_bytes(|w| hyperlink_start(w, "test"));
1143 assert!(
1144 link_start.ends_with(b"\x07"),
1145 "hyperlink_start not terminated with BEL"
1146 );
1147
1148 let link_end = to_bytes(hyperlink_end);
1149 assert!(
1150 link_end.ends_with(b"\x07"),
1151 "hyperlink_end not terminated with BEL"
1152 );
1153
1154 let link_id = to_bytes(|w| hyperlink_start_with_id(w, "id", "url"));
1155 assert!(
1156 link_id.ends_with(b"\x07"),
1157 "hyperlink_start_with_id not terminated with BEL"
1158 );
1159 }
1160
1161 #[test]
1164 fn sgr_flags_off_empty_is_noop() {
1165 let bytes = to_bytes(|w| {
1166 sgr_flags_off(w, StyleFlags::empty(), StyleFlags::empty()).unwrap();
1167 Ok(())
1168 });
1169 assert!(bytes.is_empty(), "disabling no flags should emit nothing");
1170 }
1171
1172 #[test]
1173 fn sgr_flags_off_single_bold() {
1174 let mut buf = Vec::new();
1175 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::empty()).unwrap();
1176 assert_eq!(buf, b"\x1b[22m");
1177 assert!(collateral.is_empty(), "no collateral when DIM is not kept");
1178 }
1179
1180 #[test]
1181 fn sgr_flags_off_single_dim() {
1182 let mut buf = Vec::new();
1183 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::empty()).unwrap();
1184 assert_eq!(buf, b"\x1b[22m");
1185 assert!(collateral.is_empty(), "no collateral when BOLD is not kept");
1186 }
1187
1188 #[test]
1189 fn sgr_flags_off_bold_collateral_dim() {
1190 let mut buf = Vec::new();
1192 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::DIM).unwrap();
1193 assert_eq!(buf, b"\x1b[22m");
1194 assert_eq!(collateral, StyleFlags::DIM);
1195 }
1196
1197 #[test]
1198 fn sgr_flags_off_dim_collateral_bold() {
1199 let mut buf = Vec::new();
1201 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::BOLD).unwrap();
1202 assert_eq!(buf, b"\x1b[22m");
1203 assert_eq!(collateral, StyleFlags::BOLD);
1204 }
1205
1206 #[test]
1207 fn sgr_flags_off_italic() {
1208 let mut buf = Vec::new();
1209 let collateral = sgr_flags_off(&mut buf, StyleFlags::ITALIC, StyleFlags::empty()).unwrap();
1210 assert_eq!(buf, b"\x1b[23m");
1211 assert!(collateral.is_empty());
1212 }
1213
1214 #[test]
1215 fn sgr_flags_off_underline() {
1216 let mut buf = Vec::new();
1217 let collateral =
1218 sgr_flags_off(&mut buf, StyleFlags::UNDERLINE, StyleFlags::empty()).unwrap();
1219 assert_eq!(buf, b"\x1b[24m");
1220 assert!(collateral.is_empty());
1221 }
1222
1223 #[test]
1224 fn sgr_flags_off_blink() {
1225 let mut buf = Vec::new();
1226 let collateral = sgr_flags_off(&mut buf, StyleFlags::BLINK, StyleFlags::empty()).unwrap();
1227 assert_eq!(buf, b"\x1b[25m");
1228 assert!(collateral.is_empty());
1229 }
1230
1231 #[test]
1232 fn sgr_flags_off_reverse() {
1233 let mut buf = Vec::new();
1234 let collateral = sgr_flags_off(&mut buf, StyleFlags::REVERSE, StyleFlags::empty()).unwrap();
1235 assert_eq!(buf, b"\x1b[27m");
1236 assert!(collateral.is_empty());
1237 }
1238
1239 #[test]
1240 fn sgr_flags_off_hidden() {
1241 let mut buf = Vec::new();
1242 let collateral = sgr_flags_off(&mut buf, StyleFlags::HIDDEN, StyleFlags::empty()).unwrap();
1243 assert_eq!(buf, b"\x1b[28m");
1244 assert!(collateral.is_empty());
1245 }
1246
1247 #[test]
1248 fn sgr_flags_off_strikethrough() {
1249 let mut buf = Vec::new();
1250 let collateral =
1251 sgr_flags_off(&mut buf, StyleFlags::STRIKETHROUGH, StyleFlags::empty()).unwrap();
1252 assert_eq!(buf, b"\x1b[29m");
1253 assert!(collateral.is_empty());
1254 }
1255
1256 #[test]
1257 fn sgr_flags_off_multi_no_bold_dim_overlap() {
1258 let mut buf = Vec::new();
1260 let collateral = sgr_flags_off(
1261 &mut buf,
1262 StyleFlags::ITALIC | StyleFlags::UNDERLINE,
1263 StyleFlags::empty(),
1264 )
1265 .unwrap();
1266 assert_eq!(buf, b"\x1b[23m\x1b[24m");
1268 assert!(collateral.is_empty());
1269 }
1270
1271 #[test]
1272 fn sgr_flags_off_bold_and_dim_together() {
1273 let mut buf = Vec::new();
1276 let collateral = sgr_flags_off(
1277 &mut buf,
1278 StyleFlags::BOLD | StyleFlags::DIM,
1279 StyleFlags::empty(),
1280 )
1281 .unwrap();
1282 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1283 assert!(
1284 collateral.is_empty(),
1285 "no collateral when both are disabled"
1286 );
1287 }
1288
1289 #[test]
1290 fn sgr_flags_off_overlap_keep_and_disable_does_not_report_collateral() {
1291 let mut buf = Vec::new();
1293 let collateral = sgr_flags_off(
1294 &mut buf,
1295 StyleFlags::BOLD | StyleFlags::DIM,
1296 StyleFlags::DIM,
1297 )
1298 .unwrap();
1299 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1300 assert!(
1301 collateral.is_empty(),
1302 "DIM is explicitly disabled, so it must not be reported as collateral"
1303 );
1304 }
1305
1306 #[test]
1307 fn sgr_flags_off_bold_dim_with_dim_kept() {
1308 let mut buf = Vec::new();
1310 let collateral = sgr_flags_off(
1311 &mut buf,
1312 StyleFlags::BOLD | StyleFlags::ITALIC,
1313 StyleFlags::DIM,
1314 )
1315 .unwrap();
1316 assert_eq!(
1317 collateral,
1318 StyleFlags::DIM,
1319 "DIM should be collateral damage from BOLD off (code 22)"
1320 );
1321 }
1322
1323 #[test]
1326 fn sgr_codes_for_all_single_flags() {
1327 let cases = [
1328 (StyleFlags::BOLD, 1, 22),
1329 (StyleFlags::DIM, 2, 22),
1330 (StyleFlags::ITALIC, 3, 23),
1331 (StyleFlags::UNDERLINE, 4, 24),
1332 (StyleFlags::BLINK, 5, 25),
1333 (StyleFlags::REVERSE, 7, 27),
1334 (StyleFlags::HIDDEN, 8, 28),
1335 (StyleFlags::STRIKETHROUGH, 9, 29),
1336 ];
1337 for (flag, expected_on, expected_off) in cases {
1338 let codes = sgr_codes_for_flag(flag)
1339 .unwrap_or_else(|| panic!("should return codes for {flag:?}"));
1340 assert_eq!(codes.on, expected_on, "on code for {flag:?}");
1341 assert_eq!(codes.off, expected_off, "off code for {flag:?}");
1342 }
1343 }
1344
1345 #[test]
1346 fn sgr_codes_for_composite_flag_returns_none() {
1347 let composite = StyleFlags::BOLD | StyleFlags::ITALIC;
1348 assert!(
1349 sgr_codes_for_flag(composite).is_none(),
1350 "composite flags should return None"
1351 );
1352 }
1353
1354 #[test]
1355 fn sgr_codes_for_empty_flag_returns_none() {
1356 assert!(
1357 sgr_codes_for_flag(StyleFlags::empty()).is_none(),
1358 "empty flags should return None"
1359 );
1360 }
1361
1362 #[test]
1363 fn sgr_codes_for_flag_matches_flag_table_entries() {
1364 for (flag, expected) in FLAG_TABLE {
1365 let actual = sgr_codes_for_flag(flag).expect("single-bit FLAG_TABLE entry");
1366 assert_eq!(actual.on, expected.on, "{flag:?} on code");
1367 assert_eq!(actual.off, expected.off, "{flag:?} off code");
1368 }
1369 }
1370
1371 #[test]
1374 fn cr_emits_carriage_return() {
1375 assert_eq!(to_bytes(cr), b"\r");
1376 }
1377
1378 #[test]
1379 fn lf_emits_line_feed() {
1380 assert_eq!(to_bytes(lf), b"\n");
1381 }
1382
1383 #[test]
1386 fn sgr_flags_each_single_flag_fast_path() {
1387 let cases: &[(StyleFlags, &[u8])] = &[
1388 (StyleFlags::BOLD, b"\x1b[1m"),
1389 (StyleFlags::DIM, b"\x1b[2m"),
1390 (StyleFlags::ITALIC, b"\x1b[3m"),
1391 (StyleFlags::UNDERLINE, b"\x1b[4m"),
1392 (StyleFlags::BLINK, b"\x1b[5m"),
1393 (StyleFlags::REVERSE, b"\x1b[7m"),
1394 (StyleFlags::STRIKETHROUGH, b"\x1b[9m"),
1395 (StyleFlags::HIDDEN, b"\x1b[8m"),
1396 ];
1397 for &(flag, expected) in cases {
1398 assert_eq!(
1399 to_bytes(|w| sgr_flags(w, flag)),
1400 expected,
1401 "single-flag fast path for {flag:?}"
1402 );
1403 }
1404 }
1405
1406 #[test]
1407 fn sgr_flags_all_eight() {
1408 let all = StyleFlags::BOLD
1409 | StyleFlags::DIM
1410 | StyleFlags::ITALIC
1411 | StyleFlags::UNDERLINE
1412 | StyleFlags::BLINK
1413 | StyleFlags::REVERSE
1414 | StyleFlags::HIDDEN
1415 | StyleFlags::STRIKETHROUGH;
1416 let bytes = to_bytes(|w| sgr_flags(w, all));
1417 assert_eq!(bytes, b"\x1b[1;2;3;4;5;7;8;9m");
1419 }
1420
1421 #[test]
1424 fn sgr_code_single_digit() {
1425 let mut buf = Vec::new();
1427 write_sgr_code(&mut buf, 1).unwrap();
1428 assert_eq!(buf, b"\x1b[1m");
1429 }
1430
1431 #[test]
1432 fn sgr_code_two_digits() {
1433 let mut buf = Vec::new();
1435 write_sgr_code(&mut buf, 22).unwrap();
1436 assert_eq!(buf, b"\x1b[22m");
1437 }
1438
1439 #[test]
1440 fn sgr_code_three_digits() {
1441 let mut buf = Vec::new();
1443 write_sgr_code(&mut buf, 100).unwrap();
1444 assert_eq!(buf, b"\x1b[100m");
1445 }
1446
1447 #[test]
1448 fn sgr_code_max_u8() {
1449 let mut buf = Vec::new();
1451 write_sgr_code(&mut buf, 255).unwrap();
1452 assert_eq!(buf, b"\x1b[255m");
1453 }
1454
1455 #[test]
1456 fn sgr_code_zero() {
1457 let mut buf = Vec::new();
1458 write_sgr_code(&mut buf, 0).unwrap();
1459 assert_eq!(buf, b"\x1b[0m");
1460 }
1461
1462 #[test]
1465 fn sgr_fg_16_boundary_7_to_8() {
1466 assert_eq!(to_bytes(|w| sgr_fg_16(w, 7)), b"\x1b[37m");
1468 assert_eq!(to_bytes(|w| sgr_fg_16(w, 8)), b"\x1b[90m");
1469 }
1470
1471 #[test]
1472 fn sgr_bg_16_boundary_7_to_8() {
1473 assert_eq!(to_bytes(|w| sgr_bg_16(w, 7)), b"\x1b[47m");
1474 assert_eq!(to_bytes(|w| sgr_bg_16(w, 8)), b"\x1b[100m");
1475 }
1476
1477 #[test]
1478 fn sgr_fg_16_first_color() {
1479 assert_eq!(to_bytes(|w| sgr_fg_16(w, 0)), b"\x1b[30m"); }
1481
1482 #[test]
1483 fn sgr_bg_16_last_bright() {
1484 assert_eq!(to_bytes(|w| sgr_bg_16(w, 15)), b"\x1b[107m"); }
1486
1487 #[test]
1490 fn sgr_fg_256_zero() {
1491 assert_eq!(to_bytes(|w| sgr_fg_256(w, 0)), b"\x1b[38;5;0m");
1492 }
1493
1494 #[test]
1495 fn sgr_fg_256_max() {
1496 assert_eq!(to_bytes(|w| sgr_fg_256(w, 255)), b"\x1b[38;5;255m");
1497 }
1498
1499 #[test]
1500 fn sgr_bg_256_zero() {
1501 assert_eq!(to_bytes(|w| sgr_bg_256(w, 0)), b"\x1b[48;5;0m");
1502 }
1503
1504 #[test]
1505 fn sgr_bg_256_max() {
1506 assert_eq!(to_bytes(|w| sgr_bg_256(w, 255)), b"\x1b[48;5;255m");
1507 }
1508
1509 #[test]
1512 fn cup_max_u16() {
1513 let bytes = to_bytes(|w| cup(w, u16::MAX, u16::MAX));
1515 let s = String::from_utf8(bytes).unwrap();
1516 assert!(s.starts_with("\x1b["));
1517 assert!(s.ends_with("H"));
1518 }
1519
1520 #[test]
1521 fn cha_max_u16() {
1522 let bytes = to_bytes(|w| cha(w, u16::MAX));
1523 let s = String::from_utf8(bytes).unwrap();
1524 assert!(s.starts_with("\x1b["));
1525 assert!(s.ends_with("G"));
1526 }
1527
1528 #[test]
1529 fn cursor_up_max() {
1530 let bytes = to_bytes(|w| cuu(w, u16::MAX));
1531 let s = String::from_utf8(bytes).unwrap();
1532 assert!(s.contains("65535"));
1533 assert!(s.ends_with("A"));
1534 }
1535
1536 #[test]
1539 fn scroll_region_same_top_bottom() {
1540 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 5)), b"\x1b[6;6r");
1541 }
1542
1543 #[test]
1546 fn sgr_flags_off_each_single_flag_fast_path() {
1547 let cases: &[(StyleFlags, &[u8])] = &[
1548 (StyleFlags::BOLD, b"\x1b[22m"),
1549 (StyleFlags::DIM, b"\x1b[22m"),
1550 (StyleFlags::ITALIC, b"\x1b[23m"),
1551 (StyleFlags::UNDERLINE, b"\x1b[24m"),
1552 (StyleFlags::BLINK, b"\x1b[25m"),
1553 (StyleFlags::REVERSE, b"\x1b[27m"),
1554 (StyleFlags::STRIKETHROUGH, b"\x1b[29m"),
1555 (StyleFlags::HIDDEN, b"\x1b[28m"),
1556 ];
1557 for &(flag, expected) in cases {
1558 let mut buf = Vec::new();
1559 let collateral = sgr_flags_off(&mut buf, flag, StyleFlags::empty()).unwrap();
1560 assert_eq!(buf, expected, "off sequence for {flag:?}");
1561 assert!(collateral.is_empty(), "no collateral for {flag:?}");
1562 }
1563 }
1564
1565 #[test]
1568 fn sgr_bg_packed_opaque() {
1569 let color = PackedRgba::rgb(100, 200, 50);
1570 assert_eq!(
1571 to_bytes(|w| sgr_bg_packed(w, color)),
1572 b"\x1b[48;2;100;200;50m"
1573 );
1574 }
1575
1576 #[test]
1579 fn hyperlink_empty_url() {
1580 assert_eq!(to_bytes(|w| hyperlink_start(w, "")), b"\x1b]8;;\x07");
1581 }
1582
1583 #[test]
1584 fn hyperlink_with_empty_id() {
1585 assert_eq!(
1586 to_bytes(|w| hyperlink_start_with_id(w, "", "https://x.com")),
1587 b"\x1b]8;id=;https://x.com\x07"
1588 );
1589 }
1590
1591 #[test]
1594 fn all_dynamic_sequences_start_with_esc() {
1595 let sequences: Vec<Vec<u8>> = vec![
1596 to_bytes(sgr_reset),
1597 to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)),
1598 to_bytes(|w| sgr_fg_rgb(w, 1, 2, 3)),
1599 to_bytes(|w| sgr_bg_rgb(w, 1, 2, 3)),
1600 to_bytes(|w| sgr_fg_256(w, 42)),
1601 to_bytes(|w| sgr_bg_256(w, 42)),
1602 to_bytes(|w| sgr_fg_16(w, 5)),
1603 to_bytes(|w| sgr_bg_16(w, 5)),
1604 to_bytes(sgr_fg_default),
1605 to_bytes(sgr_bg_default),
1606 to_bytes(|w| cup(w, 0, 0)),
1607 to_bytes(|w| cha(w, 0)),
1608 to_bytes(|w| cuu(w, 1)),
1609 to_bytes(|w| cud(w, 1)),
1610 to_bytes(|w| cuf(w, 1)),
1611 to_bytes(|w| cub(w, 1)),
1612 to_bytes(cursor_save),
1613 to_bytes(cursor_restore),
1614 to_bytes(cursor_hide),
1615 to_bytes(cursor_show),
1616 to_bytes(|w| erase_line(w, EraseLineMode::All)),
1617 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
1618 to_bytes(|w| set_scroll_region(w, 0, 23)),
1619 to_bytes(reset_scroll_region),
1620 to_bytes(sync_begin),
1621 to_bytes(sync_end),
1622 to_bytes(|w| hyperlink_start(w, "test")),
1623 to_bytes(hyperlink_end),
1624 to_bytes(alt_screen_enter),
1625 to_bytes(alt_screen_leave),
1626 to_bytes(bracketed_paste_enable),
1627 to_bytes(bracketed_paste_disable),
1628 to_bytes(mouse_enable),
1629 to_bytes(mouse_disable),
1630 to_bytes(focus_enable),
1631 to_bytes(focus_disable),
1632 ];
1633 for (i, seq) in sequences.iter().enumerate() {
1634 assert!(
1635 seq.starts_with(b"\x1b"),
1636 "sequence {i} should start with ESC, got {seq:?}"
1637 );
1638 }
1639 }
1640}