extui/
vt.rs

1//! VT escape sequence generation.
2//!
3//! Provides types and constants for generating ANSI/VT escape sequences used to
4//! control terminal behavior: cursor movement, text styling, screen clearing,
5//! and mode switching.
6//!
7//! # BufferWrite Trait
8//!
9//! Types implementing [`BufferWrite`] can be written to a byte buffer and used
10//! with the [`splat!`](crate::splat) macro:
11//!
12//! ```
13//! use extui::{splat, vt::{MoveCursor, CLEAR_STYLE, HIDE_CURSOR}};
14//!
15//! let mut buf = Vec::new();
16//! splat!(&mut buf, HIDE_CURSOR, MoveCursor(0, 0), CLEAR_STYLE);
17//! ```
18//!
19//! # Cursor Movement
20//!
21//! - [`MoveCursor`] - Move to absolute position
22//! - [`MoveCursorRight`] - Move right by N columns
23//! - [`MOVE_CURSOR_TO_ORIGIN`] - Move to top-left corner
24//!
25//! # Screen Control
26//!
27//! - [`ENABLE_ALT_SCREEN`] / [`DISABLE_ALT_SCREEN`] - Alternate screen buffer
28//! - [`HIDE_CURSOR`] / [`SHOW_CURSOR`] - Cursor visibility
29//! - [`CLEAR_BELOW`], [`CLEAR_ABOVE`], [`CLEAR_LINE_TO_RIGHT`] - Erase operations
30//!
31//! # Scrolling
32//!
33//! - [`ScrollBufferUp`] / [`ScrollBufferDown`] - Scroll terminal content
34//! - [`ScrollRegion`] - Define scroll region bounds
35//!
36//! # Styling
37//!
38//! - [`Modifier`] - Text attributes (bold, italic, underline, etc.)
39//! - [`style`] - Generate SGR escape sequences
40//! - [`CLEAR_STYLE`] - Reset all attributes
41
42use crate::{Style, StyleDelta, event::KeyboardEnhancementFlags};
43
44// # Safety Rationale for Unsafe Blocks
45//
46// All unsafe blocks in this module follow a common pattern for performance-critical
47// VT escape sequence generation:
48// 1. Reserve sufficient capacity before any writes
49// 2. Write bytes directly via raw pointer arithmetic
50// 3. Set the Vec length to include exactly the bytes written
51//
52// Correctness is verified by miri tests (cargo +nightly miri test --lib tui).
53
54/// Writes VT escape sequences to a byte buffer.
55///
56/// Implement this trait to enable use with the [`splat!`](crate::splat) macro.
57pub trait BufferWrite {
58    /// Appends the VT sequence bytes to the buffer.
59    fn write_to_buffer(&self, buffer: &mut Vec<u8>);
60}
61
62impl BufferWrite for [u8] {
63    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
64        buffer.extend_from_slice(self);
65    }
66}
67
68impl BufferWrite for str {
69    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
70        buffer.extend_from_slice(self.as_bytes());
71    }
72}
73
74impl<T: itoap::Integer + Copy> BufferWrite for T {
75    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
76        itoap::write_to_vec(buffer, *self);
77    }
78}
79
80#[rustfmt::skip]
81static LOOKUP: [u32;256] = [
82    16777264,16777265,16777266,16777267,16777268,16777269,16777270,16777271,16777272,16777273,
83    33566769,33567025,33567281,33567537,33567793,33568049,33568305,33568561,33568817,33569073,
84    33566770,33567026,33567282,33567538,33567794,33568050,33568306,33568562,33568818,33569074,
85    33566771,33567027,33567283,33567539,33567795,33568051,33568307,33568563,33568819,33569075,
86    33566772,33567028,33567284,33567540,33567796,33568052,33568308,33568564,33568820,33569076,
87    33566773,33567029,33567285,33567541,33567797,33568053,33568309,33568565,33568821,33569077,
88    33566774,33567030,33567286,33567542,33567798,33568054,33568310,33568566,33568822,33569078,
89    33566775,33567031,33567287,33567543,33567799,33568055,33568311,33568567,33568823,33569079,
90    33566776,33567032,33567288,33567544,33567800,33568056,33568312,33568568,33568824,33569080,
91    33566777,33567033,33567289,33567545,33567801,33568057,33568313,33568569,33568825,33569081,
92    53489713,53555249,53620785,53686321,53751857,53817393,53882929,53948465,54014001,54079537,
93    53489969,53555505,53621041,53686577,53752113,53817649,53883185,53948721,54014257,54079793,
94    53490225,53555761,53621297,53686833,53752369,53817905,53883441,53948977,54014513,54080049,
95    53490481,53556017,53621553,53687089,53752625,53818161,53883697,53949233,54014769,54080305,
96    53490737,53556273,53621809,53687345,53752881,53818417,53883953,53949489,54015025,54080561,
97    53490993,53556529,53622065,53687601,53753137,53818673,53884209,53949745,54015281,54080817,
98    53491249,53556785,53622321,53687857,53753393,53818929,53884465,53950001,54015537,54081073,
99    53491505,53557041,53622577,53688113,53753649,53819185,53884721,53950257,54015793,54081329,
100    53491761,53557297,53622833,53688369,53753905,53819441,53884977,53950513,54016049,54081585,
101    53492017,53557553,53623089,53688625,53754161,53819697,53885233,53950769,54016305,54081841,
102    53489714,53555250,53620786,53686322,53751858,53817394,53882930,53948466,54014002,54079538,
103    53489970,53555506,53621042,53686578,53752114,53817650,53883186,53948722,54014258,54079794,
104    53490226,53555762,53621298,53686834,53752370,53817906,53883442,53948978,54014514,54080050,
105    53490482,53556018,53621554,53687090,53752626,53818162,53883698,53949234,54014770,54080306,
106    53490738,53556274,53621810,53687346,53752882,53818418,53883954,53949490,54015026,54080562,
107    53490994,53556530,53622066,53687602,53753138,53818674
108];
109
110/// Writes a style escape sequence to the buffer.
111///
112/// If `clear` is true, prepends `0;` to reset attributes first.
113pub fn style(out: &mut Vec<u8>, style: Style, clear: bool) {
114    if style == Style::DEFAULT {
115        if clear {
116            out.extend_from_slice(b"\x1b[0m");
117        }
118        return;
119    }
120    out.reserve(64);
121    let len = out.len();
122    let ogptr = out.as_mut_ptr();
123    let ptr = out.as_mut_ptr();
124
125    // SAFETY: Reserved 64 bytes. Max output is ~47 bytes.
126    unsafe {
127        let mut ptr = ptr.add(len);
128        *(ptr as *mut [u8; 2]) = [0x1b, b'['];
129        ptr = ptr.add(2);
130        if clear {
131            *(ptr as *mut [u8; 2]) = [b'0', b';'];
132            ptr = ptr.add(2);
133        }
134        let mods = style.modifiers();
135        let mut first = true;
136        if !mods.is_empty() {
137            ptr = write_all_modifiers_inner(ptr, mods);
138            first = false;
139        }
140        if let Some(color) = style.fg() {
141            if !first {
142                *(ptr as *mut u8) = b';';
143                ptr = ptr.add(1);
144            } else {
145                first = false;
146            }
147            *(ptr as *mut [u8; 8]) = *b"38;5;000";
148            ptr = ptr.add(5);
149            let mask = LOOKUP[color.0 as usize];
150            let numlen = (mask >> 24) as usize;
151            *(ptr as *mut [u8; 4]) = mask.to_ne_bytes();
152            ptr = ptr.add(numlen);
153        }
154        if let Some(color) = style.bg() {
155            if !first {
156                *(ptr as *mut u8) = b';';
157                ptr = ptr.add(1);
158            }
159            *(ptr as *mut [u8; 8]) = *b"48;5;000";
160            ptr = ptr.add(5);
161            let mask = LOOKUP[color.0 as usize];
162            let numlen = (mask >> 24) as usize;
163            *(ptr as *mut [u8; 4]) = mask.to_ne_bytes();
164            ptr = ptr.add(numlen);
165        }
166        *ptr = b'm';
167        let new_len = ptr.offset_from(ogptr) as usize + 1;
168        out.set_len(new_len);
169    }
170}
171
172/// Move the cursor to a specific position.
173///
174/// Coordinates are 0-indexed (column, row). The VT sequence uses 1-indexed
175/// positions, so 1 is added internally. Uses `ESC[row;colH`.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct MoveCursor(pub u16, pub u16);
178
179impl BufferWrite for MoveCursor {
180    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
181        let (x, y) = (self.0, self.1);
182        buffer.reserve(2 + 5 + 1 + 5 + 1);
183        let len = buffer.len();
184        let optr = buffer.as_mut_ptr();
185        // SAFETY: Reserved 14 bytes (ESC[ + row + ; + col + H).
186        unsafe {
187            let mut ptr = optr.add(len);
188            *(ptr as *mut [u8; 2]) = [0x1b, b'['];
189            ptr = ptr.add(2);
190            let offset = itoap::write_to_ptr(ptr, y.saturating_add(1));
191            ptr = ptr.add(offset);
192            *ptr = b';';
193            ptr = ptr.add(1);
194            let offset = itoap::write_to_ptr(ptr, x.saturating_add(1));
195            ptr = ptr.add(offset);
196            *ptr = b'H';
197            let new_len = ptr.offset_from(optr) as usize + 1;
198            buffer.set_len(new_len);
199        }
200    }
201}
202
203/// Scroll the terminal buffer down by `n` lines.
204///
205/// Inserts `n` blank lines at the top of the scroll region, moving existing
206/// content down. Uses the VT sequence `ESC[nT`.
207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
208pub struct ScrollBufferDown(pub u16);
209
210impl BufferWrite for ScrollBufferDown {
211    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
212        buffer.reserve(2 + 5 + 1);
213        let len = buffer.len();
214        let optr = buffer.as_mut_ptr();
215        // SAFETY: Reserved 8 bytes (ESC[ + n + T).
216        unsafe {
217            let mut ptr = optr.add(len);
218            *(ptr as *mut [u8; 2]) = [0x1b, b'['];
219            ptr = ptr.add(2);
220            let offset = itoap::write_to_ptr(ptr, self.0);
221            ptr = ptr.add(offset);
222            *ptr = b'T';
223            let new_len = ptr.offset_from(optr) as usize + 1;
224            buffer.set_len(new_len);
225        }
226    }
227}
228
229/// Scroll the terminal buffer up by `n` lines.
230///
231/// Removes `n` lines from the top of the scroll region and adds `n` blank
232/// lines at the bottom. Uses the VT sequence `ESC[nS`.
233#[derive(Clone, Copy, Debug, PartialEq, Eq)]
234pub struct ScrollBufferUp(pub u16);
235
236impl BufferWrite for ScrollBufferUp {
237    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
238        buffer.reserve(2 + 5 + 1);
239        let len = buffer.len();
240        let optr = buffer.as_mut_ptr();
241        // SAFETY: Reserved 8 bytes (ESC[ + n + S).
242        unsafe {
243            let mut ptr = optr.add(len);
244            *(ptr as *mut [u8; 2]) = [0x1b, b'['];
245            ptr = ptr.add(2);
246            let offset = itoap::write_to_ptr(ptr, self.0);
247            ptr = ptr.add(offset);
248            *ptr = b'S';
249            let new_len = ptr.offset_from(optr) as usize + 1;
250            buffer.set_len(new_len);
251        }
252    }
253}
254
255/// Move the cursor right by `n` columns.
256///
257/// Uses the VT sequence `ESC[nC`.
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub struct MoveCursorRight(pub u16);
260
261impl BufferWrite for MoveCursorRight {
262    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
263        buffer.reserve(2 + 5 + 1);
264        let len = buffer.len();
265        let optr = buffer.as_mut_ptr();
266        // SAFETY: Reserved 8 bytes (ESC[ + n + C).
267        unsafe {
268            let mut ptr = optr.add(len);
269            *(ptr as *mut [u8; 2]) = [0x1b, b'['];
270            ptr = ptr.add(2);
271            let offset = itoap::write_to_ptr(ptr, self.0);
272            ptr = ptr.add(offset);
273            *ptr = b'C';
274            let new_len = ptr.offset_from(optr) as usize + 1;
275            buffer.set_len(new_len);
276        }
277    }
278}
279
280/// Move the cursor to the top-left corner. VT sequence `ESC[H`.
281pub const MOVE_CURSOR_TO_ORIGIN: &[u8] = b"\x1b[H";
282
283/// Reset all text attributes to default. VT sequence `ESC[0m`.
284pub const CLEAR_STYLE: &[u8] = b"\x1b[0m";
285/// Erase the current line. VT sequence `ESC[0m`.
286pub const ERASE_LINE: &[u8] = b"\x1b[0m";
287/// Clear from cursor to end of screen. VT sequence `ESC[J`.
288pub const CLEAR_BELOW: &[u8] = b"\x1b[J";
289/// Clear from cursor to beginning of screen. VT sequence `ESC[1J`.
290pub const CLEAR_ABOVE: &[u8] = b"\x1b[1J";
291/// Clear from cursor to end of line. VT sequence `ESC[K`.
292pub const CLEAR_LINE_TO_RIGHT: &[u8] = b"\x1b[K";
293
294/// Text styling modifiers such as bold, italic, and underline.
295///
296/// Combine modifiers using bitwise OR. Use [`Style::with_modifier`](crate::Style::with_modifier)
297/// to apply modifiers to a style.
298#[derive(Clone, Copy)]
299pub struct Modifier(pub(crate) u16);
300
301impl std::fmt::Debug for Modifier {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        let values: &[(&str, Modifier)] = &[
304            ("BOLD", Modifier::BOLD),
305            ("DIM", Modifier::DIM),
306            ("ITALIC", Modifier::ITALIC),
307            ("UNDERLINED", Modifier::UNDERLINED),
308            ("SLOW_BLINK", Modifier::SLOW_BLINK),
309            ("RAPID_BLINK", Modifier::RAPID_BLINK),
310            ("REVERSED", Modifier::REVERSED),
311            ("HIDDEN", Modifier::HIDDEN),
312            ("CROSSED_OUT", Modifier::CROSSED_OUT),
313        ];
314        let mut first = true;
315        f.debug_set()
316            .entries(values.iter().filter_map(|(name, val)| {
317                if self.has(*val) {
318                    if first {
319                        first = false;
320                    }
321                    Some(*name)
322                } else {
323                    None
324                }
325            }))
326            .finish()
327    }
328}
329impl Modifier {
330    /// Bold text.
331    pub const BOLD: Modifier = Modifier(0b0000_0000_0001_00_000);
332    /// Dim/faint text.
333    pub const DIM: Modifier = Modifier(0b0000_0000_0010_00_000);
334    /// Italic text.
335    pub const ITALIC: Modifier = Modifier(0b0000_0000_0100_00_000);
336    /// Underlined text.
337    pub const UNDERLINED: Modifier = Modifier(0b0000_0000_1000_00_000);
338    /// Slow blinking text.
339    pub const SLOW_BLINK: Modifier = Modifier(0b0000_0001_0000_00_000);
340    /// Rapid blinking text.
341    pub const RAPID_BLINK: Modifier = Modifier(0b0000_0010_0000_00_000);
342    /// Reversed foreground and background colors.
343    pub const REVERSED: Modifier = Modifier(0b0000_0100_0000_00_000);
344    /// Hidden/invisible text.
345    pub const HIDDEN: Modifier = Modifier(0b0000_1000_0000_00_000);
346    /// Crossed-out/strikethrough text.
347    pub const CROSSED_OUT: Modifier = Modifier(0b0001_0000_0000_00_000);
348    /// All modifiers combined.
349    pub const ALL: Modifier = Modifier(0b0001_1111_1111_00_000);
350
351    /// Returns `true` if no modifiers are set.
352    pub fn is_empty(self) -> bool {
353        self.0 == 0
354    }
355}
356
357impl From<Modifier> for Style {
358    fn from(value: Modifier) -> Self {
359        Style(value.0 as u32)
360    }
361}
362impl Modifier {
363    /// Returns `true` if this modifier set contains the given modifier.
364    pub fn has(self, other: Modifier) -> bool {
365        (self.0 & other.0) != 0
366    }
367}
368
369/// Writes modifier codes (1-9) with semicolon separators, returns pointer before trailing semicolon.
370///
371/// # Safety
372/// Caller must ensure `ptr` has at least 18 bytes of writable space (9 modifiers * 2 bytes).
373unsafe fn write_all_modifiers_inner(mut ptr: *mut u8, modifiers: Modifier) -> *mut u8 {
374    unsafe {
375        if modifiers.has(Modifier::BOLD) {
376            *(ptr as *mut [u8; 2]) = [b'1', b';'];
377            ptr = ptr.add(2);
378        }
379        if modifiers.has(Modifier::DIM) {
380            *(ptr as *mut [u8; 2]) = [b'2', b';'];
381            ptr = ptr.add(2);
382        }
383        if modifiers.has(Modifier::ITALIC) {
384            *(ptr as *mut [u8; 2]) = [b'3', b';'];
385            ptr = ptr.add(2);
386        }
387        if modifiers.has(Modifier::UNDERLINED) {
388            *(ptr as *mut [u8; 2]) = [b'4', b';'];
389            ptr = ptr.add(2);
390        }
391        if modifiers.has(Modifier::SLOW_BLINK) {
392            *(ptr as *mut [u8; 2]) = [b'5', b';'];
393            ptr = ptr.add(2);
394        }
395        if modifiers.has(Modifier::RAPID_BLINK) {
396            *(ptr as *mut [u8; 2]) = [b'6', b';'];
397            ptr = ptr.add(2);
398        }
399        if modifiers.has(Modifier::REVERSED) {
400            *(ptr as *mut [u8; 2]) = [b'7', b';'];
401            ptr = ptr.add(2);
402        }
403        if modifiers.has(Modifier::HIDDEN) {
404            *(ptr as *mut [u8; 2]) = [b'8', b';'];
405            ptr = ptr.add(2);
406        }
407        if modifiers.has(Modifier::CROSSED_OUT) {
408            *(ptr as *mut [u8; 2]) = [b'9', b';'];
409            ptr = ptr.add(2);
410        }
411        ptr.sub(1)
412    }
413}
414
415/// Enables mouse event reporting (button press/release and drag).
416pub const ENABLE_NON_MOTION_MOUSE_EVENTS: &[u8] = b"\x1b[?1000h\x1B[?1002h\x1b[?1015h\x1b[?1006h";
417/// Disables mouse event reporting.
418pub const DISABLE_NON_MOTION_MOUSE_EVENTS: &[u8] = b"\x1b[?1006l\x1b[?1015l\x1B[?1002l\x1b[?1000l";
419
420/// Hides the terminal cursor.
421pub const HIDE_CURSOR: &[u8] = b"\x1b[?25l";
422/// Shows the terminal cursor.
423pub const SHOW_CURSOR: &[u8] = b"\x1b[?25h";
424
425/// Switches to the alternate screen buffer.
426pub const ENABLE_ALT_SCREEN: &[u8] = b"\x1b[?1049h";
427/// Returns to the primary screen buffer.
428pub const DISABLE_ALT_SCREEN: &[u8] = b"\x1b[?1049l";
429/// Pops the keyboard enhancement mode stack.
430pub const POP_KEYBOARD_ENABLEMENT: &[u8] = b"\x1b[<1u";
431
432impl BufferWrite for KeyboardEnhancementFlags {
433    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
434        buffer.extend_from_slice(b"\x1b[>4;");
435        itoap::write_to_vec(buffer, self.bits());
436        buffer.extend_from_slice(b"m");
437    }
438}
439
440impl BufferWrite for Style {
441    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
442        style(buffer, *self, false);
443    }
444}
445
446/// Defines a vertical scroll region within the terminal.
447///
448/// Scroll operations only affect lines within this region.
449#[derive(Clone, Copy, PartialEq, Eq)]
450pub struct ScrollRegion(pub u16, pub u16);
451
452impl ScrollRegion {
453    /// Resets the scroll region to the full terminal height.
454    pub const RESET: ScrollRegion = ScrollRegion(0, 0);
455}
456impl BufferWrite for ScrollRegion {
457    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
458        if *self == ScrollRegion::RESET {
459            buffer.extend_from_slice(b"\x1b[r");
460            return;
461        }
462        buffer.extend_from_slice(b"\x1b[");
463        itoap::write_to_vec(buffer, self.0);
464        if self.1 != 0 {
465            buffer.push(b';');
466            itoap::write_to_vec(buffer, self.1);
467        }
468        buffer.push(b'r');
469    }
470}
471
472impl BufferWrite for StyleDelta {
473    fn write_to_buffer(&self, buffer: &mut Vec<u8>) {
474        if self.current == u32::MAX {
475            style(buffer, self.target, true);
476            return;
477        }
478        if self.current == self.target.0 {
479            return;
480        }
481        let removed = (self.target.0 | self.current) ^ self.target.0;
482        // todo should use added to avoid resetting modifiers
483        // let added = (self.target.0 | self.current) ^ self.current;
484        let clearing = removed & (Style::HAS_BG | Style::HAS_FG | Modifier::ALL.0 as u32) != 0;
485        if clearing {
486            style(buffer, self.target, true);
487            return;
488        }
489        let mut target = self.target;
490        if Style(self.current).fg() == target.fg() {
491            target = target.without_fg();
492        }
493        if Style(self.current).bg() == target.bg() {
494            target = target.without_bg();
495        }
496        style(buffer, target, false);
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    fn run_buffer_write_tests(cases: &[(&dyn BufferWrite, &[u8], &str)]) {
505        for (writer, expected, desc) in cases {
506            // Test from empty buffer (zero allocation)
507            let mut buf = Vec::new();
508            writer.write_to_buffer(&mut buf);
509            assert_eq!(&buf, expected, "{desc} (empty buffer)");
510
511            // Test with pre-existing contents
512            let prefix = b"PREFIX";
513            let mut buf = prefix.to_vec();
514            writer.write_to_buffer(&mut buf);
515            let mut expected_with_prefix = prefix.to_vec();
516            expected_with_prefix.extend_from_slice(expected);
517            assert_eq!(&buf, &expected_with_prefix, "{desc} (prefilled buffer)");
518        }
519    }
520
521    #[test]
522    fn buffer_write_sequences() {
523        run_buffer_write_tests(&[
524            // ScrollBufferUp
525            (&ScrollBufferUp(5), b"\x1b[5S", "ScrollBufferUp(5)"),
526            (&ScrollBufferUp(100), b"\x1b[100S", "ScrollBufferUp(100)"),
527            (&ScrollBufferUp(65535), b"\x1b[65535S", "ScrollBufferUp(65535)"),
528            // ScrollBufferDown
529            (&ScrollBufferDown(3), b"\x1b[3T", "ScrollBufferDown(3)"),
530            (&ScrollBufferDown(1), b"\x1b[1T", "ScrollBufferDown(1)"),
531            // MoveCursor
532            (&MoveCursor(0, 0), b"\x1b[1;1H", "MoveCursor(0, 0)"),
533            (&MoveCursor(10, 20), b"\x1b[21;11H", "MoveCursor(10, 20)"),
534            (&MoveCursor(99, 49), b"\x1b[50;100H", "MoveCursor(99, 49)"),
535            // MoveCursorRight
536            (&MoveCursorRight(1), b"\x1b[1C", "MoveCursorRight(1)"),
537            (&MoveCursorRight(50), b"\x1b[50C", "MoveCursorRight(50)"),
538            // ScrollRegion
539            (&ScrollRegion(1, 24), b"\x1b[1;24r", "ScrollRegion(1, 24)"),
540            (&ScrollRegion::RESET, b"\x1b[r", "ScrollRegion::RESET"),
541            // Style with fg
542            (
543                &Style::DEFAULT.with_fg(crate::Color(196)),
544                b"\x1b[38;5;196m".as_slice(),
545                "Style with fg color 196",
546            ),
547            // Style with modifiers
548            (&Style::DEFAULT.with_modifier(Modifier::BOLD), b"\x1b[1m".as_slice(), "Style with BOLD"),
549            (
550                &Style::DEFAULT.with_modifier(Modifier::BOLD).with_modifier(Modifier::ITALIC),
551                b"\x1b[1;3m".as_slice(),
552                "Style with BOLD and ITALIC",
553            ),
554            // Style default (no output)
555            (&Style::DEFAULT, b"".as_slice(), "Style::DEFAULT (no output)"),
556        ]);
557    }
558}