1#![forbid(unsafe_code)]
2
3use std::io::{self, Write};
28
29use crate::cell::{PackedRgba, StyleFlags};
30
31pub const SGR_RESET: &[u8] = b"\x1b[0m";
37
38#[inline]
40pub fn sgr_reset<W: Write>(w: &mut W) -> io::Result<()> {
41 w.write_all(SGR_RESET)
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct SgrCodes {
47 pub on: u8,
49 pub off: u8,
51}
52
53pub const SGR_BOLD: SgrCodes = SgrCodes { on: 1, off: 22 };
55pub const SGR_DIM: SgrCodes = SgrCodes { on: 2, off: 22 };
57pub const SGR_ITALIC: SgrCodes = SgrCodes { on: 3, off: 23 };
59pub const SGR_UNDERLINE: SgrCodes = SgrCodes { on: 4, off: 24 };
61pub const SGR_BLINK: SgrCodes = SgrCodes { on: 5, off: 25 };
63pub const SGR_REVERSE: SgrCodes = SgrCodes { on: 7, off: 27 };
65pub const SGR_HIDDEN: SgrCodes = SgrCodes { on: 8, off: 28 };
67pub const SGR_STRIKETHROUGH: SgrCodes = SgrCodes { on: 9, off: 29 };
69
70#[must_use]
72pub const fn sgr_codes_for_flag(flag: StyleFlags) -> Option<SgrCodes> {
73 match flag.bits() {
74 0b0000_0001 => Some(SGR_BOLD),
75 0b0000_0010 => Some(SGR_DIM),
76 0b0000_0100 => Some(SGR_ITALIC),
77 0b0000_1000 => Some(SGR_UNDERLINE),
78 0b0001_0000 => Some(SGR_BLINK),
79 0b0010_0000 => Some(SGR_REVERSE),
80 0b1000_0000 => Some(SGR_HIDDEN),
81 0b0100_0000 => Some(SGR_STRIKETHROUGH),
82 _ => None,
83 }
84}
85
86#[inline]
87fn write_u8_dec(buf: &mut [u8], n: u8) -> usize {
88 if n >= 100 {
89 let hundreds = n / 100;
90 let tens = (n / 10) % 10;
91 let ones = n % 10;
92 buf[0] = b'0' + hundreds;
93 buf[1] = b'0' + tens;
94 buf[2] = b'0' + ones;
95 3
96 } else if n >= 10 {
97 let tens = n / 10;
98 let ones = n % 10;
99 buf[0] = b'0' + tens;
100 buf[1] = b'0' + ones;
101 2
102 } else {
103 buf[0] = b'0' + n;
104 1
105 }
106}
107
108#[inline]
109fn write_sgr_code<W: Write>(w: &mut W, code: u8) -> io::Result<()> {
110 let mut buf = [0u8; 6];
111 buf[0] = 0x1b;
112 buf[1] = b'[';
113 let len = write_u8_dec(&mut buf[2..], code);
114 buf[2 + len] = b'm';
115 w.write_all(&buf[..2 + len + 1])
116}
117
118pub fn sgr_flags<W: Write>(w: &mut W, flags: StyleFlags) -> io::Result<()> {
123 if flags.is_empty() {
124 return Ok(());
125 }
126
127 let bits = flags.bits();
128 if bits.is_power_of_two()
129 && let Some(seq) = sgr_single_flag_seq(bits)
130 {
131 return w.write_all(seq);
132 }
133
134 let mut buf = [0u8; 32];
135 let mut idx = 0usize;
136 buf[idx] = 0x1b;
137 buf[idx + 1] = b'[';
138 idx += 2;
139 let mut first = true;
140
141 for (flag, codes) in FLAG_TABLE {
142 if flags.contains(flag) {
143 if !first {
144 buf[idx] = b';';
145 idx += 1;
146 }
147 idx += write_u8_dec(&mut buf[idx..], codes.on);
148 first = false;
149 }
150 }
151
152 buf[idx] = b'm';
153 idx += 1;
154 w.write_all(&buf[..idx])
155}
156
157pub const FLAG_TABLE: [(StyleFlags, SgrCodes); 8] = [
159 (StyleFlags::BOLD, SGR_BOLD),
160 (StyleFlags::DIM, SGR_DIM),
161 (StyleFlags::ITALIC, SGR_ITALIC),
162 (StyleFlags::UNDERLINE, SGR_UNDERLINE),
163 (StyleFlags::BLINK, SGR_BLINK),
164 (StyleFlags::REVERSE, SGR_REVERSE),
165 (StyleFlags::HIDDEN, SGR_HIDDEN),
166 (StyleFlags::STRIKETHROUGH, SGR_STRIKETHROUGH),
167];
168
169#[inline]
170fn sgr_single_flag_seq(bits: u8) -> Option<&'static [u8]> {
171 match bits {
172 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,
181 }
182}
183
184#[inline]
185fn sgr_single_flag_off_seq(bits: u8) -> Option<&'static [u8]> {
186 match bits {
187 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,
196 }
197}
198
199pub fn sgr_flags_off<W: Write>(
210 w: &mut W,
211 flags_to_disable: StyleFlags,
212 flags_to_keep: StyleFlags,
213) -> io::Result<StyleFlags> {
214 if flags_to_disable.is_empty() {
215 return Ok(StyleFlags::empty());
216 }
217
218 let disable_bits = flags_to_disable.bits();
219 if disable_bits.is_power_of_two()
220 && let Some(seq) = sgr_single_flag_off_seq(disable_bits)
221 {
222 w.write_all(seq)?;
223 if disable_bits == StyleFlags::BOLD.bits() && flags_to_keep.contains(StyleFlags::DIM) {
224 return Ok(StyleFlags::DIM);
225 }
226 if disable_bits == StyleFlags::DIM.bits() && flags_to_keep.contains(StyleFlags::BOLD) {
227 return Ok(StyleFlags::BOLD);
228 }
229 return Ok(StyleFlags::empty());
230 }
231
232 let mut collateral = StyleFlags::empty();
233
234 for (flag, codes) in FLAG_TABLE {
235 if !flags_to_disable.contains(flag) {
236 continue;
237 }
238 write_sgr_code(w, codes.off)?;
240 if codes.off == 22 {
242 let other = if flag == StyleFlags::BOLD {
244 StyleFlags::DIM
245 } else {
246 StyleFlags::BOLD
247 };
248 if flags_to_keep.contains(other) {
249 collateral |= other;
250 }
251 }
252 }
253
254 Ok(collateral)
255}
256
257pub fn sgr_fg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
259 write!(w, "\x1b[38;2;{r};{g};{b}m")
260}
261
262pub fn sgr_bg_rgb<W: Write>(w: &mut W, r: u8, g: u8, b: u8) -> io::Result<()> {
264 write!(w, "\x1b[48;2;{r};{g};{b}m")
265}
266
267pub fn sgr_fg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
269 write!(w, "\x1b[38;5;{index}m")
270}
271
272pub fn sgr_bg_256<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
274 write!(w, "\x1b[48;5;{index}m")
275}
276
277pub fn sgr_fg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
281 let code = if index < 8 {
282 30 + index
283 } else {
284 90 + index - 8
285 };
286 write!(w, "\x1b[{code}m")
287}
288
289pub fn sgr_bg_16<W: Write>(w: &mut W, index: u8) -> io::Result<()> {
293 let code = if index < 8 {
294 40 + index
295 } else {
296 100 + index - 8
297 };
298 write!(w, "\x1b[{code}m")
299}
300
301pub fn sgr_fg_default<W: Write>(w: &mut W) -> io::Result<()> {
303 w.write_all(b"\x1b[39m")
304}
305
306pub fn sgr_bg_default<W: Write>(w: &mut W) -> io::Result<()> {
308 w.write_all(b"\x1b[49m")
309}
310
311pub fn sgr_fg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
315 if color.a() == 0 {
316 return sgr_fg_default(w);
317 }
318 sgr_fg_rgb(w, color.r(), color.g(), color.b())
319}
320
321pub fn sgr_bg_packed<W: Write>(w: &mut W, color: PackedRgba) -> io::Result<()> {
325 if color.a() == 0 {
326 return sgr_bg_default(w);
327 }
328 sgr_bg_rgb(w, color.r(), color.g(), color.b())
329}
330
331pub fn cup<W: Write>(w: &mut W, row: u16, col: u16) -> io::Result<()> {
340 write!(
341 w,
342 "\x1b[{};{}H",
343 row.saturating_add(1),
344 col.saturating_add(1)
345 )
346}
347
348pub fn cha<W: Write>(w: &mut W, col: u16) -> io::Result<()> {
352 write!(w, "\x1b[{}G", col.saturating_add(1))
353}
354
355pub fn cuu<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
357 if n == 0 {
358 return Ok(());
359 }
360 if n == 1 {
361 w.write_all(b"\x1b[A")
362 } else {
363 write!(w, "\x1b[{n}A")
364 }
365}
366
367pub fn cud<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
369 if n == 0 {
370 return Ok(());
371 }
372 if n == 1 {
373 w.write_all(b"\x1b[B")
374 } else {
375 write!(w, "\x1b[{n}B")
376 }
377}
378
379pub fn cuf<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
381 if n == 0 {
382 return Ok(());
383 }
384 if n == 1 {
385 w.write_all(b"\x1b[C")
386 } else {
387 write!(w, "\x1b[{n}C")
388 }
389}
390
391pub fn cub<W: Write>(w: &mut W, n: u16) -> io::Result<()> {
393 if n == 0 {
394 return Ok(());
395 }
396 if n == 1 {
397 w.write_all(b"\x1b[D")
398 } else {
399 write!(w, "\x1b[{n}D")
400 }
401}
402
403#[inline]
405pub fn cr<W: Write>(w: &mut W) -> io::Result<()> {
406 w.write_all(b"\r")
407}
408
409#[inline]
413pub fn lf<W: Write>(w: &mut W) -> io::Result<()> {
414 w.write_all(b"\n")
415}
416
417pub const CURSOR_SAVE: &[u8] = b"\x1b7";
419
420pub const CURSOR_RESTORE: &[u8] = b"\x1b8";
422
423#[inline]
425pub fn cursor_save<W: Write>(w: &mut W) -> io::Result<()> {
426 w.write_all(CURSOR_SAVE)
427}
428
429#[inline]
431pub fn cursor_restore<W: Write>(w: &mut W) -> io::Result<()> {
432 w.write_all(CURSOR_RESTORE)
433}
434
435pub const CURSOR_HIDE: &[u8] = b"\x1b[?25l";
437
438pub const CURSOR_SHOW: &[u8] = b"\x1b[?25h";
440
441#[inline]
443pub fn cursor_hide<W: Write>(w: &mut W) -> io::Result<()> {
444 w.write_all(CURSOR_HIDE)
445}
446
447#[inline]
449pub fn cursor_show<W: Write>(w: &mut W) -> io::Result<()> {
450 w.write_all(CURSOR_SHOW)
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459pub enum EraseLineMode {
460 ToEnd = 0,
462 ToStart = 1,
464 All = 2,
466}
467
468pub fn erase_line<W: Write>(w: &mut W, mode: EraseLineMode) -> io::Result<()> {
470 match mode {
471 EraseLineMode::ToEnd => w.write_all(b"\x1b[K"),
472 EraseLineMode::ToStart => w.write_all(b"\x1b[1K"),
473 EraseLineMode::All => w.write_all(b"\x1b[2K"),
474 }
475}
476
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
479pub enum EraseDisplayMode {
480 ToEnd = 0,
482 ToStart = 1,
484 All = 2,
486 Scrollback = 3,
488}
489
490pub fn erase_display<W: Write>(w: &mut W, mode: EraseDisplayMode) -> io::Result<()> {
492 match mode {
493 EraseDisplayMode::ToEnd => w.write_all(b"\x1b[J"),
494 EraseDisplayMode::ToStart => w.write_all(b"\x1b[1J"),
495 EraseDisplayMode::All => w.write_all(b"\x1b[2J"),
496 EraseDisplayMode::Scrollback => w.write_all(b"\x1b[3J"),
497 }
498}
499
500pub fn set_scroll_region<W: Write>(w: &mut W, top: u16, bottom: u16) -> io::Result<()> {
508 write!(
509 w,
510 "\x1b[{};{}r",
511 top.saturating_add(1),
512 bottom.saturating_add(1)
513 )
514}
515
516pub const RESET_SCROLL_REGION: &[u8] = b"\x1b[r";
518
519#[inline]
521pub fn reset_scroll_region<W: Write>(w: &mut W) -> io::Result<()> {
522 w.write_all(RESET_SCROLL_REGION)
523}
524
525pub const SYNC_BEGIN: &[u8] = b"\x1b[?2026h";
531
532pub const SYNC_END: &[u8] = b"\x1b[?2026l";
534
535#[inline]
537pub fn sync_begin<W: Write>(w: &mut W) -> io::Result<()> {
538 w.write_all(SYNC_BEGIN)
539}
540
541#[inline]
543pub fn sync_end<W: Write>(w: &mut W) -> io::Result<()> {
544 w.write_all(SYNC_END)
545}
546
547pub fn hyperlink_start<W: Write>(w: &mut W, url: &str) -> io::Result<()> {
556 write!(w, "\x1b]8;;{url}\x1b\\")
557}
558
559pub fn hyperlink_end<W: Write>(w: &mut W) -> io::Result<()> {
563 w.write_all(b"\x1b]8;;\x1b\\")
564}
565
566pub fn hyperlink_start_with_id<W: Write>(w: &mut W, id: &str, url: &str) -> io::Result<()> {
571 write!(w, "\x1b]8;id={id};{url}\x1b\\")
572}
573
574pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
580
581pub const ALT_SCREEN_LEAVE: &[u8] = b"\x1b[?1049l";
583
584pub const BRACKETED_PASTE_ENABLE: &[u8] = b"\x1b[?2004h";
586
587pub const BRACKETED_PASTE_DISABLE: &[u8] = b"\x1b[?2004l";
589
590pub const MOUSE_ENABLE: &[u8] = b"\x1b[?1000;1002;1006h";
597
598pub const MOUSE_DISABLE: &[u8] = b"\x1b[?1000;1002;1006l";
600
601pub const FOCUS_ENABLE: &[u8] = b"\x1b[?1004h";
603
604pub const FOCUS_DISABLE: &[u8] = b"\x1b[?1004l";
606
607#[inline]
609pub fn alt_screen_enter<W: Write>(w: &mut W) -> io::Result<()> {
610 w.write_all(ALT_SCREEN_ENTER)
611}
612
613#[inline]
615pub fn alt_screen_leave<W: Write>(w: &mut W) -> io::Result<()> {
616 w.write_all(ALT_SCREEN_LEAVE)
617}
618
619#[inline]
621pub fn bracketed_paste_enable<W: Write>(w: &mut W) -> io::Result<()> {
622 w.write_all(BRACKETED_PASTE_ENABLE)
623}
624
625#[inline]
627pub fn bracketed_paste_disable<W: Write>(w: &mut W) -> io::Result<()> {
628 w.write_all(BRACKETED_PASTE_DISABLE)
629}
630
631#[inline]
633pub fn mouse_enable<W: Write>(w: &mut W) -> io::Result<()> {
634 w.write_all(MOUSE_ENABLE)
635}
636
637#[inline]
639pub fn mouse_disable<W: Write>(w: &mut W) -> io::Result<()> {
640 w.write_all(MOUSE_DISABLE)
641}
642
643#[inline]
645pub fn focus_enable<W: Write>(w: &mut W) -> io::Result<()> {
646 w.write_all(FOCUS_ENABLE)
647}
648
649#[inline]
651pub fn focus_disable<W: Write>(w: &mut W) -> io::Result<()> {
652 w.write_all(FOCUS_DISABLE)
653}
654
655#[cfg(test)]
660mod tests {
661 use super::*;
662
663 fn to_bytes<F: FnOnce(&mut Vec<u8>) -> io::Result<()>>(f: F) -> Vec<u8> {
664 let mut buf = Vec::new();
665 f(&mut buf).unwrap();
666 buf
667 }
668
669 #[test]
672 fn sgr_reset_bytes() {
673 assert_eq!(to_bytes(sgr_reset), b"\x1b[0m");
674 }
675
676 #[test]
677 fn sgr_flags_bold() {
678 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::BOLD)), b"\x1b[1m");
679 }
680
681 #[test]
682 fn sgr_flags_multiple() {
683 let flags = StyleFlags::BOLD | StyleFlags::ITALIC | StyleFlags::UNDERLINE;
684 assert_eq!(to_bytes(|w| sgr_flags(w, flags)), b"\x1b[1;3;4m");
685 }
686
687 #[test]
688 fn sgr_flags_empty() {
689 assert_eq!(to_bytes(|w| sgr_flags(w, StyleFlags::empty())), b"");
690 }
691
692 #[test]
693 fn sgr_fg_rgb_bytes() {
694 assert_eq!(
695 to_bytes(|w| sgr_fg_rgb(w, 255, 128, 0)),
696 b"\x1b[38;2;255;128;0m"
697 );
698 }
699
700 #[test]
701 fn sgr_bg_rgb_bytes() {
702 assert_eq!(to_bytes(|w| sgr_bg_rgb(w, 0, 0, 0)), b"\x1b[48;2;0;0;0m");
703 }
704
705 #[test]
706 fn sgr_fg_256_bytes() {
707 assert_eq!(to_bytes(|w| sgr_fg_256(w, 196)), b"\x1b[38;5;196m");
708 }
709
710 #[test]
711 fn sgr_bg_256_bytes() {
712 assert_eq!(to_bytes(|w| sgr_bg_256(w, 232)), b"\x1b[48;5;232m");
713 }
714
715 #[test]
716 fn sgr_fg_16_normal() {
717 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"); }
720
721 #[test]
722 fn sgr_fg_16_bright() {
723 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"); }
726
727 #[test]
728 fn sgr_bg_16_normal() {
729 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"); }
732
733 #[test]
734 fn sgr_bg_16_bright() {
735 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"); }
738
739 #[test]
740 fn sgr_default_colors() {
741 assert_eq!(to_bytes(sgr_fg_default), b"\x1b[39m");
742 assert_eq!(to_bytes(sgr_bg_default), b"\x1b[49m");
743 }
744
745 #[test]
746 fn sgr_packed_transparent_uses_default() {
747 assert_eq!(
748 to_bytes(|w| sgr_fg_packed(w, PackedRgba::TRANSPARENT)),
749 b"\x1b[39m"
750 );
751 assert_eq!(
752 to_bytes(|w| sgr_bg_packed(w, PackedRgba::TRANSPARENT)),
753 b"\x1b[49m"
754 );
755 }
756
757 #[test]
758 fn sgr_packed_opaque() {
759 let color = PackedRgba::rgb(10, 20, 30);
760 assert_eq!(
761 to_bytes(|w| sgr_fg_packed(w, color)),
762 b"\x1b[38;2;10;20;30m"
763 );
764 }
765
766 #[test]
769 fn cup_1_indexed() {
770 assert_eq!(to_bytes(|w| cup(w, 0, 0)), b"\x1b[1;1H");
771 assert_eq!(to_bytes(|w| cup(w, 23, 79)), b"\x1b[24;80H");
772 }
773
774 #[test]
775 fn cha_1_indexed() {
776 assert_eq!(to_bytes(|w| cha(w, 0)), b"\x1b[1G");
777 assert_eq!(to_bytes(|w| cha(w, 79)), b"\x1b[80G");
778 }
779
780 #[test]
781 fn cursor_relative_moves() {
782 assert_eq!(to_bytes(|w| cuu(w, 1)), b"\x1b[A");
783 assert_eq!(to_bytes(|w| cuu(w, 5)), b"\x1b[5A");
784 assert_eq!(to_bytes(|w| cud(w, 1)), b"\x1b[B");
785 assert_eq!(to_bytes(|w| cud(w, 3)), b"\x1b[3B");
786 assert_eq!(to_bytes(|w| cuf(w, 1)), b"\x1b[C");
787 assert_eq!(to_bytes(|w| cuf(w, 10)), b"\x1b[10C");
788 assert_eq!(to_bytes(|w| cub(w, 1)), b"\x1b[D");
789 assert_eq!(to_bytes(|w| cub(w, 2)), b"\x1b[2D");
790 }
791
792 #[test]
793 fn cursor_relative_zero_is_noop() {
794 assert_eq!(to_bytes(|w| cuu(w, 0)), b"");
795 assert_eq!(to_bytes(|w| cud(w, 0)), b"");
796 assert_eq!(to_bytes(|w| cuf(w, 0)), b"");
797 assert_eq!(to_bytes(|w| cub(w, 0)), b"");
798 }
799
800 #[test]
801 fn cursor_save_restore() {
802 assert_eq!(to_bytes(cursor_save), b"\x1b7");
803 assert_eq!(to_bytes(cursor_restore), b"\x1b8");
804 }
805
806 #[test]
807 fn cursor_visibility() {
808 assert_eq!(to_bytes(cursor_hide), b"\x1b[?25l");
809 assert_eq!(to_bytes(cursor_show), b"\x1b[?25h");
810 }
811
812 #[test]
815 fn erase_line_modes() {
816 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::ToEnd)), b"\x1b[K");
817 assert_eq!(
818 to_bytes(|w| erase_line(w, EraseLineMode::ToStart)),
819 b"\x1b[1K"
820 );
821 assert_eq!(to_bytes(|w| erase_line(w, EraseLineMode::All)), b"\x1b[2K");
822 }
823
824 #[test]
825 fn erase_display_modes() {
826 assert_eq!(
827 to_bytes(|w| erase_display(w, EraseDisplayMode::ToEnd)),
828 b"\x1b[J"
829 );
830 assert_eq!(
831 to_bytes(|w| erase_display(w, EraseDisplayMode::ToStart)),
832 b"\x1b[1J"
833 );
834 assert_eq!(
835 to_bytes(|w| erase_display(w, EraseDisplayMode::All)),
836 b"\x1b[2J"
837 );
838 assert_eq!(
839 to_bytes(|w| erase_display(w, EraseDisplayMode::Scrollback)),
840 b"\x1b[3J"
841 );
842 }
843
844 #[test]
847 fn scroll_region_1_indexed() {
848 assert_eq!(to_bytes(|w| set_scroll_region(w, 0, 23)), b"\x1b[1;24r");
849 assert_eq!(to_bytes(|w| set_scroll_region(w, 5, 20)), b"\x1b[6;21r");
850 }
851
852 #[test]
853 fn scroll_region_reset() {
854 assert_eq!(to_bytes(reset_scroll_region), b"\x1b[r");
855 }
856
857 #[test]
860 fn sync_output() {
861 assert_eq!(to_bytes(sync_begin), b"\x1b[?2026h");
862 assert_eq!(to_bytes(sync_end), b"\x1b[?2026l");
863 }
864
865 #[test]
868 fn hyperlink_basic() {
869 assert_eq!(
870 to_bytes(|w| hyperlink_start(w, "https://example.com")),
871 b"\x1b]8;;https://example.com\x1b\\"
872 );
873 assert_eq!(to_bytes(hyperlink_end), b"\x1b]8;;\x1b\\");
874 }
875
876 #[test]
877 fn hyperlink_with_id() {
878 assert_eq!(
879 to_bytes(|w| hyperlink_start_with_id(w, "link1", "https://example.com")),
880 b"\x1b]8;id=link1;https://example.com\x1b\\"
881 );
882 }
883
884 #[test]
887 fn alt_screen() {
888 assert_eq!(to_bytes(alt_screen_enter), b"\x1b[?1049h");
889 assert_eq!(to_bytes(alt_screen_leave), b"\x1b[?1049l");
890 }
891
892 #[test]
893 fn bracketed_paste() {
894 assert_eq!(to_bytes(bracketed_paste_enable), b"\x1b[?2004h");
895 assert_eq!(to_bytes(bracketed_paste_disable), b"\x1b[?2004l");
896 }
897
898 #[test]
899 fn mouse_mode() {
900 assert_eq!(to_bytes(mouse_enable), b"\x1b[?1000;1002;1006h");
901 assert_eq!(to_bytes(mouse_disable), b"\x1b[?1000;1002;1006l");
902 }
903
904 #[test]
905 fn focus_mode() {
906 assert_eq!(to_bytes(focus_enable), b"\x1b[?1004h");
907 assert_eq!(to_bytes(focus_disable), b"\x1b[?1004l");
908 }
909
910 #[test]
913 fn all_sequences_are_ascii() {
914 for seq in [
916 SGR_RESET,
917 CURSOR_SAVE,
918 CURSOR_RESTORE,
919 CURSOR_HIDE,
920 CURSOR_SHOW,
921 RESET_SCROLL_REGION,
922 SYNC_BEGIN,
923 SYNC_END,
924 ALT_SCREEN_ENTER,
925 ALT_SCREEN_LEAVE,
926 BRACKETED_PASTE_ENABLE,
927 BRACKETED_PASTE_DISABLE,
928 MOUSE_ENABLE,
929 MOUSE_DISABLE,
930 FOCUS_ENABLE,
931 FOCUS_DISABLE,
932 ] {
933 for &byte in seq {
934 assert!(byte < 128, "Non-ASCII byte {byte:#x} in sequence");
935 }
936 }
937 }
938
939 #[test]
940 fn osc_sequences_are_terminated() {
941 let link_start = to_bytes(|w| hyperlink_start(w, "test"));
943 assert!(
944 link_start.ends_with(b"\x1b\\"),
945 "hyperlink_start not terminated with ST"
946 );
947
948 let link_end = to_bytes(hyperlink_end);
949 assert!(
950 link_end.ends_with(b"\x1b\\"),
951 "hyperlink_end not terminated with ST"
952 );
953
954 let link_id = to_bytes(|w| hyperlink_start_with_id(w, "id", "url"));
955 assert!(
956 link_id.ends_with(b"\x1b\\"),
957 "hyperlink_start_with_id not terminated with ST"
958 );
959 }
960}