1#![expect(
2 clippy::let_underscore_must_use,
3 reason = "ANSI output cleanup intentionally ignores the best-effort flush result."
4)]
5
6use once_cell::sync::Lazy;
11use std::io::{IsTerminal, Write};
12
13pub const ESC_BYTE: u8 = 0x1b;
15
16pub const ESC_CHAR: char = '\x1b';
18
19pub const ESC: &str = "\x1b";
21
22pub const CSI: &str = "\x1b[";
24
25pub const OSC: &str = "\x1b]";
27
28pub const DCS: &str = "\x1bP";
30
31pub const ST: &str = "\x1b\\";
33
34pub(crate) const BEL_BYTE: u8 = 0x07;
36
37pub const BEL_CHAR: char = '\x07';
39
40const BEL: &str = "\x07";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum HitlNotifyMode {
46 Off,
47 Bell,
48 Rich,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum TerminalNotifyKind {
54 BellOnly,
55 Osc9,
56 Osc777,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum NotifyMethodOverride {
62 Auto,
63 Bell,
64 Osc9,
65}
66
67static DETECTED_NOTIFY_KIND: Lazy<TerminalNotifyKind> = Lazy::new(detect_terminal_notify_kind);
68
69#[inline]
71pub fn play_bell(enabled: bool) {
72 if !is_bell_enabled(enabled) {
73 return;
74 }
75 emit_bell();
76}
77
78#[inline]
80fn is_bell_enabled(default_enabled: bool) -> bool {
81 if let Ok(val) = std::env::var("VTCODE_HITL_BELL") {
82 return !matches!(val.trim().to_ascii_lowercase().as_str(), "false" | "0" | "off");
83 }
84 default_enabled
85}
86
87#[inline]
88fn emit_bell() {
89 print!("{BEL}");
90 let _ = std::io::stdout().flush();
91}
92
93#[inline]
94pub fn notify_attention(default_enabled: bool, message: Option<&str>) {
95 notify_attention_with_mode(default_enabled, message, NotifyMethodOverride::Auto);
96}
97
98#[inline]
99pub fn notify_attention_with_mode(default_enabled: bool, message: Option<&str>, method: NotifyMethodOverride) {
100 if !is_bell_enabled(default_enabled) {
101 return;
102 }
103
104 if !std::io::stdout().is_terminal() {
105 return;
106 }
107
108 let mode = hitl_notify_mode(default_enabled);
109 if matches!(mode, HitlNotifyMode::Off) {
110 return;
111 }
112
113 if matches!(mode, HitlNotifyMode::Rich) {
114 let notify_kind = match method {
115 NotifyMethodOverride::Auto => *DETECTED_NOTIFY_KIND,
116 NotifyMethodOverride::Bell => TerminalNotifyKind::BellOnly,
117 NotifyMethodOverride::Osc9 => TerminalNotifyKind::Osc9,
118 };
119 match notify_kind {
120 TerminalNotifyKind::Osc9 => send_osc9_notification(message),
121 TerminalNotifyKind::Osc777 => send_osc777_notification(message),
122 TerminalNotifyKind::BellOnly => {} }
124 }
125
126 emit_bell();
127}
128
129fn hitl_notify_mode(default_enabled: bool) -> HitlNotifyMode {
130 if let Ok(raw) = std::env::var("VTCODE_HITL_NOTIFY") {
131 let v = raw.trim().to_ascii_lowercase();
132 return match v.as_str() {
133 "off" | "0" | "false" => HitlNotifyMode::Off,
134 "bell" => HitlNotifyMode::Bell,
135 "rich" | "osc" | "notify" => HitlNotifyMode::Rich,
136 _ => HitlNotifyMode::Bell,
137 };
138 }
139
140 if default_enabled {
141 HitlNotifyMode::Rich
142 } else {
143 HitlNotifyMode::Off
144 }
145}
146
147fn detect_terminal_notify_kind() -> TerminalNotifyKind {
148 if let Ok(explicit_kind) = std::env::var("VTCODE_NOTIFY_KIND") {
149 let explicit = explicit_kind.trim().to_ascii_lowercase();
150 return match explicit.as_str() {
151 "osc9" => TerminalNotifyKind::Osc9,
152 "osc777" => TerminalNotifyKind::Osc777,
153 "bell" | "off" => TerminalNotifyKind::BellOnly,
154 _ => TerminalNotifyKind::BellOnly,
155 };
156 }
157
158 let term = std::env::var("TERM").unwrap_or_default().to_ascii_lowercase();
159 let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default().to_ascii_lowercase();
160 let has_kitty = std::env::var("KITTY_WINDOW_ID").is_ok();
161 let has_iterm = std::env::var("ITERM_SESSION_ID").is_ok();
162 let has_wezterm = std::env::var("WEZTERM_PANE").is_ok();
163 let has_vte = std::env::var("VTE_VERSION").is_ok();
164
165 detect_terminal_notify_kind_from(&term, &term_program, has_kitty, has_iterm, has_wezterm, has_vte)
166}
167
168fn send_osc777_notification(message: Option<&str>) {
169 let body = sanitize_notification_text(message.unwrap_or("Human approval required"));
170 let title = sanitize_notification_text("VT Code");
171 let payload = build_osc777_payload(&title, &body);
172 print!("{payload}{BEL}");
173 let _ = std::io::stdout().flush();
174}
175
176fn send_osc9_notification(message: Option<&str>) {
177 let body = sanitize_notification_text(message.unwrap_or("Human approval required"));
178 let payload = build_osc9_payload(&body);
179 print!("{payload}{BEL}");
180 let _ = std::io::stdout().flush();
181}
182
183fn sanitize_notification_text(raw: &str) -> String {
184 const MAX_LEN: usize = 200;
185 let cleaned = raw.chars().filter(|c| *c >= ' ' && *c != '\u{007f}').collect::<String>();
186 let cleaned = crate::formatting::truncate_byte_budget(&cleaned, MAX_LEN, "");
187 cleaned.replace(';', ":")
188}
189
190fn detect_terminal_notify_kind_from(
191 term: &str,
192 term_program: &str,
193 has_kitty: bool,
194 has_iterm: bool,
195 has_wezterm: bool,
196 has_vte: bool,
197) -> TerminalNotifyKind {
198 if term.contains("kitty") || has_kitty {
199 return TerminalNotifyKind::Osc777;
200 }
201
202 if term_program.contains("ghostty") {
205 return TerminalNotifyKind::BellOnly;
206 }
207
208 if term_program.contains("iterm")
209 || term_program.contains("wezterm")
210 || term_program.contains("warp")
211 || term_program.contains("apple_terminal")
212 || has_iterm
213 || has_wezterm
214 {
215 return TerminalNotifyKind::Osc9;
216 }
217
218 if has_vte {
219 return TerminalNotifyKind::Osc777;
220 }
221
222 TerminalNotifyKind::BellOnly
223}
224
225fn build_osc777_payload(title: &str, body: &str) -> String {
226 format!("{OSC}777;notify;{title};{body}")
227}
228
229fn build_osc9_payload(body: &str) -> String {
230 format!("{OSC}9;{body}")
231}
232
233#[cfg(test)]
234mod redraw_tests {
235 use super::*;
236
237 #[test]
238 fn terminal_mapping_is_deterministic() {
239 assert_eq!(
240 detect_terminal_notify_kind_from("xterm-kitty", "", false, false, false, false),
241 TerminalNotifyKind::Osc777
242 );
243 assert_eq!(
245 detect_terminal_notify_kind_from("xterm-ghostty", "ghostty", false, false, false, false),
246 TerminalNotifyKind::BellOnly
247 );
248 assert_eq!(
249 detect_terminal_notify_kind_from("xterm-256color", "wezterm", false, false, false, false),
250 TerminalNotifyKind::Osc9
251 );
252 assert_eq!(
253 detect_terminal_notify_kind_from("xterm-256color", "", false, false, false, true),
254 TerminalNotifyKind::Osc777
255 );
256 assert_eq!(
257 detect_terminal_notify_kind_from("xterm-256color", "", false, false, false, false),
258 TerminalNotifyKind::BellOnly
259 );
260 }
261
262 #[test]
263 fn osc_payload_format_is_stable() {
264 assert_eq!(build_osc9_payload("done"), format!("{OSC}9;done"));
265 assert_eq!(build_osc777_payload("VT Code", "finished"), format!("{OSC}777;notify;VT Code;finished"));
266 }
267
268 #[test]
269 fn notification_sanitization_does_not_split_utf8() {
270 let raw = format!("{}界", "a".repeat(199));
271
272 assert_eq!(sanitize_notification_text(&raw), "a".repeat(199));
273 }
274}
275
276pub const RESET: &str = "\x1b[0m";
278
279pub const BOLD: &str = "\x1b[1m";
281pub const DIM: &str = "\x1b[2m";
282pub const ITALIC: &str = "\x1b[3m";
283pub const UNDERLINE: &str = "\x1b[4m";
284pub const BLINK: &str = "\x1b[5m";
285pub const REVERSE: &str = "\x1b[7m";
286pub const HIDDEN: &str = "\x1b[8m";
287pub const STRIKETHROUGH: &str = "\x1b[9m";
288
289pub const RESET_BOLD_DIM: &str = "\x1b[22m";
290pub const RESET_ITALIC: &str = "\x1b[23m";
291pub const RESET_UNDERLINE: &str = "\x1b[24m";
292pub const RESET_BLINK: &str = "\x1b[25m";
293pub const RESET_REVERSE: &str = "\x1b[27m";
294pub const RESET_HIDDEN: &str = "\x1b[28m";
295pub const RESET_STRIKETHROUGH: &str = "\x1b[29m";
296
297pub const FG_BLACK: &str = "\x1b[30m";
299pub const FG_RED: &str = "\x1b[31m";
300pub const FG_GREEN: &str = "\x1b[32m";
301pub const FG_YELLOW: &str = "\x1b[33m";
302pub const FG_BLUE: &str = "\x1b[34m";
303pub const FG_MAGENTA: &str = "\x1b[35m";
304pub const FG_CYAN: &str = "\x1b[36m";
305pub const FG_WHITE: &str = "\x1b[37m";
306pub const FG_DEFAULT: &str = "\x1b[39m";
307
308pub const BG_BLACK: &str = "\x1b[40m";
310pub const BG_RED: &str = "\x1b[41m";
311pub const BG_GREEN: &str = "\x1b[42m";
312pub const BG_YELLOW: &str = "\x1b[43m";
313pub const BG_BLUE: &str = "\x1b[44m";
314pub const BG_MAGENTA: &str = "\x1b[45m";
315pub const BG_CYAN: &str = "\x1b[46m";
316pub const BG_WHITE: &str = "\x1b[47m";
317pub const BG_DEFAULT: &str = "\x1b[49m";
318
319pub const FG_BRIGHT_BLACK: &str = "\x1b[90m";
321pub const FG_BRIGHT_RED: &str = "\x1b[91m";
322pub const FG_BRIGHT_GREEN: &str = "\x1b[92m";
323pub const FG_BRIGHT_YELLOW: &str = "\x1b[93m";
324pub const FG_BRIGHT_BLUE: &str = "\x1b[94m";
325pub const FG_BRIGHT_MAGENTA: &str = "\x1b[95m";
326pub const FG_BRIGHT_CYAN: &str = "\x1b[96m";
327pub const FG_BRIGHT_WHITE: &str = "\x1b[97m";
328
329pub const BG_BRIGHT_BLACK: &str = "\x1b[100m";
331pub const BG_BRIGHT_RED: &str = "\x1b[101m";
332pub const BG_BRIGHT_GREEN: &str = "\x1b[102m";
333pub const BG_BRIGHT_YELLOW: &str = "\x1b[103m";
334pub const BG_BRIGHT_BLUE: &str = "\x1b[104m";
335pub const BG_BRIGHT_MAGENTA: &str = "\x1b[105m";
336pub const BG_BRIGHT_CYAN: &str = "\x1b[106m";
337pub const BG_BRIGHT_WHITE: &str = "\x1b[107m";
338
339pub const CURSOR_HOME: &str = "\x1b[H";
341pub const CURSOR_HIDE: &str = "\x1b[?25l";
342pub const CURSOR_SHOW: &str = "\x1b[?25h";
343pub const CURSOR_SAVE_DEC: &str = "\x1b7";
344pub const CURSOR_RESTORE_DEC: &str = "\x1b8";
345pub const CURSOR_SAVE_SCO: &str = "\x1b[s";
346pub const CURSOR_RESTORE_SCO: &str = "\x1b[u";
347
348pub const CLEAR_SCREEN: &str = "\x1b[2J";
350pub const CLEAR_TO_END_OF_SCREEN: &str = "\x1b[0J";
351pub const CLEAR_TO_START_OF_SCREEN: &str = "\x1b[1J";
352pub const CLEAR_SAVED_LINES: &str = "\x1b[3J";
353pub const CLEAR_LINE: &str = "\x1b[2K";
354pub const CLEAR_TO_END_OF_LINE: &str = "\x1b[0K";
355pub const CLEAR_TO_START_OF_LINE: &str = "\x1b[1K";
356
357pub const ALT_BUFFER_ENABLE: &str = "\x1b[?1049h";
359pub const ALT_BUFFER_DISABLE: &str = "\x1b[?1049l";
360pub const SCREEN_SAVE: &str = "\x1b[?47h";
361pub const SCREEN_RESTORE: &str = "\x1b[?47l";
362pub const LINE_WRAP_ENABLE: &str = "\x1b[=7h";
363pub const LINE_WRAP_DISABLE: &str = "\x1b[=7l";
364
365pub const SCROLL_REGION_RESET: &str = "\x1b[r";
368
369pub const INSERT_LINE: &str = "\x1b[L";
372pub const DELETE_LINE: &str = "\x1b[M";
374pub const INSERT_CHAR: &str = "\x1b[@";
376pub const DELETE_CHAR: &str = "\x1b[P";
378pub const ERASE_CHAR: &str = "\x1b[X";
380
381pub const SCROLL_UP: &str = "\x1b[S";
384pub const SCROLL_DOWN: &str = "\x1b[T";
386
387pub const INDEX: &str = "\x1bD";
390pub const NEXT_LINE: &str = "\x1bE";
392pub const TAB_SET: &str = "\x1bH";
394pub const REVERSE_INDEX: &str = "\x1bM";
396pub const FULL_RESET: &str = "\x1bc";
398pub const KEYPAD_APPLICATION: &str = "\x1b=";
400pub const KEYPAD_NUMERIC: &str = "\x1b>";
402
403pub const MOUSE_X10_ENABLE: &str = "\x1b[?9h";
406pub const MOUSE_X10_DISABLE: &str = "\x1b[?9l";
407pub const MOUSE_NORMAL_ENABLE: &str = "\x1b[?1000h";
409pub const MOUSE_NORMAL_DISABLE: &str = "\x1b[?1000l";
410pub const MOUSE_BUTTON_EVENT_ENABLE: &str = "\x1b[?1002h";
412pub const MOUSE_BUTTON_EVENT_DISABLE: &str = "\x1b[?1002l";
413pub const MOUSE_ANY_EVENT_ENABLE: &str = "\x1b[?1003h";
415pub const MOUSE_ANY_EVENT_DISABLE: &str = "\x1b[?1003l";
416pub const MOUSE_SGR_ENABLE: &str = "\x1b[?1006h";
418pub const MOUSE_SGR_DISABLE: &str = "\x1b[?1006l";
419pub const MOUSE_URXVT_ENABLE: &str = "\x1b[?1015h";
421pub const MOUSE_URXVT_DISABLE: &str = "\x1b[?1015l";
422
423pub const BRACKETED_PASTE_ENABLE: &str = "\x1b[?2004h";
426pub const BRACKETED_PASTE_DISABLE: &str = "\x1b[?2004l";
427pub const FOCUS_EVENT_ENABLE: &str = "\x1b[?1004h";
429pub const FOCUS_EVENT_DISABLE: &str = "\x1b[?1004l";
430pub const SYNC_OUTPUT_BEGIN: &str = "\x1b[?2026h";
432pub const SYNC_OUTPUT_END: &str = "\x1b[?2026l";
433pub const APP_CURSOR_KEYS_ENABLE: &str = "\x1b[?1h";
435pub const APP_CURSOR_KEYS_DISABLE: &str = "\x1b[?1l";
436pub const ORIGIN_MODE_ENABLE: &str = "\x1b[?6h";
438pub const ORIGIN_MODE_DISABLE: &str = "\x1b[?6l";
439pub const AUTO_WRAP_ENABLE: &str = "\x1b[?7h";
441pub const AUTO_WRAP_DISABLE: &str = "\x1b[?7l";
442
443pub const DEVICE_ATTRIBUTES_REQUEST: &str = "\x1b[c";
446pub const CURSOR_POSITION_REQUEST: &str = "\x1b[6n";
448pub const DEVICE_STATUS_REQUEST: &str = "\x1b[5n";
450pub const COLOR_SCHEME_MODE_REQUEST: &str = "\x1b[?996n";
453pub const COLOR_SCHEME_REPORT_PREFIX: &str = "\x1b[?997;";
456pub const COLOR_SCHEME_REPORTS_ENABLE: &str = "\x1b[?2031h";
460pub const COLOR_SCHEME_REPORTS_DISABLE: &str = "\x1b[?2031l";
462
463pub const OSC_SET_TITLE_PREFIX: &str = "\x1b]2;";
466pub const OSC_SET_ICON_PREFIX: &str = "\x1b]1;";
468pub const OSC_SET_ICON_AND_TITLE_PREFIX: &str = "\x1b]0;";
470pub const OSC_FG_COLOR_PREFIX: &str = "\x1b]10;";
472pub const OSC_BG_COLOR_PREFIX: &str = "\x1b]11;";
474pub const OSC_CURSOR_COLOR_PREFIX: &str = "\x1b]12;";
476const OSC_HYPERLINK_PREFIX: &str = "\x1b]8;";
478pub const OSC_CLIPBOARD_PREFIX: &str = "\x1b]52;";
480
481pub const CHARSET_UTF8: &str = "\x1b%G";
484pub const CHARSET_DEFAULT: &str = "\x1b%@";
486
487#[inline]
490pub fn cursor_up(n: u16) -> String {
491 format!("{CSI}{n}A")
492}
493
494#[inline]
495pub fn cursor_down(n: u16) -> String {
496 format!("{CSI}{n}B")
497}
498
499#[inline]
500pub fn cursor_right(n: u16) -> String {
501 format!("{CSI}{n}C")
502}
503
504#[inline]
505pub fn cursor_left(n: u16) -> String {
506 format!("{CSI}{n}D")
507}
508
509#[inline]
510pub fn cursor_to(row: u16, col: u16) -> String {
511 format!("{CSI}{row};{col}H")
512}
513
514const REDRAW_LINE_PREFIX: &str = "\r\x1b[2K";
518
519#[inline]
520fn redraw_line_prefix() -> &'static str {
521 REDRAW_LINE_PREFIX
522}
523
524#[inline]
528fn format_redraw_line(content: &str) -> String {
529 format!("{}{}", redraw_line_prefix(), content)
530}
531
532#[inline]
533pub fn fg_256(color_id: u8) -> String {
534 format!("{CSI}38;5;{color_id}m")
535}
536
537#[inline]
538pub fn bg_256(color_id: u8) -> String {
539 format!("{CSI}48;5;{color_id}m")
540}
541
542#[inline]
543pub fn fg_rgb(r: u8, g: u8, b: u8) -> String {
544 format!("{CSI}38;2;{r};{g};{b}m")
545}
546
547#[inline]
548pub fn bg_rgb(r: u8, g: u8, b: u8) -> String {
549 format!("{CSI}48;2;{r};{g};{b}m")
550}
551
552#[inline]
553pub fn colored(text: &str, color: &str) -> String {
554 format!("{color}{text}{RESET}")
555}
556
557#[inline]
558pub fn bold(text: &str) -> String {
559 format!("{BOLD}{text}{RESET_BOLD_DIM}")
560}
561
562#[inline]
563pub fn italic(text: &str) -> String {
564 format!("{ITALIC}{text}{RESET_ITALIC}")
565}
566
567#[inline]
568pub fn underline(text: &str) -> String {
569 format!("{UNDERLINE}{text}{RESET_UNDERLINE}")
570}
571
572#[inline]
573pub fn dim(text: &str) -> String {
574 format!("{DIM}{text}{RESET_BOLD_DIM}")
575}
576
577#[inline]
578pub fn combine_styles(text: &str, styles: &[&str]) -> String {
579 let mut result = String::with_capacity(text.len() + styles.len() * 10);
580 for style in styles {
581 result.push_str(style);
582 }
583 result.push_str(text);
584 result.push_str(RESET);
585 result
586}
587
588pub mod semantic {
589 use super::*;
590 pub const ERROR: &str = FG_BRIGHT_RED;
591 pub const SUCCESS: &str = FG_BRIGHT_GREEN;
592 pub const WARNING: &str = FG_BRIGHT_YELLOW;
593 pub const INFO: &str = FG_BRIGHT_CYAN;
594 pub const MUTED: &str = DIM;
595 pub const EMPHASIS: &str = BOLD;
596 pub const DEBUG: &str = FG_BRIGHT_BLACK;
597}
598
599#[inline]
600#[must_use]
601pub fn contains_ansi(text: &str) -> bool {
602 text.contains(ESC_CHAR)
603}
604
605#[inline]
606#[must_use]
607pub fn starts_with_ansi(text: &str) -> bool {
608 text.starts_with(ESC_CHAR)
609}
610
611#[inline]
612#[must_use]
613pub fn ends_with_ansi(text: &str) -> bool {
614 text.ends_with('m') && text.contains(ESC)
615}
616
617#[inline]
618#[must_use]
619pub fn display_width(text: &str) -> usize {
620 crate::ansi::strip_ansi(text).len()
621}
622
623pub fn pad_to_width(text: &str, width: usize, pad_char: char) -> String {
624 let current_width = display_width(text);
625 if current_width >= width {
626 text.to_string()
627 } else {
628 let padding = pad_char.to_string().repeat(width - current_width);
629 format!("{text}{padding}")
630 }
631}
632
633pub fn truncate_to_width(text: &str, max_width: usize, ellipsis: &str) -> String {
634 let stripped = crate::ansi::strip_ansi(text);
635 if stripped.len() <= max_width {
636 return text.to_string();
637 }
638
639 let truncate_at = max_width.saturating_sub(ellipsis.len());
640 let truncated_plain: String = stripped.chars().take(truncate_at).collect();
641
642 if starts_with_ansi(text) {
643 let mut ansi_prefix = String::new();
644 for ch in text.chars() {
645 ansi_prefix.push(ch);
646 if ch == '\x1b' {
647 continue;
648 }
649 if ch.is_alphabetic() && ansi_prefix.contains('\x1b') {
650 break;
651 }
652 }
653 format!("{ansi_prefix}{truncated_plain}{ellipsis}{RESET}")
654 } else {
655 format!("{truncated_plain}{ellipsis}")
656 }
657}
658
659#[inline]
660pub fn write_styled<W: Write>(writer: &mut W, text: &str, style: &str) -> std::io::Result<()> {
661 writer.write_all(style.as_bytes())?;
662 writer.write_all(text.as_bytes())?;
663 writer.write_all(RESET.as_bytes())?;
664 Ok(())
665}
666
667#[inline]
668pub fn format_styled_into(buffer: &mut String, text: &str, style: &str) {
669 buffer.push_str(style);
670 buffer.push_str(text);
671 buffer.push_str(RESET);
672}
673
674#[inline]
676pub fn set_scroll_region(top: u16, bottom: u16) -> String {
677 format!("{CSI}{top};{bottom}r")
678}
679
680#[inline]
682pub fn insert_lines(n: u16) -> String {
683 format!("{CSI}{n}L")
684}
685
686#[inline]
688pub fn delete_lines(n: u16) -> String {
689 format!("{CSI}{n}M")
690}
691
692#[inline]
694pub fn scroll_up(n: u16) -> String {
695 format!("{CSI}{n}S")
696}
697
698#[inline]
700pub fn scroll_down(n: u16) -> String {
701 format!("{CSI}{n}T")
702}
703
704#[inline]
706pub fn set_window_title(title: &str) -> String {
707 format!("{OSC_SET_TITLE_PREFIX}{title}{BEL}")
708}
709
710#[inline]
712pub fn hyperlink_open(url: &str) -> String {
713 format!("{OSC_HYPERLINK_PREFIX};{url}{ST}")
714}
715
716#[inline]
718pub fn hyperlink_close() -> String {
719 format!("{OSC_HYPERLINK_PREFIX};{ST}")
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn redraw_prefix_matches_cli_pattern() {
728 assert_eq!(redraw_line_prefix(), "\r\x1b[2K");
729 }
730
731 #[test]
732 fn redraw_line_formats_expected_sequence() {
733 assert_eq!(format_redraw_line("Done"), "\r\x1b[2KDone");
734 }
735}