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_sgr_code<W: Write>(w: &mut W, code: u8) -> io::Result<()> {
117 let mut buf = [0u8; 6];
118 buf[0] = 0x1b;
119 buf[1] = b'[';
120 let len = write_u8_dec(&mut buf[2..], code);
121 buf[2 + len] = b'm';
122 w.write_all(&buf[..2 + len + 1])
123}
124
125pub fn sgr_flags<W: Write>(w: &mut W, flags: StyleFlags) -> io::Result<()> {
130 if flags.is_empty() {
131 return Ok(());
132 }
133
134 let bits = flags.bits();
135 if bits.is_power_of_two()
136 && let Some(seq) = sgr_single_flag_seq(bits)
137 {
138 return w.write_all(seq);
139 }
140
141 let mut buf = [0u8; 32];
142 let mut idx = 0usize;
143 buf[idx] = 0x1b;
144 buf[idx + 1] = b'[';
145 idx += 2;
146 let mut first = true;
147
148 for (flag, codes) in FLAG_TABLE {
149 if flags.contains(flag) {
150 if !first {
151 buf[idx] = b';';
152 idx += 1;
153 }
154 idx += write_u8_dec(&mut buf[idx..], codes.on);
155 first = false;
156 }
157 }
158
159 buf[idx] = b'm';
160 idx += 1;
161 w.write_all(&buf[..idx])
162}
163
164pub const FLAG_TABLE: [(StyleFlags, SgrCodes); 8] = [
166 (StyleFlags::BOLD, SGR_BOLD),
167 (StyleFlags::DIM, SGR_DIM),
168 (StyleFlags::ITALIC, SGR_ITALIC),
169 (StyleFlags::UNDERLINE, SGR_UNDERLINE),
170 (StyleFlags::BLINK, SGR_BLINK),
171 (StyleFlags::REVERSE, SGR_REVERSE),
172 (StyleFlags::HIDDEN, SGR_HIDDEN),
173 (StyleFlags::STRIKETHROUGH, SGR_STRIKETHROUGH),
174];
175
176#[inline]
177fn sgr_single_flag_seq(bits: u8) -> Option<&'static [u8]> {
178 match bits {
179 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,
188 }
189}
190
191#[inline]
192fn sgr_single_flag_off_seq(bits: u8) -> Option<&'static [u8]> {
193 match bits {
194 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,
203 }
204}
205
206pub fn sgr_flags_off<W: Write>(
217 w: &mut W,
218 flags_to_disable: StyleFlags,
219 flags_to_keep: StyleFlags,
220) -> io::Result<StyleFlags> {
221 if flags_to_disable.is_empty() {
222 return Ok(StyleFlags::empty());
223 }
224
225 let disable_bits = flags_to_disable.bits();
226 if disable_bits.is_power_of_two()
227 && let Some(seq) = sgr_single_flag_off_seq(disable_bits)
228 {
229 w.write_all(seq)?;
230 if disable_bits == StyleFlags::BOLD.bits() && flags_to_keep.contains(StyleFlags::DIM) {
231 return Ok(StyleFlags::DIM);
232 }
233 if disable_bits == StyleFlags::DIM.bits() && flags_to_keep.contains(StyleFlags::BOLD) {
234 return Ok(StyleFlags::BOLD);
235 }
236 return Ok(StyleFlags::empty());
237 }
238
239 let mut collateral = StyleFlags::empty();
240
241 for (flag, codes) in FLAG_TABLE {
242 if !flags_to_disable.contains(flag) {
243 continue;
244 }
245 write_sgr_code(w, codes.off)?;
247 if codes.off == 22 {
249 let other = if flag == StyleFlags::BOLD {
251 StyleFlags::DIM
252 } else {
253 StyleFlags::BOLD
254 };
255 if flags_to_keep.contains(other) && !flags_to_disable.contains(other) {
256 collateral |= other;
257 }
258 }
259 }
260
261 Ok(collateral)
262}
263
264pub fn sgr_fg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
266 write!(w, "\x1b[38;2;{r};{g};{b}m")
267}
268
269pub fn sgr_bg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
271 write!(w, "\x1b[48;2;{r};{g};{b}m")
272}
273
274pub fn sgr_fg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
276 write!(w, "\x1b[38;5;{index}m")
277}
278
279pub fn sgr_bg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
281 write!(w, "\x1b[48;5;{index}m")
282}
283
284pub fn sgr_fg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
288 let code = if index < 8 {
289 30 + index
290 } else {
291 90 + index - 8
292 };
293 write!(w, "\x1b[{code}m")
294}
295
296pub fn sgr_bg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
300 let code = if index < 8 {
301 40 + index
302 } else {
303 100 + index - 8
304 };
305 write!(w, "\x1b[{code}m")
306}
307
308pub fn sgr_fg_default<W: Write>(w: &mut W) -> io::Result<()> {
310 w.write_all(b"\x1b[39m")
311}
312
313pub fn sgr_bg_default<W: Write>(w: &mut W) -> io::Result<()> {
315 w.write_all(b"\x1b[49m")
316}
317
318pub fn sgr_fg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
322 if color.a() == 0 {
323 return sgr_fg_default(w);
324 }
325 sgr_fg_rgb(w, color.r(), color.g(), color.b())
326}
327
328pub fn sgr_bg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
332 if color.a() == 0 {
333 return sgr_bg_default(w);
334 }
335 sgr_bg_rgb(w, color.r(), color.g(), color.b())
336}
337
338pub fn cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
347 write!(
348 w,
349 "\x1b[{};{}H",
350 row.saturating_add(1),
351 col.saturating_add(1)
352 )
353}
354
355pub fn cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
359 write!(w, "\x1b[{}G", col.saturating_add(1))
360}
361
362pub fn cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
364 if n == 0 {
365 return Ok(());
366 }
367 if n == 1 {
368 w.write_all(b"\x1b[A")
369 } else {
370 write!(w, "\x1b[{n}A")
371 }
372}
373
374pub fn cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
376 if n == 0 {
377 return Ok(());
378 }
379 if n == 1 {
380 w.write_all(b"\x1b[B")
381 } else {
382 write!(w, "\x1b[{n}B")
383 }
384}
385
386pub fn cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
388 if n == 0 {
389 return Ok(());
390 }
391 if n == 1 {
392 w.write_all(b"\x1b[C")
393 } else {
394 write!(w, "\x1b[{n}C")
395 }
396}
397
398pub fn cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
400 if n == 0 {
401 return Ok(());
402 }
403 if n == 1 {
404 w.write_all(b"\x1b[D")
405 } else {
406 write!(w, "\x1b[{n}D")
407 }
408}
409
410#[inline]
412pub fn cr<W: Write>(w: &mut W) -> io::Result<()> {
413 w.write_all(b"\r")
414}
415
416#[inline]
420pub fn lf<W: Write>(w: &mut W) -> io::Result<()> {
421 w.write_all(b"\n")
422}
423
424pub const CURSOR_SAVE: &[u8] = b"\x1b7";
426
427pub const CURSOR_RESTORE: &[u8] = b"\x1b8";
429
430#[inline]
432pub fn cursor_save<W: Write>(w: &mut W) -> io::Result<()> {
433 w.write_all(CURSOR_SAVE)
434}
435
436#[inline]
438pub fn cursor_restore<W: Write>(w: &mut W) -> io::Result<()> {
439 w.write_all(CURSOR_RESTORE)
440}
441
442pub const CURSOR_HIDE: &[u8] = b"\x1b[?25l";
444
445pub const CURSOR_SHOW: &[u8] = b"\x1b[?25h";
447
448#[inline]
450pub fn cursor_hide<W: Write>(w: &mut W) -> io::Result<()> {
451 w.write_all(CURSOR_HIDE)
452}
453
454#[inline]
456pub fn cursor_show<W: Write>(w: &mut W) -> io::Result<()> {
457 w.write_all(CURSOR_SHOW)
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum EraseLineMode {
467 ToEnd = 0,
469 ToStart = 1,
471 All = 2,
473}
474
475pub fn erase_line<W: Write>(w: &mut W, mode: EraseLineMode) -> io::Result<()> {
477 match mode {
478 EraseLineMode::ToEnd => w.write_all(b"\x1b[K"),
479 EraseLineMode::ToStart => w.write_all(b"\x1b[1K"),
480 EraseLineMode::All => w.write_all(b"\x1b[2K"),
481 }
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum EraseDisplayMode {
487 ToEnd = 0,
489 ToStart = 1,
491 All = 2,
493 Scrollback = 3,
495}
496
497pub fn erase_display<W: Write>(w: &mut W, mode: EraseDisplayMode) -> io::Result<()> {
499 match mode {
500 EraseDisplayMode::ToEnd => w.write_all(b"\x1b[J"),
501 EraseDisplayMode::ToStart => w.write_all(b"\x1b[1J"),
502 EraseDisplayMode::All => w.write_all(b"\x1b[2J"),
503 EraseDisplayMode::Scrollback => w.write_all(b"\x1b[3J"),
504 }
505}
506
507pub fn set_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
515 write!(
516 w,
517 "\x1b[{};{}r",
518 top.saturating_add(1),
519 bottom.saturating_add(1)
520 )
521}
522
523pub const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
525
526#[inline]
528pub fn reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
529 w.write_all(RESET_SCROLL_REGION)
530}
531
532pub const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
538
539pub const SYNC_END: &[u8] = b"\x1b[?2026l";
541
542#[inline]
544pub fn sync_begin<W: Write>(w: &mut W) -> io::Result<()> {
545 w.write_all(SYNC_BEGIN)
546}
547
548#[inline]
550pub fn sync_end<W: Write>(w: &mut W) -> io::Result<()> {
551 w.write_all(SYNC_END)
552}
553
554pub fn hyperlink_start<W: Write>(w: &mut W, url: &str) -> io::Result<()> {
563 if !osc8_field_is_safe(url) {
564 return Ok(());
565 }
566 write!(w, "\x1b]8;;{url}\x1b\\")
567}
568
569pub fn hyperlink_end<W: Write>(w: &mut W) -> io::Result<()> {
573 w.write_all(b"\x1b]8;;\x1b\\")
574}
575
576pub fn hyperlink_start_with_id<W: Write>(w: &mut W, id: &str, url: &str) -> io::Result<()> {
581 if !osc8_field_is_safe(url) || !osc8_field_is_safe(id) || id.contains(';') {
582 return Ok(());
583 }
584 write!(w, "\x1b]8;id={id};{url}\x1b\\")
585}
586
587pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
593
594pub const ALT_SCREEN_LEAVE: &[u8] = b"\x1b[?1049l";
596
597pub const BRACKETED_PASTE_ENABLE: &[u8] = b"\x1b[?2004h";
599
600pub const BRACKETED_PASTE_DISABLE: &[u8] = b"\x1b[?2004l";
602
603pub const MOUSE_ENABLE: &[u8] = b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1000;1002;1006h\x1b[?1000h\x1b[?1002h\x1b[?1006h";
614
615pub 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";
617
618pub const FOCUS_ENABLE: &[u8] = b"\x1b[?1004h";
620
621pub const FOCUS_DISABLE: &[u8] = b"\x1b[?1004l";
623
624#[inline]
626pub fn alt_screen_enter<W: Write>(w: &mut W) -> io::Result<()> {
627 w.write_all(ALT_SCREEN_ENTER)
628}
629
630#[inline]
632pub fn alt_screen_leave<W: Write>(w: &mut W) -> io::Result<()> {
633 w.write_all(ALT_SCREEN_LEAVE)
634}
635
636#[inline]
638pub fn bracketed_paste_enable<W: Write>(w: &mut W) -> io::Result<()> {
639 w.write_all(BRACKETED_PASTE_ENABLE)
640}
641
642#[inline]
644pub fn bracketed_paste_disable<W: Write>(w: &mut W) -> io::Result<()> {
645 w.write_all(BRACKETED_PASTE_DISABLE)
646}
647
648#[inline]
650pub fn mouse_enable<W: Write>(w: &mut W) -> io::Result<()> {
651 w.write_all(MOUSE_ENABLE)
652}
653
654#[inline]
656pub fn mouse_disable<W: Write>(w: &mut W) -> io::Result<()> {
657 w.write_all(MOUSE_DISABLE)
658}
659
660#[inline]
662pub fn focus_enable<W: Write>(w: &mut W) -> io::Result<()> {
663 w.write_all(FOCUS_ENABLE)
664}
665
666#[inline]
668pub fn focus_disable<W: Write>(w: &mut W) -> io::Result<()> {
669 w.write_all(FOCUS_DISABLE)
670}
671
672#[cfg(test)]
677mod tests {
678 use super::*;
679
680 fn to_bytes<F: FnOnce(&mut Vec<u8>) -> io::Result<()>>(f: F) -> Vec<u8> {
681 let mut buf = Vec::new();
682 f(&mut buf).unwrap();
683 buf
684 }
685
686 #[test]
689 fn sgr_reset_bytes() {
690 assert_eq!(to_bytes(sgr_reset), b"\x1b[0m");
691 }
692
693 #[test]
694 fn sgr_flags_bold() {
695 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)), b"\x1b[1m");
696 }
697
698 #[test]
699 fn sgr_flags_multiple() {
700 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
701 assert_eq!(to_bytes(|w| sgr_flags(w, flags)), b"\x1b[1;3;4m");
702 }
703
704 #[test]
705 fn sgr_flags_empty() {
706 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::empty())), b"");
707 }
708
709 #[test]
710 fn sgr_fg_rgb_bytes() {
711 assert_eq!(
712 to_bytes(|w| sgr_fg_rgb(w, 255, 128, 0)),
713 b"\x1b[38;2;255;128;0m"
714 );
715 }
716
717 #[test]
718 fn sgr_bg_rgb_bytes() {
719 assert_eq!(to_bytes(|w| sgr_bg_rgb(w, 0, 0, 0)), b"\x1b[48;2;0;0;0m");
720 }
721
722 #[test]
723 fn sgr_fg_256_bytes() {
724 assert_eq!(to_bytes(|w| sgr_fg_256(w, 196)), b"\x1b[38;5;196m");
725 }
726
727 #[test]
728 fn sgr_bg_256_bytes() {
729 assert_eq!(to_bytes(|w| sgr_bg_256(w, 232)), b"\x1b[48;5;232m");
730 }
731
732 #[test]
733 fn sgr_fg_16_normal() {
734 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"); }
737
738 #[test]
739 fn sgr_fg_16_bright() {
740 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"); }
743
744 #[test]
745 fn sgr_bg_16_normal() {
746 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"); }
749
750 #[test]
751 fn sgr_bg_16_bright() {
752 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"); }
755
756 #[test]
757 fn sgr_default_colors() {
758 assert_eq!(to_bytes(sgr_fg_default), b"\x1b[39m");
759 assert_eq!(to_bytes(sgr_bg_default), b"\x1b[49m");
760 }
761
762 #[test]
763 fn sgr_packed_transparent_uses_default() {
764 assert_eq!(
765 to_bytes(|w| sgr_fg_packed(w, PackedRgba::TRANSPARENT)),
766 b"\x1b[39m"
767 );
768 assert_eq!(
769 to_bytes(|w| sgr_bg_packed(w, PackedRgba::TRANSPARENT)),
770 b"\x1b[49m"
771 );
772 }
773
774 #[test]
775 fn sgr_packed_opaque() {
776 let color = PackedRgba::rgb(10, 20, 30);
777 assert_eq!(
778 to_bytes(|w| sgr_fg_packed(w, color)),
779 b"\x1b[38;2;10;20;30m"
780 );
781 }
782
783 #[test]
786 fn cup_1_indexed() {
787 assert_eq!(to_bytes(|w| cup(w, 0, 0)), b"\x1b[1;1H");
788 assert_eq!(to_bytes(|w| cup(w, 23, 79)), b"\x1b[24;80H");
789 }
790
791 #[test]
792 fn cha_1_indexed() {
793 assert_eq!(to_bytes(|w| cha(w, 0)), b"\x1b[1G");
794 assert_eq!(to_bytes(|w| cha(w, 79)), b"\x1b[80G");
795 }
796
797 #[test]
798 fn cursor_relative_moves() {
799 assert_eq!(to_bytes(|w| cuu(w, 1)), b"\x1b[A");
800 assert_eq!(to_bytes(|w| cuu(w, 5)), b"\x1b[5A");
801 assert_eq!(to_bytes(|w| cud(w, 1)), b"\x1b[B");
802 assert_eq!(to_bytes(|w| cud(w, 3)), b"\x1b[3B");
803 assert_eq!(to_bytes(|w| cuf(w, 1)), b"\x1b[C");
804 assert_eq!(to_bytes(|w| cuf(w, 10)), b"\x1b[10C");
805 assert_eq!(to_bytes(|w| cub(w, 1)), b"\x1b[D");
806 assert_eq!(to_bytes(|w| cub(w, 2)), b"\x1b[2D");
807 }
808
809 #[test]
810 fn cursor_relative_zero_is_noop() {
811 assert_eq!(to_bytes(|w| cuu(w, 0)), b"");
812 assert_eq!(to_bytes(|w| cud(w, 0)), b"");
813 assert_eq!(to_bytes(|w| cuf(w, 0)), b"");
814 assert_eq!(to_bytes(|w| cub(w, 0)), b"");
815 }
816
817 #[test]
818 fn cursor_save_restore() {
819 assert_eq!(to_bytes(cursor_save), b"\x1b7");
820 assert_eq!(to_bytes(cursor_restore), b"\x1b8");
821 }
822
823 #[test]
824 fn cursor_visibility() {
825 assert_eq!(to_bytes(cursor_hide), b"\x1b[?25l");
826 assert_eq!(to_bytes(cursor_show), b"\x1b[?25h");
827 }
828
829 #[test]
832 fn erase_line_modes() {
833 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::ToEnd)), b"\x1b[K");
834 assert_eq!(
835 to_bytes(|w| erase_line(w, EraseLineMode::ToStart)),
836 b"\x1b[1K"
837 );
838 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::All)), b"\x1b[2K");
839 }
840
841 #[test]
842 fn erase_display_modes() {
843 assert_eq!(
844 to_bytes(|w| erase_display(w, EraseDisplayMode::ToEnd)),
845 b"\x1b[J"
846 );
847 assert_eq!(
848 to_bytes(|w| erase_display(w, EraseDisplayMode::ToStart)),
849 b"\x1b[1J"
850 );
851 assert_eq!(
852 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
853 b"\x1b[2J"
854 );
855 assert_eq!(
856 to_bytes(|w| erase_display(w, EraseDisplayMode::Scrollback)),
857 b"\x1b[3J"
858 );
859 }
860
861 #[test]
864 fn scroll_region_1_indexed() {
865 assert_eq!(to_bytes(|w| set_scroll_region(w, 0, 23)), b"\x1b[1;24r");
866 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 20)), b"\x1b[6;21r");
867 }
868
869 #[test]
870 fn scroll_region_reset() {
871 assert_eq!(to_bytes(reset_scroll_region), b"\x1b[r");
872 }
873
874 #[test]
877 fn sync_output() {
878 assert_eq!(to_bytes(sync_begin), b"\x1b[?2026h");
879 assert_eq!(to_bytes(sync_end), b"\x1b[?2026l");
880 }
881
882 #[test]
885 fn hyperlink_basic() {
886 assert_eq!(
887 to_bytes(|w| hyperlink_start(w, "https://example.com")),
888 b"\x1b]8;;https://example.com\x1b\\"
889 );
890 assert_eq!(to_bytes(hyperlink_end), b"\x1b]8;;\x1b\\");
891 }
892
893 #[test]
894 fn hyperlink_with_id() {
895 assert_eq!(
896 to_bytes(|w| hyperlink_start_with_id(w, "link1", "https://example.com")),
897 b"\x1b]8;id=link1;https://example.com\x1b\\"
898 );
899 }
900
901 #[test]
902 fn hyperlink_rejects_control_chars() {
903 assert_eq!(
904 to_bytes(|w| hyperlink_start(w, "https://exa\x1bmple.com")),
905 b""
906 );
907 assert_eq!(
908 to_bytes(|w| hyperlink_start_with_id(w, "id", "https://exa\u{009d}mple.com")),
909 b""
910 );
911 }
912
913 #[test]
914 fn hyperlink_with_id_rejects_parameter_breakout() {
915 assert_eq!(
916 to_bytes(|w| hyperlink_start_with_id(w, "id;malicious=1", "https://example.com")),
917 b""
918 );
919 }
920
921 #[test]
922 fn hyperlink_rejects_overlong_fields() {
923 let long_url = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
924 assert_eq!(to_bytes(|w| hyperlink_start(w, &long_url)), b"");
925
926 let long_id = "x".repeat(MAX_OSC8_FIELD_BYTES + 1);
927 assert_eq!(
928 to_bytes(|w| hyperlink_start_with_id(w, &long_id, "https://example.com")),
929 b""
930 );
931 }
932
933 #[test]
936 fn alt_screen() {
937 assert_eq!(to_bytes(alt_screen_enter), b"\x1b[?1049h");
938 assert_eq!(to_bytes(alt_screen_leave), b"\x1b[?1049l");
939 }
940
941 #[test]
942 fn bracketed_paste() {
943 assert_eq!(to_bytes(bracketed_paste_enable), b"\x1b[?2004h");
944 assert_eq!(to_bytes(bracketed_paste_disable), b"\x1b[?2004l");
945 }
946
947 #[test]
948 fn mouse_mode() {
949 assert_eq!(
950 to_bytes(mouse_enable),
951 b"\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?1000;1002;1006h\x1b[?1000h\x1b[?1002h\x1b[?1006h"
952 );
953 assert_eq!(
954 to_bytes(mouse_disable),
955 b"\x1b[?1000;1002;1006l\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1001l\x1b[?1003l\x1b[?1005l\x1b[?1015l\x1b[?1016l"
956 );
957
958 let enabled = to_bytes(mouse_enable);
959 assert!(
960 !enabled.ends_with(b"\x1b[?1016l"),
961 "mouse enable should not end with 1016l (can force X10 fallback)"
962 );
963 let pos_1016l = enabled
964 .windows(b"\x1b[?1016l".len())
965 .position(|w| w == b"\x1b[?1016l")
966 .expect("mouse enable should clear 1016 before enabling SGR");
967 let pos_1006h = enabled
968 .windows(b"\x1b[?1006h".len())
969 .position(|w| w == b"\x1b[?1006h")
970 .expect("mouse enable should include 1006h");
971 assert!(
972 pos_1016l < pos_1006h,
973 "1016l must be emitted before 1006h to preserve SGR mode"
974 );
975 }
976
977 #[test]
978 fn focus_mode() {
979 assert_eq!(to_bytes(focus_enable), b"\x1b[?1004h");
980 assert_eq!(to_bytes(focus_disable), b"\x1b[?1004l");
981 }
982
983 #[test]
986 fn all_sequences_are_ascii() {
987 for seq in [
989 SGR_RESET,
990 CURSOR_SAVE,
991 CURSOR_RESTORE,
992 CURSOR_HIDE,
993 CURSOR_SHOW,
994 RESET_SCROLL_REGION,
995 SYNC_BEGIN,
996 SYNC_END,
997 ALT_SCREEN_ENTER,
998 ALT_SCREEN_LEAVE,
999 BRACKETED_PASTE_ENABLE,
1000 BRACKETED_PASTE_DISABLE,
1001 MOUSE_ENABLE,
1002 MOUSE_DISABLE,
1003 FOCUS_ENABLE,
1004 FOCUS_DISABLE,
1005 ] {
1006 for &byte in seq {
1007 assert!(byte < 128, "Non-ASCII byte {byte:#x} in sequence");
1008 }
1009 }
1010 }
1011
1012 #[test]
1013 fn osc_sequences_are_terminated() {
1014 let link_start = to_bytes(|w| hyperlink_start(w, "test"));
1016 assert!(
1017 link_start.ends_with(b"\x1b\\"),
1018 "hyperlink_start not terminated with ST"
1019 );
1020
1021 let link_end = to_bytes(hyperlink_end);
1022 assert!(
1023 link_end.ends_with(b"\x1b\\"),
1024 "hyperlink_end not terminated with ST"
1025 );
1026
1027 let link_id = to_bytes(|w| hyperlink_start_with_id(w, "id", "url"));
1028 assert!(
1029 link_id.ends_with(b"\x1b\\"),
1030 "hyperlink_start_with_id not terminated with ST"
1031 );
1032 }
1033
1034 #[test]
1037 fn sgr_flags_off_empty_is_noop() {
1038 let bytes = to_bytes(|w| {
1039 sgr_flags_off(w, StyleFlags::empty(), StyleFlags::empty()).unwrap();
1040 Ok(())
1041 });
1042 assert!(bytes.is_empty(), "disabling no flags should emit nothing");
1043 }
1044
1045 #[test]
1046 fn sgr_flags_off_single_bold() {
1047 let mut buf = Vec::new();
1048 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::empty()).unwrap();
1049 assert_eq!(buf, b"\x1b[22m");
1050 assert!(collateral.is_empty(), "no collateral when DIM is not kept");
1051 }
1052
1053 #[test]
1054 fn sgr_flags_off_single_dim() {
1055 let mut buf = Vec::new();
1056 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::empty()).unwrap();
1057 assert_eq!(buf, b"\x1b[22m");
1058 assert!(collateral.is_empty(), "no collateral when BOLD is not kept");
1059 }
1060
1061 #[test]
1062 fn sgr_flags_off_bold_collateral_dim() {
1063 let mut buf = Vec::new();
1065 let collateral = sgr_flags_off(&mut buf, StyleFlags::BOLD, StyleFlags::DIM).unwrap();
1066 assert_eq!(buf, b"\x1b[22m");
1067 assert_eq!(collateral, StyleFlags::DIM);
1068 }
1069
1070 #[test]
1071 fn sgr_flags_off_dim_collateral_bold() {
1072 let mut buf = Vec::new();
1074 let collateral = sgr_flags_off(&mut buf, StyleFlags::DIM, StyleFlags::BOLD).unwrap();
1075 assert_eq!(buf, b"\x1b[22m");
1076 assert_eq!(collateral, StyleFlags::BOLD);
1077 }
1078
1079 #[test]
1080 fn sgr_flags_off_italic() {
1081 let mut buf = Vec::new();
1082 let collateral = sgr_flags_off(&mut buf, StyleFlags::ITALIC, StyleFlags::empty()).unwrap();
1083 assert_eq!(buf, b"\x1b[23m");
1084 assert!(collateral.is_empty());
1085 }
1086
1087 #[test]
1088 fn sgr_flags_off_underline() {
1089 let mut buf = Vec::new();
1090 let collateral =
1091 sgr_flags_off(&mut buf, StyleFlags::UNDERLINE, StyleFlags::empty()).unwrap();
1092 assert_eq!(buf, b"\x1b[24m");
1093 assert!(collateral.is_empty());
1094 }
1095
1096 #[test]
1097 fn sgr_flags_off_blink() {
1098 let mut buf = Vec::new();
1099 let collateral = sgr_flags_off(&mut buf, StyleFlags::BLINK, StyleFlags::empty()).unwrap();
1100 assert_eq!(buf, b"\x1b[25m");
1101 assert!(collateral.is_empty());
1102 }
1103
1104 #[test]
1105 fn sgr_flags_off_reverse() {
1106 let mut buf = Vec::new();
1107 let collateral = sgr_flags_off(&mut buf, StyleFlags::REVERSE, StyleFlags::empty()).unwrap();
1108 assert_eq!(buf, b"\x1b[27m");
1109 assert!(collateral.is_empty());
1110 }
1111
1112 #[test]
1113 fn sgr_flags_off_hidden() {
1114 let mut buf = Vec::new();
1115 let collateral = sgr_flags_off(&mut buf, StyleFlags::HIDDEN, StyleFlags::empty()).unwrap();
1116 assert_eq!(buf, b"\x1b[28m");
1117 assert!(collateral.is_empty());
1118 }
1119
1120 #[test]
1121 fn sgr_flags_off_strikethrough() {
1122 let mut buf = Vec::new();
1123 let collateral =
1124 sgr_flags_off(&mut buf, StyleFlags::STRIKETHROUGH, StyleFlags::empty()).unwrap();
1125 assert_eq!(buf, b"\x1b[29m");
1126 assert!(collateral.is_empty());
1127 }
1128
1129 #[test]
1130 fn sgr_flags_off_multi_no_bold_dim_overlap() {
1131 let mut buf = Vec::new();
1133 let collateral = sgr_flags_off(
1134 &mut buf,
1135 StyleFlags::ITALIC | StyleFlags::UNDERLINE,
1136 StyleFlags::empty(),
1137 )
1138 .unwrap();
1139 assert_eq!(buf, b"\x1b[23m\x1b[24m");
1141 assert!(collateral.is_empty());
1142 }
1143
1144 #[test]
1145 fn sgr_flags_off_bold_and_dim_together() {
1146 let mut buf = Vec::new();
1149 let collateral = sgr_flags_off(
1150 &mut buf,
1151 StyleFlags::BOLD | StyleFlags::DIM,
1152 StyleFlags::empty(),
1153 )
1154 .unwrap();
1155 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1156 assert!(
1157 collateral.is_empty(),
1158 "no collateral when both are disabled"
1159 );
1160 }
1161
1162 #[test]
1163 fn sgr_flags_off_overlap_keep_and_disable_does_not_report_collateral() {
1164 let mut buf = Vec::new();
1166 let collateral = sgr_flags_off(
1167 &mut buf,
1168 StyleFlags::BOLD | StyleFlags::DIM,
1169 StyleFlags::DIM,
1170 )
1171 .unwrap();
1172 assert_eq!(buf, b"\x1b[22m\x1b[22m");
1173 assert!(
1174 collateral.is_empty(),
1175 "DIM is explicitly disabled, so it must not be reported as collateral"
1176 );
1177 }
1178
1179 #[test]
1180 fn sgr_flags_off_bold_dim_with_dim_kept() {
1181 let mut buf = Vec::new();
1183 let collateral = sgr_flags_off(
1184 &mut buf,
1185 StyleFlags::BOLD | StyleFlags::ITALIC,
1186 StyleFlags::DIM,
1187 )
1188 .unwrap();
1189 assert_eq!(
1190 collateral,
1191 StyleFlags::DIM,
1192 "DIM should be collateral damage from BOLD off (code 22)"
1193 );
1194 }
1195
1196 #[test]
1199 fn sgr_codes_for_all_single_flags() {
1200 let cases = [
1201 (StyleFlags::BOLD, 1, 22),
1202 (StyleFlags::DIM, 2, 22),
1203 (StyleFlags::ITALIC, 3, 23),
1204 (StyleFlags::UNDERLINE, 4, 24),
1205 (StyleFlags::BLINK, 5, 25),
1206 (StyleFlags::REVERSE, 7, 27),
1207 (StyleFlags::HIDDEN, 8, 28),
1208 (StyleFlags::STRIKETHROUGH, 9, 29),
1209 ];
1210 for (flag, expected_on, expected_off) in cases {
1211 let codes = sgr_codes_for_flag(flag)
1212 .unwrap_or_else(|| panic!("should return codes for {flag:?}"));
1213 assert_eq!(codes.on, expected_on, "on code for {flag:?}");
1214 assert_eq!(codes.off, expected_off, "off code for {flag:?}");
1215 }
1216 }
1217
1218 #[test]
1219 fn sgr_codes_for_composite_flag_returns_none() {
1220 let composite = StyleFlags::BOLD | StyleFlags::ITALIC;
1221 assert!(
1222 sgr_codes_for_flag(composite).is_none(),
1223 "composite flags should return None"
1224 );
1225 }
1226
1227 #[test]
1228 fn sgr_codes_for_empty_flag_returns_none() {
1229 assert!(
1230 sgr_codes_for_flag(StyleFlags::empty()).is_none(),
1231 "empty flags should return None"
1232 );
1233 }
1234
1235 #[test]
1236 fn sgr_codes_for_flag_matches_flag_table_entries() {
1237 for (flag, expected) in FLAG_TABLE {
1238 let actual = sgr_codes_for_flag(flag).expect("single-bit FLAG_TABLE entry");
1239 assert_eq!(actual.on, expected.on, "{flag:?} on code");
1240 assert_eq!(actual.off, expected.off, "{flag:?} off code");
1241 }
1242 }
1243
1244 #[test]
1247 fn cr_emits_carriage_return() {
1248 assert_eq!(to_bytes(cr), b"\r");
1249 }
1250
1251 #[test]
1252 fn lf_emits_line_feed() {
1253 assert_eq!(to_bytes(lf), b"\n");
1254 }
1255
1256 #[test]
1259 fn sgr_flags_each_single_flag_fast_path() {
1260 let cases: &[(StyleFlags, &[u8])] = &[
1261 (StyleFlags::BOLD, b"\x1b[1m"),
1262 (StyleFlags::DIM, b"\x1b[2m"),
1263 (StyleFlags::ITALIC, b"\x1b[3m"),
1264 (StyleFlags::UNDERLINE, b"\x1b[4m"),
1265 (StyleFlags::BLINK, b"\x1b[5m"),
1266 (StyleFlags::REVERSE, b"\x1b[7m"),
1267 (StyleFlags::STRIKETHROUGH, b"\x1b[9m"),
1268 (StyleFlags::HIDDEN, b"\x1b[8m"),
1269 ];
1270 for &(flag, expected) in cases {
1271 assert_eq!(
1272 to_bytes(|w| sgr_flags(w, flag)),
1273 expected,
1274 "single-flag fast path for {flag:?}"
1275 );
1276 }
1277 }
1278
1279 #[test]
1280 fn sgr_flags_all_eight() {
1281 let all = StyleFlags::BOLD
1282 | StyleFlags::DIM
1283 | StyleFlags::ITALIC
1284 | StyleFlags::UNDERLINE
1285 | StyleFlags::BLINK
1286 | StyleFlags::REVERSE
1287 | StyleFlags::HIDDEN
1288 | StyleFlags::STRIKETHROUGH;
1289 let bytes = to_bytes(|w| sgr_flags(w, all));
1290 assert_eq!(bytes, b"\x1b[1;2;3;4;5;7;8;9m");
1292 }
1293
1294 #[test]
1297 fn sgr_code_single_digit() {
1298 let mut buf = Vec::new();
1300 write_sgr_code(&mut buf, 1).unwrap();
1301 assert_eq!(buf, b"\x1b[1m");
1302 }
1303
1304 #[test]
1305 fn sgr_code_two_digits() {
1306 let mut buf = Vec::new();
1308 write_sgr_code(&mut buf, 22).unwrap();
1309 assert_eq!(buf, b"\x1b[22m");
1310 }
1311
1312 #[test]
1313 fn sgr_code_three_digits() {
1314 let mut buf = Vec::new();
1316 write_sgr_code(&mut buf, 100).unwrap();
1317 assert_eq!(buf, b"\x1b[100m");
1318 }
1319
1320 #[test]
1321 fn sgr_code_max_u8() {
1322 let mut buf = Vec::new();
1324 write_sgr_code(&mut buf, 255).unwrap();
1325 assert_eq!(buf, b"\x1b[255m");
1326 }
1327
1328 #[test]
1329 fn sgr_code_zero() {
1330 let mut buf = Vec::new();
1331 write_sgr_code(&mut buf, 0).unwrap();
1332 assert_eq!(buf, b"\x1b[0m");
1333 }
1334
1335 #[test]
1338 fn sgr_fg_16_boundary_7_to_8() {
1339 assert_eq!(to_bytes(|w| sgr_fg_16(w, 7)), b"\x1b[37m");
1341 assert_eq!(to_bytes(|w| sgr_fg_16(w, 8)), b"\x1b[90m");
1342 }
1343
1344 #[test]
1345 fn sgr_bg_16_boundary_7_to_8() {
1346 assert_eq!(to_bytes(|w| sgr_bg_16(w, 7)), b"\x1b[47m");
1347 assert_eq!(to_bytes(|w| sgr_bg_16(w, 8)), b"\x1b[100m");
1348 }
1349
1350 #[test]
1351 fn sgr_fg_16_first_color() {
1352 assert_eq!(to_bytes(|w| sgr_fg_16(w, 0)), b"\x1b[30m"); }
1354
1355 #[test]
1356 fn sgr_bg_16_last_bright() {
1357 assert_eq!(to_bytes(|w| sgr_bg_16(w, 15)), b"\x1b[107m"); }
1359
1360 #[test]
1363 fn sgr_fg_256_zero() {
1364 assert_eq!(to_bytes(|w| sgr_fg_256(w, 0)), b"\x1b[38;5;0m");
1365 }
1366
1367 #[test]
1368 fn sgr_fg_256_max() {
1369 assert_eq!(to_bytes(|w| sgr_fg_256(w, 255)), b"\x1b[38;5;255m");
1370 }
1371
1372 #[test]
1373 fn sgr_bg_256_zero() {
1374 assert_eq!(to_bytes(|w| sgr_bg_256(w, 0)), b"\x1b[48;5;0m");
1375 }
1376
1377 #[test]
1378 fn sgr_bg_256_max() {
1379 assert_eq!(to_bytes(|w| sgr_bg_256(w, 255)), b"\x1b[48;5;255m");
1380 }
1381
1382 #[test]
1385 fn cup_max_u16() {
1386 let bytes = to_bytes(|w| cup(w, u16::MAX, u16::MAX));
1388 let s = String::from_utf8(bytes).unwrap();
1389 assert!(s.starts_with("\x1b["));
1390 assert!(s.ends_with("H"));
1391 }
1392
1393 #[test]
1394 fn cha_max_u16() {
1395 let bytes = to_bytes(|w| cha(w, u16::MAX));
1396 let s = String::from_utf8(bytes).unwrap();
1397 assert!(s.starts_with("\x1b["));
1398 assert!(s.ends_with("G"));
1399 }
1400
1401 #[test]
1402 fn cursor_up_max() {
1403 let bytes = to_bytes(|w| cuu(w, u16::MAX));
1404 let s = String::from_utf8(bytes).unwrap();
1405 assert!(s.contains("65535"));
1406 assert!(s.ends_with("A"));
1407 }
1408
1409 #[test]
1412 fn scroll_region_same_top_bottom() {
1413 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 5)), b"\x1b[6;6r");
1414 }
1415
1416 #[test]
1419 fn sgr_flags_off_each_single_flag_fast_path() {
1420 let cases: &[(StyleFlags, &[u8])] = &[
1421 (StyleFlags::BOLD, b"\x1b[22m"),
1422 (StyleFlags::DIM, b"\x1b[22m"),
1423 (StyleFlags::ITALIC, b"\x1b[23m"),
1424 (StyleFlags::UNDERLINE, b"\x1b[24m"),
1425 (StyleFlags::BLINK, b"\x1b[25m"),
1426 (StyleFlags::REVERSE, b"\x1b[27m"),
1427 (StyleFlags::STRIKETHROUGH, b"\x1b[29m"),
1428 (StyleFlags::HIDDEN, b"\x1b[28m"),
1429 ];
1430 for &(flag, expected) in cases {
1431 let mut buf = Vec::new();
1432 let collateral = sgr_flags_off(&mut buf, flag, StyleFlags::empty()).unwrap();
1433 assert_eq!(buf, expected, "off sequence for {flag:?}");
1434 assert!(collateral.is_empty(), "no collateral for {flag:?}");
1435 }
1436 }
1437
1438 #[test]
1441 fn sgr_bg_packed_opaque() {
1442 let color = PackedRgba::rgb(100, 200, 50);
1443 assert_eq!(
1444 to_bytes(|w| sgr_bg_packed(w, color)),
1445 b"\x1b[48;2;100;200;50m"
1446 );
1447 }
1448
1449 #[test]
1452 fn hyperlink_empty_url() {
1453 assert_eq!(to_bytes(|w| hyperlink_start(w, "")), b"\x1b]8;;\x1b\\");
1454 }
1455
1456 #[test]
1457 fn hyperlink_with_empty_id() {
1458 assert_eq!(
1459 to_bytes(|w| hyperlink_start_with_id(w, "", "https://x.com")),
1460 b"\x1b]8;id=;https://x.com\x1b\\"
1461 );
1462 }
1463
1464 #[test]
1467 fn all_dynamic_sequences_start_with_esc() {
1468 let sequences: Vec<Vec<u8>> = vec![
1469 to_bytes(sgr_reset),
1470 to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)),
1471 to_bytes(|w| sgr_fg_rgb(w, 1, 2, 3)),
1472 to_bytes(|w| sgr_bg_rgb(w, 1, 2, 3)),
1473 to_bytes(|w| sgr_fg_256(w, 42)),
1474 to_bytes(|w| sgr_bg_256(w, 42)),
1475 to_bytes(|w| sgr_fg_16(w, 5)),
1476 to_bytes(|w| sgr_bg_16(w, 5)),
1477 to_bytes(sgr_fg_default),
1478 to_bytes(sgr_bg_default),
1479 to_bytes(|w| cup(w, 0, 0)),
1480 to_bytes(|w| cha(w, 0)),
1481 to_bytes(|w| cuu(w, 1)),
1482 to_bytes(|w| cud(w, 1)),
1483 to_bytes(|w| cuf(w, 1)),
1484 to_bytes(|w| cub(w, 1)),
1485 to_bytes(cursor_save),
1486 to_bytes(cursor_restore),
1487 to_bytes(cursor_hide),
1488 to_bytes(cursor_show),
1489 to_bytes(|w| erase_line(w, EraseLineMode::All)),
1490 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
1491 to_bytes(|w| set_scroll_region(w, 0, 23)),
1492 to_bytes(reset_scroll_region),
1493 to_bytes(sync_begin),
1494 to_bytes(sync_end),
1495 to_bytes(|w| hyperlink_start(w, "test")),
1496 to_bytes(hyperlink_end),
1497 to_bytes(alt_screen_enter),
1498 to_bytes(alt_screen_leave),
1499 to_bytes(bracketed_paste_enable),
1500 to_bytes(bracketed_paste_disable),
1501 to_bytes(mouse_enable),
1502 to_bytes(mouse_disable),
1503 to_bytes(focus_enable),
1504 to_bytes(focus_disable),
1505 ];
1506 for (i, seq) in sequences.iter().enumerate() {
1507 assert!(
1508 seq.starts_with(b"\x1b"),
1509 "sequence {i} should start with ESC, got {seq:?}"
1510 );
1511 }
1512 }
1513}