1use std::collections::HashMap;
11use std::env;
12use std::fs::OpenOptions;
13use std::io::{self, Stdout, Write};
14use std::sync::{Arc, Mutex, OnceLock};
15
16use crossterm::terminal::{self, ClearType};
17use crossterm::{cursor, execute, terminal as ct};
18
19use crate::Renderable;
20use crate::cells::cell_len;
21use crate::color::{ColorSystem, ColorTriplet, SimpleColor};
22use crate::emoji::Emoji;
23use crate::export_format::{CONSOLE_HTML_FORMAT, CONSOLE_SVG_FORMAT};
24use crate::highlighter::Highlighter;
25use crate::screen_buffer::ScreenBuffer;
26use crate::segment::{ControlType, Segment, Segments};
27use crate::style::Style;
28use crate::table::{Column, Row, Table};
29use crate::terminal_theme::{DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme};
30use crate::text::Text;
31use crate::theme::{Theme, ThemeStack};
32use crate::traceback::Traceback;
33
34use std::time::SystemTime;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37enum WindowsRenderMode {
38 Segment,
39 Streaming,
40}
41
42fn parse_windows_render_mode(value: Option<&str>) -> WindowsRenderMode {
43 match value.map(str::trim).map(str::to_ascii_lowercase).as_deref() {
44 Some("streaming") => WindowsRenderMode::Streaming,
45 Some("segment") => WindowsRenderMode::Segment,
46 _ => WindowsRenderMode::Streaming,
47 }
48}
49
50fn detect_windows_render_mode() -> WindowsRenderMode {
51 parse_windows_render_mode(env::var("RICH_RS_WINDOWS_RENDER_MODE").ok().as_deref())
52}
53
54fn parse_bool_env(value: &str) -> Option<bool> {
55 match value.trim().to_ascii_lowercase().as_str() {
56 "1" | "true" | "yes" | "on" => Some(true),
57 "0" | "false" | "no" | "off" => Some(false),
58 _ => None,
59 }
60}
61
62fn detect_legacy_windows_default() -> bool {
63 if let Ok(value) = env::var("RICH_RS_LEGACY_WINDOWS")
64 && let Some(parsed) = parse_bool_env(&value)
65 {
66 return parsed;
67 }
68 #[cfg(windows)]
69 {
70 return !crossterm::ansi_support::supports_ansi();
73 }
74 #[cfg(not(windows))]
75 {
76 false
77 }
78}
79
80fn debug_segments_log(line: &str) {
81 static PATH: OnceLock<Option<String>> = OnceLock::new();
82 let path = PATH.get_or_init(|| env::var("RICH_RS_DEBUG_SEGMENTS_FILE").ok());
83 let Some(path) = path.as_ref() else {
84 return;
85 };
86 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
87 let _ = writeln!(file, "{line}");
88 }
89}
90
91fn debug_ansi_log(line: &str) {
92 static PATH: OnceLock<Option<String>> = OnceLock::new();
93 let path = PATH.get_or_init(|| env::var("RICH_RS_DEBUG_ANSI_FILE").ok());
94 let Some(path) = path.as_ref() else {
95 return;
96 };
97 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
98 let _ = writeln!(file, "{line}");
99 }
100}
101
102fn debug_segments_match_text(text: &str) -> bool {
103 static FILTERS: OnceLock<Vec<String>> = OnceLock::new();
104 let filters = FILTERS.get_or_init(|| {
105 env::var("RICH_RS_DEBUG_SEGMENTS_FILTER")
106 .ok()
107 .map(|value| {
108 value
109 .split(',')
110 .map(|part| part.trim().to_ascii_lowercase())
111 .filter(|part| !part.is_empty())
112 .collect::<Vec<_>>()
113 })
114 .unwrap_or_default()
115 });
116 if filters.is_empty() {
117 return true;
118 }
119 let lowered = text.to_ascii_lowercase();
120 filters.iter().any(|filter| lowered.contains(filter))
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum JustifyMethod {
130 #[default]
132 Default,
133 Left,
135 Center,
137 Right,
139 Full,
141}
142
143impl JustifyMethod {
144 pub fn parse(s: &str) -> Option<Self> {
146 match s.to_lowercase().as_str() {
147 "default" => Some(JustifyMethod::Default),
148 "left" => Some(JustifyMethod::Left),
149 "center" => Some(JustifyMethod::Center),
150 "right" => Some(JustifyMethod::Right),
151 "full" => Some(JustifyMethod::Full),
152 _ => None,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163pub enum OverflowMethod {
164 #[default]
166 Fold,
167 Crop,
169 Ellipsis,
171 Ignore,
173}
174
175impl OverflowMethod {
176 pub fn parse(s: &str) -> Option<Self> {
178 match s.to_lowercase().as_str() {
179 "fold" => Some(OverflowMethod::Fold),
180 "crop" => Some(OverflowMethod::Crop),
181 "ellipsis" => Some(OverflowMethod::Ellipsis),
182 "ignore" => Some(OverflowMethod::Ignore),
183 _ => None,
184 }
185 }
186}
187
188#[derive(Debug, Clone)]
204pub struct ConsoleOptions {
205 pub size: (usize, usize),
207 pub min_width: usize,
209 pub max_width: usize,
211 pub max_height: usize,
213 pub height: Option<usize>,
215 pub is_terminal: bool,
217 pub encoding: String,
219 pub legacy_windows: bool,
221 pub justify: Option<JustifyMethod>,
223 pub overflow: Option<OverflowMethod>,
225 pub no_wrap: bool,
227 pub highlight: Option<bool>,
229 pub markup: Option<bool>,
231
232 pub theme_stack: ThemeStack,
237 pub theme_name: String,
240 pub markup_enabled: bool,
242 pub emoji_enabled: bool,
244 pub highlight_enabled: bool,
246 pub tab_size: usize,
248 pub disable_line_wrap: bool,
256 pub color_system: Option<ColorSystem>,
258}
259
260impl Default for ConsoleOptions {
261 fn default() -> Self {
262 ConsoleOptions {
263 size: (80, 24),
264 min_width: 1,
265 max_width: 80,
266 max_height: 24,
267 height: None,
268 is_terminal: true,
269 encoding: "utf-8".to_string(),
270 legacy_windows: false,
271 justify: None,
272 overflow: None,
273 no_wrap: false,
274 highlight: None,
275 markup: None,
276 theme_stack: ThemeStack::default(),
278 theme_name: "default".to_string(),
279 markup_enabled: true,
280 emoji_enabled: true,
281 highlight_enabled: true,
282 tab_size: 8,
283 disable_line_wrap: false,
284 color_system: Some(ColorSystem::EightBit),
285 }
286 }
287}
288
289impl ConsoleOptions {
290 pub fn from_terminal() -> Self {
292 let (width, height) = terminal::size().unwrap_or((80, 24));
293 let width = width as usize;
294 let height = height as usize;
295 let is_terminal = atty::is(atty::Stream::Stdout);
296 let color_system = Console::<Stdout>::detect_color_system_static(is_terminal);
297 ConsoleOptions {
298 size: (width, height),
299 min_width: 1,
300 max_width: width.max(1),
301 max_height: height,
302 height: None,
303 is_terminal,
304 disable_line_wrap: true,
308 color_system,
309 ..Default::default()
310 }
311 }
312
313 pub fn get_style(&self, name: &str) -> Option<Style> {
315 self.theme_stack.get_style(name)
316 }
317
318 pub fn ascii_only(&self) -> bool {
320 !self.encoding.to_lowercase().starts_with("utf")
321 }
322
323 pub fn copy(&self) -> Self {
325 self.clone()
326 }
327
328 pub fn update(
333 &self,
334 width: Option<usize>,
335 min_width: Option<usize>,
336 max_width: Option<usize>,
337 justify: Option<Option<JustifyMethod>>,
338 overflow: Option<Option<OverflowMethod>>,
339 no_wrap: Option<bool>,
340 highlight: Option<Option<bool>>,
341 markup: Option<Option<bool>>,
342 height: Option<Option<usize>>,
343 ) -> Self {
344 let mut options = self.clone();
345
346 if let Some(w) = width {
347 options.min_width = w.max(0);
348 options.max_width = w.max(0);
349 }
350 if let Some(w) = min_width {
351 options.min_width = w;
352 }
353 if let Some(w) = max_width {
354 options.max_width = w;
355 }
356 if let Some(j) = justify {
357 options.justify = j;
358 }
359 if let Some(o) = overflow {
360 options.overflow = o;
361 }
362 if let Some(nw) = no_wrap {
363 options.no_wrap = nw;
364 }
365 if let Some(h) = highlight {
366 options.highlight = h;
367 }
368 if let Some(m) = markup {
369 options.markup = m;
370 }
371 if let Some(h) = height {
372 if let Some(h) = h {
373 options.max_height = h;
374 }
375 options.height = h;
376 }
377
378 options
379 }
380
381 pub fn update_width(&self, width: usize) -> Self {
383 let mut options = self.clone();
384 options.min_width = width.max(0);
385 options.max_width = width.max(0);
386 options
387 }
388
389 pub fn update_height(&self, height: usize) -> Self {
391 let mut options = self.clone();
392 options.max_height = height;
393 options.height = Some(height);
394 options
395 }
396
397 pub fn update_dimensions(&self, width: usize, height: usize) -> Self {
399 let mut options = self.clone();
400 options.min_width = width.max(0);
401 options.max_width = width.max(0);
402 options.max_height = height;
403 options.height = Some(height);
404 options
405 }
406
407 pub fn reset_height(&self) -> Self {
409 let mut options = self.clone();
410 options.height = None;
411 options
412 }
413}
414
415pub struct Console<W: Write = Stdout> {
444 writer: W,
446 options: ConsoleOptions,
448 color_system: Option<ColorSystem>,
450 force_terminal: Option<bool>,
452 legacy_windows: bool,
454 markup_enabled: bool,
456 emoji_enabled: bool,
458 highlight_enabled: bool,
460 theme_stack: ThemeStack,
462 theme_name: String,
464 is_alt_screen: bool,
466 quiet: bool,
468 tab_size: usize,
470 live: LiveManager,
472 link_ids: HashMap<Arc<str>, Arc<str>>,
474 next_link_id: u64,
476 record: bool,
478 record_buffer: Arc<Mutex<Vec<Segment>>>,
480 render_hooks: Vec<Box<dyn Fn(&Segments) -> Segments + Send + Sync>>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485enum LiveVerticalOverflow {
486 Crop,
487 Ellipsis,
488 Visible,
489}
490
491impl From<crate::live::VerticalOverflowMethod> for LiveVerticalOverflow {
492 fn from(v: crate::live::VerticalOverflowMethod) -> Self {
493 match v {
494 crate::live::VerticalOverflowMethod::Crop => Self::Crop,
495 crate::live::VerticalOverflowMethod::Ellipsis => Self::Ellipsis,
496 crate::live::VerticalOverflowMethod::Visible => Self::Visible,
497 }
498 }
499}
500
501struct LiveEntry {
502 renderable: Box<dyn crate::Renderable + Send + Sync>,
503 vertical_overflow: LiveVerticalOverflow,
504}
505
506#[derive(Default)]
507struct LiveManager {
508 next_id: usize,
509 stack: Vec<usize>,
510 entries: HashMap<usize, LiveEntry>,
511 shape: Option<(usize, usize)>,
512 buffer: Option<ScreenBuffer>,
513}
514
515impl Console<Stdout> {
516 pub fn new() -> Self {
518 let options = ConsoleOptions::from_terminal();
519 let color_system = Self::detect_color_system_static(options.is_terminal);
520
521 Console {
522 writer: io::stdout(),
523 options,
524 color_system,
525 force_terminal: None,
526 legacy_windows: cfg!(windows) && detect_legacy_windows_default(),
527 markup_enabled: true,
528 emoji_enabled: true,
529 highlight_enabled: true,
530 theme_stack: ThemeStack::new(Theme::default()),
531 theme_name: "default".to_string(),
532 is_alt_screen: false,
533 quiet: false,
534 tab_size: 8,
535 live: LiveManager::default(),
536 link_ids: HashMap::new(),
537 next_link_id: 1,
538 record: false,
539 record_buffer: Arc::new(Mutex::new(Vec::new())),
540 render_hooks: Vec::new(),
541 }
542 }
543
544 pub fn new_with_record() -> Self {
559 let mut console = Self::new();
560 console.record = true;
561 console
562 }
563
564 pub fn with_theme(mut self, name: &str) -> Self {
579 if let Some(theme) = Theme::from_name(name) {
580 self.theme_stack = ThemeStack::new(theme.clone());
581 self.options.theme_stack = ThemeStack::new(theme);
582 self.theme_name = name.to_string();
583 self.options.theme_name = name.to_string();
584 }
585 self
586 }
587
588 pub fn with_options(options: ConsoleOptions) -> Self {
594 Console {
595 writer: io::stdout(),
596 color_system: options.color_system,
598 markup_enabled: options.markup_enabled,
599 emoji_enabled: options.emoji_enabled,
600 highlight_enabled: options.highlight_enabled,
601 theme_stack: options.theme_stack.clone(),
602 theme_name: options.theme_name.clone(),
603 tab_size: options.tab_size,
604 legacy_windows: options.legacy_windows,
605 force_terminal: None,
607 is_alt_screen: false,
608 quiet: false,
609 options,
611 live: LiveManager::default(),
612 link_ids: HashMap::new(),
613 next_link_id: 1,
614 record: false,
615 record_buffer: Arc::new(Mutex::new(Vec::new())),
616 render_hooks: Vec::new(),
617 }
618 }
619
620 fn detect_color_system_static(is_terminal: bool) -> Option<ColorSystem> {
622 if let Ok(value) = env::var("RICH_RS_COLOR_SYSTEM") {
624 match value.to_ascii_lowercase().as_str() {
625 "none" | "off" | "0" => return None,
626 "16" | "standard" => return Some(ColorSystem::Standard),
627 "256" | "eightbit" | "8bit" => return Some(ColorSystem::EightBit),
628 "truecolor" | "24bit" | "rgb" => return Some(ColorSystem::TrueColor),
629 "auto" => {}
630 _ => {}
631 }
632 }
633
634 if env::var("NO_COLOR").is_ok() {
636 return None;
637 }
638
639 let force_color = env::var("FORCE_COLOR").is_ok();
640 if !is_terminal && !force_color {
641 return None;
642 }
643
644 #[cfg(windows)]
645 if is_terminal && !crossterm::ansi_support::supports_ansi() {
646 return Some(ColorSystem::Standard);
648 }
649
650 if let Ok(colorterm) = env::var("COLORTERM") {
651 let ct = colorterm.to_ascii_lowercase();
652 if ct == "truecolor" || ct == "24bit" || ct == "yes" || ct == "true" {
653 return Some(ColorSystem::TrueColor);
654 }
655 }
656
657 if let Ok(term) = env::var("TERM") {
658 let term_lower = term.to_ascii_lowercase();
659 if term_lower.contains("truecolor")
660 || term_lower.contains("24bit")
661 || term_lower.contains("direct")
662 {
663 return Some(ColorSystem::TrueColor);
664 }
665 if term_lower.contains("256color") {
666 return Some(ColorSystem::EightBit);
667 }
668 if term_lower == "dumb" || term_lower == "unknown" {
669 return None;
670 }
671 }
672
673 if is_terminal {
675 #[cfg(windows)]
676 {
677 return Some(ColorSystem::TrueColor);
678 }
679 #[cfg(not(windows))]
680 {
681 return Some(ColorSystem::TrueColor);
682 }
683 }
684 if force_color {
685 return Some(ColorSystem::EightBit);
686 }
687 None
688 }
689}
690
691impl Default for Console<Stdout> {
692 fn default() -> Self {
693 Console::new()
694 }
695}
696
697impl Console<Vec<u8>> {
698 pub fn capture() -> Self {
713 Console::with_writer(
714 Vec::new(),
715 ConsoleOptions {
716 is_terminal: false,
717 ..Default::default()
718 },
719 )
720 }
721
722 pub fn capture_with_options(options: ConsoleOptions) -> Self {
724 Console::with_writer(Vec::new(), options)
725 }
726
727 pub fn get_captured(&self) -> String {
729 String::from_utf8_lossy(&self.writer).to_string()
730 }
731
732 pub fn get_captured_bytes(&self) -> &[u8] {
734 &self.writer
735 }
736
737 pub fn clear_captured(&mut self) {
739 self.writer.clear();
740 }
741}
742
743impl<W: Write> Console<W> {
744 fn link_id_for_url(&mut self, url: &Arc<str>) -> Arc<str> {
745 if let Some(existing) = self.link_ids.get(url) {
746 return existing.clone();
747 }
748 let id: Arc<str> = Arc::from(format!("richrs-{}", self.next_link_id));
749 self.next_link_id = self.next_link_id.saturating_add(1);
750 self.link_ids.insert(url.clone(), id.clone());
751 id
752 }
753
754 pub fn with_writer(writer: W, options: ConsoleOptions) -> Self {
759 Console {
760 writer,
761 color_system: options.color_system,
763 markup_enabled: options.markup_enabled,
764 emoji_enabled: options.emoji_enabled,
765 highlight_enabled: options.highlight_enabled,
766 theme_stack: options.theme_stack.clone(),
767 theme_name: options.theme_name.clone(),
768 tab_size: options.tab_size,
769 legacy_windows: options.legacy_windows,
770 force_terminal: None,
772 is_alt_screen: false,
773 quiet: false,
774 options,
776 live: LiveManager::default(),
777 link_ids: HashMap::new(),
778 next_link_id: 1,
779 record: false,
780 record_buffer: Arc::new(Mutex::new(Vec::new())),
781 render_hooks: Vec::new(),
782 }
783 }
784
785 pub fn options(&self) -> &ConsoleOptions {
791 &self.options
792 }
793
794 pub fn options_mut(&mut self) -> &mut ConsoleOptions {
808 &mut self.options
809 }
810
811 pub fn sync_from_options(&mut self) {
816 self.markup_enabled = self.options.markup_enabled;
817 self.emoji_enabled = self.options.emoji_enabled;
818 self.highlight_enabled = self.options.highlight_enabled;
819 self.tab_size = self.options.tab_size;
820 self.color_system = self.options.color_system;
821 self.theme_stack = self.options.theme_stack.clone();
822 self.theme_name = self.options.theme_name.clone();
823 self.legacy_windows = self.options.legacy_windows;
824 }
825
826 pub fn options_with_state(&self) -> ConsoleOptions {
839 self.options.clone()
841 }
842
843 pub fn width(&self) -> usize {
845 self.options.max_width
846 }
847
848 pub fn height(&self) -> usize {
850 self.options.max_height
851 }
852
853 pub fn size(&self) -> (usize, usize) {
855 self.options.size
856 }
857
858 pub fn set_size(&mut self, width: usize, height: usize) {
860 self.options.size = (width, height);
861 self.options.max_width = width;
862 self.options.max_height = height;
863 }
864
865 pub fn is_terminal(&self) -> bool {
867 self.force_terminal.unwrap_or(self.options.is_terminal)
868 }
869
870 pub fn is_dumb_terminal(&self) -> bool {
872 match env::var("TERM") {
873 Ok(term) => {
874 let t = term.to_lowercase();
875 t == "dumb" || t == "unknown"
876 }
877 Err(_) => false,
878 }
879 }
880
881 pub fn set_force_terminal(&mut self, force: Option<bool>) {
883 self.force_terminal = force;
884 }
885
886 pub fn color_system(&self) -> Option<ColorSystem> {
888 self.color_system
889 }
890
891 pub fn set_color_system(&mut self, system: Option<ColorSystem>) {
893 self.color_system = system;
894 self.options.color_system = system;
895 }
896
897 pub fn is_markup_enabled(&self) -> bool {
899 self.markup_enabled
900 }
901
902 pub fn set_markup_enabled(&mut self, enabled: bool) {
904 self.markup_enabled = enabled;
905 self.options.markup_enabled = enabled;
906 }
907
908 pub fn is_emoji_enabled(&self) -> bool {
910 self.emoji_enabled
911 }
912
913 pub fn set_emoji_enabled(&mut self, enabled: bool) {
915 self.emoji_enabled = enabled;
916 self.options.emoji_enabled = enabled;
917 }
918
919 pub fn is_highlight_enabled(&self) -> bool {
921 self.highlight_enabled
922 }
923
924 pub fn set_highlight_enabled(&mut self, enabled: bool) {
926 self.highlight_enabled = enabled;
927 self.options.highlight_enabled = enabled;
928 }
929
930 pub fn tab_size(&self) -> usize {
932 self.tab_size
933 }
934
935 pub fn encoding(&self) -> &str {
937 &self.options.encoding
938 }
939
940 pub fn set_encoding(&mut self, encoding: impl Into<String>) {
942 self.options.encoding = encoding.into();
943 }
944
945 pub fn set_tab_size(&mut self, size: usize) {
947 self.tab_size = size;
948 self.options.tab_size = size;
949 }
950
951 pub fn is_quiet(&self) -> bool {
953 self.quiet
954 }
955
956 pub fn set_quiet(&mut self, quiet: bool) {
958 self.quiet = quiet;
959 }
960
961 pub fn theme_name(&self) -> &str {
965 &self.theme_name
966 }
967
968 pub fn set_theme(&mut self, name: &str) {
982 if let Some(theme) = Theme::from_name(name) {
983 self.theme_stack = ThemeStack::new(theme.clone());
985 self.options.theme_stack = ThemeStack::new(theme);
986 self.theme_name = name.to_string();
987 self.options.theme_name = name.to_string();
988 }
989 }
990
991 pub fn theme_stack(&self) -> &ThemeStack {
993 &self.theme_stack
994 }
995
996 pub fn theme_stack_mut(&mut self) -> &mut ThemeStack {
1006 &mut self.theme_stack
1007 }
1008
1009 pub fn sync_theme_to_options(&mut self) {
1014 self.options.theme_stack = self.theme_stack.clone();
1015 }
1016
1017 pub fn push_theme(&mut self, theme: Theme) {
1021 self.theme_stack.push_theme(theme.clone());
1022 self.options.theme_stack.push_theme(theme);
1023 }
1024
1025 pub fn pop_theme(&mut self) -> Result<(), crate::theme::ThemeError> {
1029 self.theme_stack.pop_theme()?;
1030 self.options.theme_stack.pop_theme()
1031 }
1032
1033 pub fn render_lines<R: Renderable + ?Sized>(
1054 &self,
1055 renderable: &R,
1056 options: Option<&ConsoleOptions>,
1057 style: Option<Style>,
1058 pad: bool,
1059 new_lines: bool,
1060 ) -> Vec<Vec<Segment>> {
1061 let render_options = options.cloned().unwrap_or_else(|| self.options.clone());
1063
1064 let temp_console = Console::<Stdout>::with_options(render_options.clone());
1068 let segments = renderable.render(&temp_console, &render_options);
1069
1070 let segments = if let Some(s) = style {
1072 Segment::apply_style_to_segments(segments, Some(s), None)
1073 } else {
1074 segments
1075 };
1076 let segments = self.apply_render_hooks(segments);
1077
1078 let width = render_options.max_width;
1080 Segment::split_and_crop_lines(segments, width, style, pad, new_lines)
1081 }
1082
1083 fn apply_render_hooks(&self, mut segments: Segments) -> Segments {
1084 for hook in &self.render_hooks {
1085 segments = hook(&segments);
1086 }
1087 segments
1088 }
1089
1090 pub fn render_str(
1105 &self,
1106 text: &str,
1107 markup: Option<bool>,
1108 emoji: Option<bool>,
1109 highlight: Option<bool>,
1110 highlighter: Option<&dyn Highlighter>,
1111 ) -> Text {
1112 let markup_enabled = markup.unwrap_or(self.markup_enabled);
1113 let emoji_enabled = emoji.unwrap_or(self.emoji_enabled);
1114 let highlight_enabled = highlight.unwrap_or(self.highlight_enabled);
1115
1116 let processed_text = if emoji_enabled {
1118 Emoji::replace(text)
1119 } else {
1120 text.to_string()
1121 };
1122
1123 let mut result = if markup_enabled {
1125 Text::from_markup(&processed_text, false)
1126 .unwrap_or_else(|_| Text::plain(&processed_text))
1127 } else {
1128 Text::plain(&processed_text)
1129 };
1130
1131 if let (true, Some(hl)) = (highlight_enabled, highlighter) {
1133 hl.highlight(&mut result);
1134 }
1135
1136 result
1137 }
1138
1139 pub fn write_raw(&mut self, data: &[u8]) -> io::Result<()> {
1145 if self.quiet {
1146 return Ok(());
1147 }
1148 self.writer.write_all(data)?;
1149 self.writer.flush()
1150 }
1151
1152 pub fn write_str(&mut self, s: &str) -> io::Result<()> {
1154 self.write_raw(s.as_bytes())
1155 }
1156
1157 pub fn print_text(&mut self, text: &str) -> io::Result<()> {
1159 if self.quiet {
1160 return Ok(());
1161 }
1162 if self.record || (self.is_terminal() && !self.is_dumb_terminal() && self.has_live()) {
1164 return self.print(&Text::plain(text), None, None, None, false, "\n");
1165 }
1166 writeln!(self.writer, "{}", text)?;
1167 self.writer.flush()
1168 }
1169
1170 pub fn print_styled(&mut self, text: &str, style: Style) -> io::Result<()> {
1172 if self.quiet {
1173 return Ok(());
1174 }
1175 if self.record || (self.is_terminal() && !self.is_dumb_terminal() && self.has_live()) {
1177 return self.print(&Text::styled(text, style), None, None, None, false, "\n");
1178 }
1179 if let Some(color_system) = self.color_system {
1181 let styled = style.render(text, color_system);
1182 writeln!(self.writer, "{}", styled)?;
1183 } else {
1184 writeln!(self.writer, "{}", text)?;
1185 }
1186 self.writer.flush()
1187 }
1188
1189 pub fn print_traceback(&mut self, traceback: &Traceback) -> io::Result<()> {
1208 self.print(traceback, None, None, None, false, "\n")
1209 }
1210
1211 pub fn print_segment(&mut self, segment: &Segment) -> io::Result<()> {
1213 if self.quiet {
1214 return Ok(());
1215 }
1216
1217 if let Some(style) = segment.style {
1218 if let Some(color_system) = self.color_system {
1219 let styled = style.render(&segment.text, color_system);
1220 write!(self.writer, "{}", styled)?;
1221 } else {
1222 write!(self.writer, "{}", segment.text)?;
1223 }
1224 } else {
1225 write!(self.writer, "{}", segment.text)?;
1226 }
1227 self.writer.flush()
1228 }
1229
1230 pub fn print_segments(&mut self, segments: &Segments) -> io::Result<()> {
1235 if self.quiet {
1236 return Ok(());
1237 }
1238 if cfg!(windows) && detect_windows_render_mode() == WindowsRenderMode::Segment {
1239 return self.print_segments_segment_mode(segments);
1240 }
1241
1242 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1243 struct StyleState {
1244 fg: crate::color::SimpleColor,
1245 bg: crate::color::SimpleColor,
1246 bold: bool,
1247 dim: bool,
1248 italic: bool,
1249 underline: bool,
1250 blink: bool,
1251 reverse: bool,
1252 strike: bool,
1253 }
1254
1255 impl StyleState {
1256 const DEFAULT: Self = Self {
1257 fg: crate::color::SimpleColor::Default,
1258 bg: crate::color::SimpleColor::Default,
1259 bold: false,
1260 dim: false,
1261 italic: false,
1262 underline: false,
1263 blink: false,
1264 reverse: false,
1265 strike: false,
1266 };
1267
1268 fn from_style(style: Option<Style>) -> Self {
1269 let style = style.unwrap_or_default();
1270 Self {
1271 fg: style.color.unwrap_or(crate::color::SimpleColor::Default),
1272 bg: style.bgcolor.unwrap_or(crate::color::SimpleColor::Default),
1273 bold: style.bold.unwrap_or(false),
1274 dim: style.dim.unwrap_or(false),
1275 italic: style.italic.unwrap_or(false),
1276 underline: style.underline.unwrap_or(false),
1277 blink: style.blink.unwrap_or(false),
1278 reverse: style.reverse.unwrap_or(false),
1279 strike: style.strike.unwrap_or(false),
1280 }
1281 }
1282
1283 fn sgr_diff(self, target: Self, color_system: ColorSystem) -> String {
1284 if self == target {
1285 return String::new();
1286 }
1287
1288 let mut sgr: Vec<String> = Vec::new();
1289
1290 let needs_22 = (self.bold && !target.bold) || (self.dim && !target.dim);
1293 if needs_22 {
1294 sgr.push("22".to_string());
1295 }
1296 if self.italic && !target.italic {
1297 sgr.push("23".to_string());
1298 }
1299 if self.underline && !target.underline {
1300 sgr.push("24".to_string());
1301 }
1302 if self.blink && !target.blink {
1303 sgr.push("25".to_string());
1304 }
1305 if self.reverse && !target.reverse {
1306 sgr.push("27".to_string());
1307 }
1308 if self.strike && !target.strike {
1309 sgr.push("29".to_string());
1310 }
1311
1312 if self.fg != target.fg {
1315 let fg = target.fg.downgrade(color_system);
1316 sgr.extend(fg.get_ansi_codes(true));
1317 }
1318 if self.bg != target.bg {
1319 let bg = target.bg.downgrade(color_system);
1320 sgr.extend(bg.get_ansi_codes(false));
1321 }
1322
1323 if target.bold && (!self.bold || needs_22) {
1325 sgr.push("1".to_string());
1326 }
1327 if target.dim && (!self.dim || needs_22) {
1328 sgr.push("2".to_string());
1329 }
1330 if target.italic && !self.italic {
1331 sgr.push("3".to_string());
1332 }
1333 if target.underline && !self.underline {
1334 sgr.push("4".to_string());
1335 }
1336 if target.blink && !self.blink {
1337 sgr.push("5".to_string());
1338 }
1339 if target.reverse && !self.reverse {
1340 sgr.push("7".to_string());
1341 }
1342 if target.strike && !self.strike {
1343 sgr.push("9".to_string());
1344 }
1345
1346 sgr.join(";")
1347 }
1348 }
1349
1350 let mut current = StyleState::DEFAULT;
1351 let mut used_sgr = false;
1352 let hyperlinks_enabled = self.is_terminal() && !self.is_dumb_terminal();
1353 let mut current_link: Option<(Arc<str>, Option<Arc<str>>)> = None;
1354 let mut hyperlink_manual = false;
1355
1356 for segment in segments.iter() {
1357 if let Some(control) = &segment.control {
1358 debug_segments_log(&format!("[control][streaming] {:?}", control));
1359 match control {
1362 ControlType::Bell => write!(self.writer, "\x07")?,
1363 ControlType::CarriageReturn => write!(self.writer, "\r")?,
1364 ControlType::Home => write!(self.writer, "\x1b[H")?,
1365 ControlType::Clear => write!(self.writer, "\x1b[2J\x1b[H")?,
1366 ControlType::ShowCursor => write!(self.writer, "\x1b[?25h")?,
1367 ControlType::HideCursor => write!(self.writer, "\x1b[?25l")?,
1368 ControlType::EnableAltScreen => write!(self.writer, "\x1b[?1049h")?,
1369 ControlType::DisableAltScreen => write!(self.writer, "\x1b[?1049l")?,
1370 ControlType::SetTitle => {
1371 }
1373 ControlType::CursorUp(n) => write!(self.writer, "\x1b[{}A", n)?,
1374 ControlType::CursorDown(n) => write!(self.writer, "\x1b[{}B", n)?,
1375 ControlType::CursorForward(n) => write!(self.writer, "\x1b[{}C", n)?,
1376 ControlType::CursorBackward(n) => write!(self.writer, "\x1b[{}D", n)?,
1377 ControlType::EraseInLine(mode) => write!(self.writer, "\x1b[{}K", mode)?,
1378 ControlType::HyperlinkStart { url, id } => {
1379 if hyperlinks_enabled {
1380 if let Some(id) = id.as_deref() {
1381 write!(self.writer, "\x1b]8;id={};{}\x1b\\", id, url)?;
1382 } else {
1383 write!(self.writer, "\x1b]8;;{}\x1b\\", url)?;
1384 }
1385 current_link = Some((url.clone(), id.clone()));
1386 hyperlink_manual = true;
1387 }
1388 }
1389 ControlType::HyperlinkEnd => {
1390 if hyperlinks_enabled {
1391 write!(self.writer, "\x1b]8;;\x1b\\")?;
1392 current_link = None;
1393 hyperlink_manual = false;
1394 }
1395 }
1396 ControlType::MoveTo { x, y } => {
1397 write!(
1399 self.writer,
1400 "\x1b[{};{}H",
1401 (*y as usize) + 1,
1402 (*x as usize) + 1
1403 )?
1404 }
1405 }
1406 continue;
1407 }
1408
1409 if hyperlinks_enabled && !hyperlink_manual {
1410 let mut desired_link: Option<(Arc<str>, Option<Arc<str>>)> = None;
1411 if let Some(meta) = segment.meta.as_ref() {
1412 if let Some(url) = meta.link.as_ref() {
1413 let url = url.clone();
1414 let id = meta
1415 .link_id
1416 .clone()
1417 .or_else(|| Some(self.link_id_for_url(&url)));
1418 desired_link = Some((url, id));
1419 }
1420 }
1421
1422 if desired_link != current_link {
1423 if current_link.is_some() {
1425 write!(self.writer, "\x1b]8;;\x1b\\")?;
1426 }
1427 if let Some((url, id)) = &desired_link {
1429 if let Some(id) = id.as_deref() {
1430 write!(self.writer, "\x1b]8;id={};{}\x1b\\", id, url)?;
1431 } else {
1432 write!(self.writer, "\x1b]8;;{}\x1b\\", url)?;
1433 }
1434 }
1435 current_link = desired_link;
1436 }
1437 }
1438
1439 if let Some(color_system) = self.color_system {
1440 if debug_segments_match_text(&segment.text) {
1441 debug_segments_log(&format!(
1442 "[segment][streaming] text={:?} style={:?} color_system={:?}",
1443 segment.text, segment.style, self.color_system
1444 ));
1445 }
1446 let target = StyleState::from_style(segment.style);
1447 let diff = current.sgr_diff(target, color_system);
1448 if !diff.is_empty() {
1449 write!(self.writer, "\x1b[{}m", diff)?;
1450 if debug_segments_match_text(&segment.text) {
1451 debug_ansi_log(&format!(
1452 "[ansi][streaming] text={:?} sgr=\\x1b[{}m target={:?}",
1453 segment.text, diff, target
1454 ));
1455 }
1456 used_sgr = true;
1457 }
1458 write!(self.writer, "{}", segment.text)?;
1459 current = target;
1460 } else {
1461 if debug_segments_match_text(&segment.text) {
1462 debug_segments_log(&format!(
1463 "[segment][streaming] text={:?} style={:?} color_system=None",
1464 segment.text, segment.style
1465 ));
1466 }
1467 write!(self.writer, "{}", segment.text)?;
1468 }
1469 }
1470
1471 if hyperlinks_enabled && current_link.is_some() {
1473 write!(self.writer, "\x1b]8;;\x1b\\")?;
1474 }
1475
1476 if self.color_system.is_some() && used_sgr && current != StyleState::DEFAULT {
1478 write!(self.writer, "\x1b[0m")?;
1479 debug_ansi_log("[ansi][streaming] tail-reset=\\x1b[0m");
1480 }
1481
1482 self.writer.flush()
1483 }
1484
1485 fn print_segments_segment_mode(&mut self, segments: &Segments) -> io::Result<()> {
1486 let hyperlinks_enabled = self.is_terminal() && !self.is_dumb_terminal();
1487 let mut current_link: Option<(Arc<str>, Option<Arc<str>>)> = None;
1488 let mut hyperlink_manual = false;
1489
1490 for segment in segments.iter() {
1491 if let Some(control) = &segment.control {
1492 debug_segments_log(&format!("[control][segment] {:?}", control));
1493 match control {
1494 ControlType::Bell => write!(self.writer, "\x07")?,
1495 ControlType::CarriageReturn => write!(self.writer, "\r")?,
1496 ControlType::Home => write!(self.writer, "\x1b[H")?,
1497 ControlType::Clear => write!(self.writer, "\x1b[2J\x1b[H")?,
1498 ControlType::ShowCursor => write!(self.writer, "\x1b[?25h")?,
1499 ControlType::HideCursor => write!(self.writer, "\x1b[?25l")?,
1500 ControlType::EnableAltScreen => write!(self.writer, "\x1b[?1049h")?,
1501 ControlType::DisableAltScreen => write!(self.writer, "\x1b[?1049l")?,
1502 ControlType::SetTitle => {}
1503 ControlType::CursorUp(n) => write!(self.writer, "\x1b[{}A", n)?,
1504 ControlType::CursorDown(n) => write!(self.writer, "\x1b[{}B", n)?,
1505 ControlType::CursorForward(n) => write!(self.writer, "\x1b[{}C", n)?,
1506 ControlType::CursorBackward(n) => write!(self.writer, "\x1b[{}D", n)?,
1507 ControlType::EraseInLine(mode) => write!(self.writer, "\x1b[{}K", mode)?,
1508 ControlType::HyperlinkStart { url, id } => {
1509 if hyperlinks_enabled {
1510 if let Some(id) = id.as_deref() {
1511 write!(self.writer, "\x1b]8;id={};{}\x1b\\", id, url)?;
1512 } else {
1513 write!(self.writer, "\x1b]8;;{}\x1b\\", url)?;
1514 }
1515 current_link = Some((url.clone(), id.clone()));
1516 hyperlink_manual = true;
1517 }
1518 }
1519 ControlType::HyperlinkEnd => {
1520 if hyperlinks_enabled {
1521 write!(self.writer, "\x1b]8;;\x1b\\")?;
1522 current_link = None;
1523 hyperlink_manual = false;
1524 }
1525 }
1526 ControlType::MoveTo { x, y } => write!(
1527 self.writer,
1528 "\x1b[{};{}H",
1529 (*y as usize) + 1,
1530 (*x as usize) + 1
1531 )?,
1532 }
1533 continue;
1534 }
1535
1536 if hyperlinks_enabled && !hyperlink_manual {
1537 let mut desired_link: Option<(Arc<str>, Option<Arc<str>>)> = None;
1538 if let Some(meta) = segment.meta.as_ref() {
1539 if let Some(url) = meta.link.as_ref() {
1540 let url = url.clone();
1541 let id = meta
1542 .link_id
1543 .clone()
1544 .or_else(|| Some(self.link_id_for_url(&url)));
1545 desired_link = Some((url, id));
1546 }
1547 }
1548
1549 if desired_link != current_link {
1550 if current_link.is_some() {
1551 write!(self.writer, "\x1b]8;;\x1b\\")?;
1552 }
1553 if let Some((url, id)) = &desired_link {
1554 if let Some(id) = id.as_deref() {
1555 write!(self.writer, "\x1b]8;id={};{}\x1b\\", id, url)?;
1556 } else {
1557 write!(self.writer, "\x1b]8;;{}\x1b\\", url)?;
1558 }
1559 }
1560 current_link = desired_link;
1561 }
1562 }
1563
1564 if let Some(style) = segment.style {
1565 if debug_segments_match_text(&segment.text) {
1566 debug_segments_log(&format!(
1567 "[segment][segment] text={:?} style={:?} color_system={:?}",
1568 segment.text, style, self.color_system
1569 ));
1570 }
1571 if let Some(color_system) = self.color_system {
1572 let styled = style.render(&segment.text, color_system);
1573 if debug_segments_match_text(&segment.text) {
1574 let sgr = styled
1575 .strip_prefix("\x1b[")
1576 .and_then(|rest| rest.split_once('m').map(|(a, _)| a))
1577 .unwrap_or("<none>");
1578 debug_ansi_log(&format!(
1579 "[ansi][segment] text={:?} sgr=\\x1b[{}m style={:?} color_system={:?}",
1580 segment.text, sgr, style, self.color_system
1581 ));
1582 }
1583 write!(self.writer, "{}", styled)?;
1584 } else {
1585 write!(self.writer, "{}", segment.text)?;
1586 }
1587 } else {
1588 if debug_segments_match_text(&segment.text) {
1589 debug_segments_log(&format!(
1590 "[segment][segment] text={:?} style=None color_system={:?}",
1591 segment.text, self.color_system
1592 ));
1593 }
1594 write!(self.writer, "{}", segment.text)?;
1595 }
1596 }
1597
1598 if hyperlinks_enabled && current_link.is_some() {
1599 write!(self.writer, "\x1b]8;;\x1b\\")?;
1600 }
1601
1602 self.writer.flush()
1603 }
1604
1605 pub fn print<R: Renderable + ?Sized>(
1619 &mut self,
1620 renderable: &R,
1621 style: Option<Style>,
1622 justify: Option<JustifyMethod>,
1623 overflow: Option<OverflowMethod>,
1624 no_wrap: bool,
1625 end: &str,
1626 ) -> io::Result<()> {
1627 if self.quiet {
1628 return Ok(());
1629 }
1630
1631 let options = self.options.update(
1633 None,
1634 None,
1635 None,
1636 Some(justify),
1637 Some(overflow),
1638 Some(no_wrap),
1639 None,
1640 None,
1641 None,
1642 );
1643
1644 let temp_console = Console::<Stdout>::with_options(options.clone());
1647
1648 let segments = renderable.render(&temp_console, &options);
1650
1651 let mut segments = if let Some(s) = style {
1653 Segment::apply_style_to_segments(segments, Some(s), None)
1654 } else {
1655 segments
1656 };
1657 segments = self.apply_render_hooks(segments);
1658
1659 let live_active = self.is_terminal() && !self.is_dumb_terminal() && self.has_live();
1660 let mut end_to_write = end;
1661 if live_active {
1662 if !end.is_empty() {
1665 segments.push(Segment::new(end.to_string()));
1666 }
1667 end_to_write = "";
1668
1669 let (live_segments, full_redraw) = self.render_live_segments(&options);
1670 let mut wrapped = Segments::new();
1671 let cursor_controls = if full_redraw {
1672 self.live_position_cursor()
1673 } else {
1674 self.live_position_cursor_no_erase()
1675 };
1676 for seg in cursor_controls.iter() {
1677 wrapped.push(seg.clone());
1678 }
1679 for seg in segments.into_iter() {
1680 wrapped.push(seg);
1681 }
1682 for seg in live_segments.into_iter() {
1683 wrapped.push(seg);
1684 }
1685 segments = wrapped;
1686 }
1687
1688 let should_disable_wrap = self.options.disable_line_wrap && atty::is(atty::Stream::Stdout);
1689 if should_disable_wrap {
1690 write!(self.writer, "\x1b[?7l")?;
1693 }
1694
1695 if self.record {
1697 if let Ok(mut buffer) = self.record_buffer.lock() {
1698 for seg in segments.iter() {
1699 buffer.push(seg.clone());
1700 }
1701 if !end_to_write.is_empty() {
1702 buffer.push(Segment::new(end_to_write.to_string()));
1703 }
1704 }
1705 }
1706
1707 let result = (|| {
1708 self.print_segments(&segments)?;
1710
1711 if !end_to_write.is_empty() {
1713 write!(self.writer, "{}", end_to_write)?;
1714 }
1715
1716 self.writer.flush()
1717 })();
1718
1719 if should_disable_wrap {
1720 let _ = write!(self.writer, "\x1b[?7h");
1722 }
1723
1724 result
1725 }
1726
1727 pub fn log<R: Renderable + ?Sized>(
1751 &mut self,
1752 renderable: &R,
1753 file: Option<&str>,
1754 line: Option<u32>,
1755 ) -> io::Result<()> {
1756 if self.quiet {
1757 return Ok(());
1758 }
1759
1760 let now = SystemTime::now();
1762 let duration = now
1763 .duration_since(SystemTime::UNIX_EPOCH)
1764 .unwrap_or_default();
1765 let secs = duration.as_secs();
1766 let hours = (secs / 3600) % 24;
1767 let minutes = (secs / 60) % 60;
1768 let seconds = secs % 60;
1769
1770 let timestamp = format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds);
1772 let time_style = self
1773 .theme_stack
1774 .get_style("log.time")
1775 .unwrap_or_else(|| Style::new().with_dim(true));
1776 let time_text = Text::styled(×tamp, time_style);
1777
1778 let mut grid = Table::grid().with_padding(0, 1).with_expand(true);
1780
1781 grid.add_column(Column::new().style(time_style).no_wrap(true));
1783
1784 let message_style = self
1786 .theme_stack
1787 .get_style("log.message")
1788 .unwrap_or_default();
1789 grid.add_column(Column::new().style(message_style).ratio(1));
1790
1791 let has_path = file.is_some();
1793 if has_path {
1794 let path_style = self
1795 .theme_stack
1796 .get_style("log.path")
1797 .unwrap_or_else(|| Style::new().with_dim(true));
1798 grid.add_column(Column::new().style(path_style).no_wrap(true));
1799 }
1800
1801 let mut cells: Vec<Box<dyn Renderable + Send + Sync>> = vec![Box::new(time_text)];
1803
1804 let options = self.options.clone();
1807 let temp_console = Console::<Stdout>::with_options(options.clone());
1808 let segments = renderable.render(&temp_console, &options);
1809
1810 let mut message_text = Text::plain("");
1812 for seg in segments.iter() {
1813 if seg.control.is_none() {
1814 message_text.append(&*seg.text, seg.style);
1815 }
1816 }
1817 cells.push(Box::new(message_text));
1818
1819 if let Some(f) = file {
1821 let filename = f.rsplit(['/', '\\']).next().unwrap_or(f);
1823 let path_text = if let Some(l) = line {
1824 Text::plain(format!("{}:{}", filename, l))
1825 } else {
1826 Text::plain(filename)
1827 };
1828 cells.push(Box::new(path_text));
1829 }
1830
1831 grid.add_row(Row::new(cells));
1832
1833 self.print(&grid, None, None, None, false, "\n")
1835 }
1836
1837 pub fn rule(&mut self, title: Option<&str>) -> io::Result<()> {
1839 if self.quiet {
1840 return Ok(());
1841 }
1842
1843 let width = self.width();
1844 match title {
1845 Some(t) => {
1846 let title_width = crate::cells::cell_len(t);
1848 let padding = (width.saturating_sub(title_width + 2)) / 2;
1849 let line: String = "─".repeat(padding);
1850 writeln!(self.writer, "{} {} {}", line, t, line)?;
1851 }
1852 None => {
1853 let line: String = "─".repeat(width);
1854 writeln!(self.writer, "{}", line)?;
1855 }
1856 }
1857 self.writer.flush()
1858 }
1859
1860 pub fn line(&mut self, count: usize) -> io::Result<()> {
1862 if self.quiet {
1863 return Ok(());
1864 }
1865
1866 if self.is_terminal() && !self.is_dumb_terminal() && self.has_live() {
1867 for _ in 0..count {
1868 self.print(&Text::plain(""), None, None, None, false, "\n")?;
1869 }
1870 return Ok(());
1871 }
1872
1873 for _ in 0..count {
1874 writeln!(self.writer)?;
1875 }
1876 self.writer.flush()
1877 }
1878
1879 pub fn clear(&mut self) -> io::Result<()> {
1885 if !self.is_terminal() {
1886 return Ok(());
1887 }
1888 execute!(self.writer, ct::Clear(ClearType::All))?;
1889 execute!(self.writer, cursor::MoveTo(0, 0))?;
1890 self.writer.flush()
1891 }
1892
1893 pub fn clear_line(&mut self) -> io::Result<()> {
1895 if !self.is_terminal() {
1896 return Ok(());
1897 }
1898 execute!(self.writer, ct::Clear(ClearType::CurrentLine))?;
1899 self.writer.flush()
1900 }
1901
1902 pub fn move_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
1904 if !self.is_terminal() {
1905 return Ok(());
1906 }
1907 execute!(self.writer, cursor::MoveTo(x, y))?;
1908 self.writer.flush()
1909 }
1910
1911 pub fn show_cursor(&mut self, show: bool) -> io::Result<bool> {
1913 if !self.is_terminal() {
1914 return Ok(false);
1915 }
1916 if show {
1917 execute!(self.writer, cursor::Show)?;
1918 } else {
1919 execute!(self.writer, cursor::Hide)?;
1920 }
1921 self.writer.flush()?;
1922 Ok(true)
1923 }
1924
1925 pub fn enter_alt_screen(&mut self) -> io::Result<bool> {
1930 if !self.is_terminal() || self.legacy_windows {
1931 return Ok(false);
1932 }
1933 self.set_alt_screen(true)
1934 }
1935
1936 pub fn leave_alt_screen(&mut self) -> io::Result<bool> {
1938 if !self.is_terminal() || !self.is_alt_screen {
1939 return Ok(false);
1940 }
1941 self.set_alt_screen(false)
1942 }
1943
1944 pub fn is_alt_screen(&self) -> bool {
1946 self.is_alt_screen
1947 }
1948
1949 pub fn set_alt_screen(&mut self, enable: bool) -> io::Result<bool> {
1954 if !self.is_terminal() || self.legacy_windows {
1955 return Ok(false);
1956 }
1957 if enable == self.is_alt_screen {
1958 return Ok(false);
1959 }
1960
1961 let mut segs = Segments::new();
1962 if enable {
1963 segs.push(Segment::control(ControlType::EnableAltScreen));
1964 segs.push(Segment::control(ControlType::Home));
1965 self.is_alt_screen = true;
1966 } else {
1967 segs.push(Segment::control(ControlType::DisableAltScreen));
1968 self.is_alt_screen = false;
1969 }
1970 self.print_segments(&segs)?;
1971 Ok(true)
1972 }
1973
1974 pub fn screen(
1995 &mut self,
1996 hide_cursor: bool,
1997 style: Option<Style>,
1998 ) -> io::Result<crate::screen_context::ScreenContext<'_, W>> {
1999 crate::screen_context::ScreenContext::new(self, hide_cursor, style)
2000 }
2001
2002 pub fn set_window_title(&mut self, title: &str) -> io::Result<bool> {
2004 if !self.is_terminal() {
2005 return Ok(false);
2006 }
2007 execute!(self.writer, ct::SetTitle(title))?;
2008 self.writer.flush()?;
2009 Ok(true)
2010 }
2011
2012 pub fn bell(&mut self) -> io::Result<()> {
2014 write!(self.writer, "\x07")?;
2015 self.writer.flush()
2016 }
2017
2018 pub fn live_start(
2023 &mut self,
2024 renderable: Box<dyn crate::Renderable + Send + Sync>,
2025 vertical_overflow: crate::live::VerticalOverflowMethod,
2026 ) -> (usize, bool) {
2027 let is_root = self.live.stack.is_empty();
2028 let id = self.live.next_id;
2029 self.live.next_id += 1;
2030
2031 self.live.entries.insert(
2032 id,
2033 LiveEntry {
2034 renderable,
2035 vertical_overflow: vertical_overflow.into(),
2036 },
2037 );
2038 self.live.stack.push(id);
2039 (id, is_root)
2040 }
2041
2042 pub fn live_update(&mut self, id: usize, renderable: Box<dyn crate::Renderable + Send + Sync>) {
2043 if let Some(entry) = self.live.entries.get_mut(&id) {
2044 entry.renderable = renderable;
2045 }
2046 }
2047
2048 pub fn live_set_vertical_overflow(
2049 &mut self,
2050 id: usize,
2051 vertical_overflow: crate::live::VerticalOverflowMethod,
2052 ) {
2053 if let Some(entry) = self.live.entries.get_mut(&id) {
2054 entry.vertical_overflow = vertical_overflow.into();
2055 }
2056 }
2057
2058 pub fn live_stop(&mut self, id: usize) -> Option<Box<dyn crate::Renderable + Send + Sync>> {
2059 self.live.stack.retain(|&x| x != id);
2060 let entry = self.live.entries.remove(&id);
2061 if self.live.stack.is_empty() {
2062 self.live.shape = None;
2063 self.live.buffer = None;
2064 }
2065 entry.map(|e| e.renderable)
2066 }
2067
2068 pub fn live_clear(&mut self) {
2069 self.live.stack.clear();
2070 self.live.entries.clear();
2071 self.live.shape = None;
2072 self.live.buffer = None;
2073 }
2074
2075 fn has_live(&self) -> bool {
2076 !self.live.stack.is_empty()
2077 }
2078
2079 fn live_root(&self) -> Option<&LiveEntry> {
2080 let id = *self.live.stack.first()?;
2081 self.live.entries.get(&id)
2082 }
2083
2084 pub(crate) fn live_position_cursor(&self) -> Segments {
2085 let Some((_, height)) = self.live.shape else {
2086 return Segments::new();
2087 };
2088 if height == 0 {
2089 return Segments::new();
2090 }
2091 let mut controls = Vec::new();
2092 controls.push(Segment::control(ControlType::CarriageReturn));
2093 controls.push(Segment::control(ControlType::EraseInLine(2)));
2094 for _ in 0..height.saturating_sub(1) {
2095 controls.push(Segment::control(ControlType::CursorUp(1)));
2096 controls.push(Segment::control(ControlType::CarriageReturn));
2097 controls.push(Segment::control(ControlType::EraseInLine(2)));
2098 }
2099 Segments::from_iter(controls)
2100 }
2101
2102 pub(crate) fn live_position_cursor_no_erase(&self) -> Segments {
2103 let Some((_, height)) = self.live.shape else {
2104 return Segments::new();
2105 };
2106 if height == 0 {
2107 return Segments::new();
2108 }
2109 let mut controls = Vec::new();
2110 controls.push(Segment::control(ControlType::CarriageReturn));
2111 for _ in 0..height.saturating_sub(1) {
2112 controls.push(Segment::control(ControlType::CursorUp(1)));
2113 }
2114 Segments::from_iter(controls)
2115 }
2116
2117 pub(crate) fn live_restore_cursor(&self) -> Segments {
2118 let Some((_, height)) = self.live.shape else {
2119 return Segments::new();
2120 };
2121 if height == 0 {
2122 return Segments::new();
2123 }
2124 let mut controls = Vec::new();
2125 controls.push(Segment::control(ControlType::CarriageReturn));
2126 for _ in 0..height {
2127 controls.push(Segment::control(ControlType::CursorUp(1)));
2128 controls.push(Segment::control(ControlType::CarriageReturn));
2129 controls.push(Segment::control(ControlType::EraseInLine(2)));
2130 }
2131 Segments::from_iter(controls)
2132 }
2133
2134 fn render_live_segments(&mut self, options: &ConsoleOptions) -> (Segments, bool) {
2135 let root = match self.live_root() {
2136 Some(root) => root,
2137 None => return (Segments::new(), false),
2138 };
2139
2140 let mut lines: Vec<Vec<Segment>> = Vec::new();
2141 for id in self.live.stack.iter() {
2142 if let Some(entry) = self.live.entries.get(id) {
2143 let mut rendered =
2144 self.render_lines(entry.renderable.as_ref(), Some(options), None, false, false);
2145 lines.append(&mut rendered);
2146 }
2147 }
2148
2149 let max_height = options.size.1;
2150 if max_height > 0 && lines.len() > max_height {
2151 match root.vertical_overflow {
2152 LiveVerticalOverflow::Visible => {}
2153 LiveVerticalOverflow::Crop => {
2154 lines.truncate(max_height);
2155 }
2156 LiveVerticalOverflow::Ellipsis => {
2157 lines.truncate(max_height.saturating_sub(1));
2158 let style = options.get_style("live.ellipsis").unwrap_or_default();
2159 let ellipsis = Text::styled("...", style).center(options.max_width);
2160 let ellipsis_lines =
2161 self.render_lines(&ellipsis, Some(options), None, false, false);
2162 if let Some(first) = ellipsis_lines.into_iter().next() {
2163 lines.push(first);
2164 }
2165 }
2166 }
2167 }
2168
2169 let shape = Segment::get_shape(&lines);
2170 self.live.shape = Some(shape);
2171
2172 let width = options.max_width.max(1);
2173 let height = shape.1.max(1);
2174 let current_buffer = ScreenBuffer::from_lines(&lines, width, height, None);
2175
2176 let use_diff = self.live.buffer.as_ref().is_some_and(|previous| {
2177 previous.width == current_buffer.width && previous.height == current_buffer.height
2178 });
2179
2180 if use_diff {
2181 let previous = self.live.buffer.as_ref().expect("checked above");
2182 let diff = current_buffer.diff_to_segments_from_origin(previous);
2183 self.live.buffer = Some(current_buffer);
2184 return (diff, false);
2185 }
2186
2187 self.live.buffer = Some(current_buffer);
2188
2189 let mut out = Segments::new();
2190 let new_line = Segment::line();
2191 for (i, line) in lines.into_iter().enumerate() {
2192 for seg in line {
2193 out.push(seg);
2194 }
2195 if i + 1 < shape.1 {
2196 out.push(new_line.clone());
2197 }
2198 }
2199 (out, true)
2200 }
2201
2202 pub fn input(&mut self, prompt: &Text, password: bool) -> io::Result<String> {
2236 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
2237 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
2238
2239 self.print(prompt, None, None, None, false, "")?;
2241 self.writer.flush()?;
2242
2243 if password && self.is_terminal() && !self.is_dumb_terminal() {
2245 enable_raw_mode()?;
2246
2247 let result = (|| -> io::Result<String> {
2248 let mut input = String::new();
2249
2250 loop {
2251 if let Event::Key(KeyEvent {
2252 code, modifiers, ..
2253 }) = event::read()?
2254 {
2255 match code {
2256 KeyCode::Enter => {
2257 write!(self.writer, "\r\n")?;
2259 self.writer.flush()?;
2260 return Ok(input);
2261 }
2262 KeyCode::Backspace => {
2263 input.pop();
2264 }
2265 KeyCode::Char(c) => {
2266 if c == 'c' && modifiers.contains(KeyModifiers::CONTROL) {
2268 write!(self.writer, "\r\n")?;
2269 self.writer.flush()?;
2270 return Err(io::Error::new(
2271 io::ErrorKind::Interrupted,
2272 "Input cancelled",
2273 ));
2274 }
2275 if c == 'd' && modifiers.contains(KeyModifiers::CONTROL) {
2277 write!(self.writer, "\r\n")?;
2278 self.writer.flush()?;
2279 return Err(io::Error::new(
2280 io::ErrorKind::UnexpectedEof,
2281 "EOF",
2282 ));
2283 }
2284 input.push(c);
2285 }
2286 KeyCode::Esc => {
2287 write!(self.writer, "\r\n")?;
2289 self.writer.flush()?;
2290 return Err(io::Error::new(
2291 io::ErrorKind::Interrupted,
2292 "Input cancelled",
2293 ));
2294 }
2295 _ => {}
2296 }
2297 }
2298 }
2299 })();
2300
2301 let _ = disable_raw_mode();
2303 result
2304 } else {
2305 let mut input = String::new();
2307 io::stdin().read_line(&mut input)?;
2308 if input.ends_with('\n') {
2310 input.pop();
2311 if input.ends_with('\r') {
2312 input.pop();
2313 }
2314 }
2315 Ok(input)
2316 }
2317 }
2318
2319 pub fn measure<R: Renderable + ?Sized>(
2334 &self,
2335 renderable: &R,
2336 options: Option<&ConsoleOptions>,
2337 ) -> crate::measure::Measurement {
2338 let measure_opts = options.cloned().unwrap_or_else(|| self.options.clone());
2340
2341 let temp_console = Console::<Stdout>::with_options(measure_opts.clone());
2344 renderable.measure(&temp_console, &measure_opts)
2345 }
2346
2347 pub fn out(
2356 &mut self,
2357 text: &str,
2358 style: Option<Style>,
2359 _highlight: Option<bool>,
2360 ) -> io::Result<()> {
2361 if self.quiet {
2362 return Ok(());
2363 }
2364 self.print(
2365 &Text::plain(text),
2366 style,
2367 None,
2368 Some(OverflowMethod::Ignore),
2369 true, "\n",
2371 )
2372 }
2373
2374 pub fn export_text(&self, clear: bool, styles: bool) -> String {
2379 let mut buffer = self.record_buffer.lock().unwrap();
2380 let text = if styles {
2381 buffer
2382 .iter()
2383 .filter(|s| s.control.is_none())
2384 .map(|s| {
2385 if let Some(style) = s.style {
2386 if let Some(color_system) = self.color_system {
2387 style.render(&s.text, color_system)
2388 } else {
2389 s.text.to_string()
2390 }
2391 } else {
2392 s.text.to_string()
2393 }
2394 })
2395 .collect::<String>()
2396 } else {
2397 buffer
2398 .iter()
2399 .filter(|s| s.control.is_none())
2400 .map(|s| s.text.to_string())
2401 .collect::<String>()
2402 };
2403 if clear {
2404 buffer.clear();
2405 }
2406 text
2407 }
2408
2409 pub fn save_text(&self, path: &str, clear: bool, styles: bool) -> io::Result<()> {
2411 let text = self.export_text(clear, styles);
2412 std::fs::write(path, text)
2413 }
2414
2415 pub fn push_render_hook(&mut self, hook: Box<dyn Fn(&Segments) -> Segments + Send + Sync>) {
2417 self.render_hooks.push(hook);
2418 }
2419
2420 pub fn pop_render_hook(&mut self) {
2422 self.render_hooks.pop();
2423 }
2424
2425 pub fn status(
2430 &self,
2431 status: &str,
2432 spinner: Option<&str>,
2433 spinner_style: Option<Style>,
2434 speed: Option<f64>,
2435 refresh_per_second: Option<f64>,
2436 ) -> crate::status::Status {
2437 crate::status::Status::with_options(
2438 status,
2439 spinner.unwrap_or("dots"),
2440 spinner_style,
2441 speed.unwrap_or(1.0),
2442 refresh_per_second.unwrap_or(12.5),
2443 )
2444 }
2445
2446 pub fn print_json(
2450 &mut self,
2451 json: &str,
2452 indent: usize,
2453 highlight: bool,
2454 sort_keys: bool,
2455 ) -> io::Result<()> {
2456 let json_renderable = crate::json::Json::new(json, indent, highlight, sort_keys);
2457 self.print(&json_renderable, None, None, None, true, "\n")
2458 }
2459}
2460
2461impl Console<Stdout> {
2464 pub fn render<R: Renderable + ?Sized>(&self, renderable: &R) -> Segments {
2466 self.apply_render_hooks(renderable.render(self, &self.options))
2467 }
2468
2469 pub fn render_with_options<R: Renderable + ?Sized>(
2471 &self,
2472 renderable: &R,
2473 options: &ConsoleOptions,
2474 ) -> Segments {
2475 self.apply_render_hooks(renderable.render(self, options))
2476 }
2477
2478 pub fn update_screen_lines(
2482 &mut self,
2483 lines: &[Vec<Segment>],
2484 x: u16,
2485 y: u16,
2486 ) -> io::Result<()> {
2487 if !self.is_alt_screen() {
2488 return Err(io::Error::new(
2489 io::ErrorKind::Other,
2490 "Alt screen must be enabled to call update_screen_lines",
2491 ));
2492 }
2493
2494 let mut segments = Segments::new();
2495 for (offset, line) in lines.iter().enumerate() {
2496 segments.push(Segment::control(ControlType::MoveTo {
2497 x,
2498 y: y.saturating_add(offset as u16),
2499 }));
2500 segments.extend(line.iter().cloned());
2501 }
2502 self.print_segments(&segments)?;
2503 Ok(())
2504 }
2505}
2506
2507#[derive(Debug, Clone, Default)]
2513pub struct PagerOptions {
2514 pub styles: bool,
2516}
2517
2518impl PagerOptions {
2519 pub fn new() -> Self {
2521 Self::default()
2522 }
2523
2524 pub fn with_styles(mut self, styles: bool) -> Self {
2526 self.styles = styles;
2527 self
2528 }
2529}
2530
2531pub struct PagerContext {
2549 buffer: Vec<u8>,
2551 options: PagerOptions,
2553 console_options: ConsoleOptions,
2555}
2556
2557impl PagerContext {
2558 fn new(console_options: ConsoleOptions, options: Option<PagerOptions>) -> Self {
2560 let options = options.unwrap_or_default();
2561 let mut console_options = console_options;
2563 if !options.styles {
2564 console_options.is_terminal = false;
2565 console_options.color_system = None;
2566 }
2567 Self {
2568 buffer: Vec::new(),
2569 options,
2570 console_options,
2571 }
2572 }
2573
2574 pub fn print_text(&mut self, text: &str) -> io::Result<()> {
2576 writeln!(self.buffer, "{}", text)
2577 }
2578
2579 pub fn print<R: crate::Renderable + ?Sized>(
2581 &mut self,
2582 renderable: &R,
2583 style: Option<Style>,
2584 justify: Option<JustifyMethod>,
2585 overflow: Option<OverflowMethod>,
2586 no_wrap: bool,
2587 end: &str,
2588 ) -> io::Result<()> {
2589 let mut console = Console::with_writer(Vec::new(), self.console_options.clone());
2591
2592 console.print(renderable, style, justify, overflow, no_wrap, end)?;
2594
2595 self.buffer.extend_from_slice(console.get_captured_bytes());
2597 Ok(())
2598 }
2599
2600 pub fn get_buffer(&self) -> &[u8] {
2602 &self.buffer
2603 }
2604
2605 pub fn get_buffer_string(&self) -> String {
2607 String::from_utf8_lossy(&self.buffer).to_string()
2608 }
2609
2610 pub fn show(&self) -> io::Result<()> {
2612 use crate::pager::{Pager, SystemPager};
2613 let pager = SystemPager::with_styles(self.options.styles);
2614 let content = self.get_buffer_string();
2615 pager.show(&content)
2616 }
2617}
2618
2619impl Drop for PagerContext {
2620 fn drop(&mut self) {
2621 if !self.buffer.is_empty() {
2622 let _ = self.show();
2623 }
2624 }
2625}
2626
2627impl Console<Stdout> {
2628 pub fn pager(&self, options: Option<PagerOptions>) -> PagerContext {
2650 PagerContext::new(self.options.clone(), options)
2651 }
2652
2653 pub fn is_recording(&self) -> bool {
2659 self.record
2660 }
2661
2662 pub fn set_record(&mut self, record: bool) {
2667 self.record = record;
2668 }
2669
2670 pub fn clear_record_buffer(&mut self) {
2672 if let Ok(mut buffer) = self.record_buffer.lock() {
2673 buffer.clear();
2674 }
2675 }
2676
2677 pub fn get_record_buffer(&self) -> Vec<Segment> {
2681 self.record_buffer
2682 .lock()
2683 .map(|buf| buf.clone())
2684 .unwrap_or_default()
2685 }
2686
2687 pub fn export_svg(
2712 &mut self,
2713 title: &str,
2714 theme: Option<&TerminalTheme>,
2715 clear: bool,
2716 code_format: Option<&str>,
2717 font_aspect_ratio: f64,
2718 unique_id: Option<&str>,
2719 ) -> String {
2720 let theme = theme.unwrap_or(&*SVG_EXPORT_THEME);
2721 let code_format = code_format.unwrap_or(CONSOLE_SVG_FORMAT);
2722
2723 let mut classes: HashMap<String, usize> = HashMap::new();
2725 let mut style_no = 1usize;
2726
2727 let width = self.width();
2728 let char_height = 20.0;
2729 let char_width = char_height * font_aspect_ratio;
2730 let line_height = char_height * 1.22;
2731
2732 let margin_top = 1.0;
2733 let margin_right = 1.0;
2734 let margin_bottom = 1.0;
2735 let margin_left = 1.0;
2736
2737 let padding_top = 40.0;
2738 let padding_right = 8.0;
2739 let padding_bottom = 8.0;
2740 let padding_left = 8.0;
2741
2742 let padding_width = padding_left + padding_right;
2743 let padding_height = padding_top + padding_bottom;
2744 let margin_width = margin_left + margin_right;
2745 let margin_height = margin_top + margin_bottom;
2746
2747 let mut text_backgrounds: Vec<String> = Vec::new();
2748 let mut text_group: Vec<String> = Vec::new();
2749
2750 let segments: Vec<Segment> = {
2752 let mut buffer = self.record_buffer.lock().unwrap();
2753 let segments: Vec<Segment> = buffer
2754 .iter()
2755 .filter(|s| s.control.is_none())
2756 .cloned()
2757 .collect();
2758 if clear {
2759 buffer.clear();
2760 }
2761 segments
2762 };
2763
2764 let unique_id = unique_id.map(|s| s.to_string()).unwrap_or_else(|| {
2766 let content: String = segments
2767 .iter()
2768 .map(|s| format!("{:?}", s))
2769 .collect::<Vec<_>>()
2770 .join("");
2771 let hash = adler32(&format!("{}{}", content, title));
2772 format!("terminal-{}", hash)
2773 });
2774
2775 let lines =
2777 Segment::split_and_crop_lines(Segments::from_iter(segments), width, None, false, false);
2778
2779 let mut y = 0usize;
2780 for line in &lines {
2781 let mut x = 0usize;
2782
2783 for segment in line {
2784 let style = segment.style.unwrap_or_default();
2785 let rules = get_svg_style_for_segment(&style, theme);
2786
2787 if !classes.contains_key(&rules) {
2788 classes.insert(rules.clone(), style_no);
2789 style_no += 1;
2790 }
2791 let class_name = format!("r{}", classes[&rules]);
2792
2793 let has_background = if style.reverse.unwrap_or(false) {
2795 true
2796 } else {
2797 style.bgcolor.is_some() && !is_default_color(style.bgcolor)
2798 };
2799
2800 let background = if style.reverse.unwrap_or(false) {
2801 style
2802 .color
2803 .map(|c| resolve_color_for_svg(c, theme, true))
2804 .unwrap_or(theme.foreground_color)
2805 } else {
2806 style
2807 .bgcolor
2808 .map(|c| resolve_color_for_svg(c, theme, false))
2809 .unwrap_or(theme.background_color)
2810 };
2811
2812 let text_length = cell_len(&segment.text);
2813
2814 if has_background {
2815 text_backgrounds.push(make_tag(
2816 "rect",
2817 None,
2818 &[
2819 ("fill", &background.hex()),
2820 ("x", &format_number(x as f64 * char_width)),
2821 ("y", &format_number(y as f64 * line_height + 1.5)),
2822 ("width", &format_number(char_width * text_length as f64)),
2823 ("height", &format_number(line_height + 0.25)),
2824 ("shape-rendering", "crispEdges"),
2825 ],
2826 ));
2827 }
2828
2829 if !segment.text.chars().all(|c| c == ' ') {
2831 text_group.push(make_tag(
2832 "text",
2833 Some(&escape_text(&segment.text)),
2834 &[
2835 ("class", &format!("{}-{}", unique_id, class_name)),
2836 ("x", &format_number(x as f64 * char_width)),
2837 ("y", &format_number(y as f64 * line_height + char_height)),
2838 (
2839 "textLength",
2840 &format_number(char_width * text_length as f64),
2841 ),
2842 ("clip-path", &format!("url(#{}-line-{})", unique_id, y)),
2843 ],
2844 ));
2845 }
2846
2847 x += text_length;
2848 }
2849 y += 1;
2850 }
2851
2852 let line_offsets: Vec<f64> = (0..y)
2854 .map(|line_no| line_no as f64 * line_height + 1.5)
2855 .collect();
2856
2857 let lines_svg: String = line_offsets
2858 .iter()
2859 .enumerate()
2860 .map(|(line_no, offset)| {
2861 format!(
2862 r#"<clipPath id="{}-line-{}">
2863 {}
2864 </clipPath>"#,
2865 unique_id,
2866 line_no,
2867 make_tag(
2868 "rect",
2869 None,
2870 &[
2871 ("x", "0"),
2872 ("y", &format_number(*offset)),
2873 ("width", &format_number(char_width * width as f64)),
2874 ("height", &format_number(line_height + 0.25)),
2875 ],
2876 )
2877 )
2878 })
2879 .collect::<Vec<_>>()
2880 .join("\n");
2881
2882 let styles: String = classes
2884 .iter()
2885 .map(|(css, rule_no)| format!(".{}-r{} {{ {} }}", unique_id, rule_no, css))
2886 .collect::<Vec<_>>()
2887 .join("\n");
2888
2889 let backgrounds = text_backgrounds.join("");
2890 let matrix = text_group.join("\n");
2891
2892 let terminal_width = (width as f64 * char_width + padding_width).ceil();
2893 let terminal_height = (y as f64 + 1.0) * line_height + padding_height;
2894
2895 let mut chrome = make_tag(
2897 "rect",
2898 None,
2899 &[
2900 ("fill", &theme.background_color.hex()),
2901 ("stroke", "rgba(255,255,255,0.35)"),
2902 ("stroke-width", "1"),
2903 ("x", &format_number(margin_left)),
2904 ("y", &format_number(margin_top)),
2905 ("width", &format_number(terminal_width)),
2906 ("height", &format_number(terminal_height)),
2907 ("rx", "8"),
2908 ],
2909 );
2910
2911 if !title.is_empty() {
2913 chrome.push_str(&make_tag(
2914 "text",
2915 Some(&escape_text(title)),
2916 &[
2917 ("class", &format!("{}-title", unique_id)),
2918 ("fill", &theme.foreground_color.hex()),
2919 ("text-anchor", "middle"),
2920 ("x", &format_number(terminal_width / 2.0)),
2921 ("y", &format_number(margin_top + char_height + 6.0)),
2922 ],
2923 ));
2924 }
2925
2926 chrome.push_str(
2928 r##"
2929 <g transform="translate(26,22)">
2930 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
2931 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
2932 <circle cx="44" cy="0" r="7" fill="#28c840"/>
2933 </g>
2934 "##,
2935 );
2936
2937 code_format
2939 .replace("{unique_id}", &unique_id)
2940 .replace("{char_width}", &format_number(char_width))
2941 .replace("{char_height}", &format_number(char_height))
2942 .replace("{line_height}", &format_number(line_height))
2943 .replace(
2944 "{terminal_width}",
2945 &format_number(char_width * width as f64 - 1.0),
2946 )
2947 .replace(
2948 "{terminal_height}",
2949 &format_number((y as f64 + 1.0) * line_height - 1.0),
2950 )
2951 .replace("{width}", &format_number(terminal_width + margin_width))
2952 .replace("{height}", &format_number(terminal_height + margin_height))
2953 .replace("{terminal_x}", &format_number(margin_left + padding_left))
2954 .replace("{terminal_y}", &format_number(margin_top + padding_top))
2955 .replace("{styles}", &styles)
2956 .replace("{chrome}", &chrome)
2957 .replace("{backgrounds}", &backgrounds)
2958 .replace("{matrix}", &matrix)
2959 .replace("{lines}", &lines_svg)
2960 }
2961
2962 pub fn export_html(
2988 &mut self,
2989 theme: Option<&TerminalTheme>,
2990 clear: bool,
2991 code_format: Option<&str>,
2992 ) -> String {
2993 let theme = theme.unwrap_or(&*DEFAULT_TERMINAL_THEME);
2994 let code_format = code_format.unwrap_or(CONSOLE_HTML_FORMAT);
2995
2996 let mut classes: HashMap<String, usize> = HashMap::new();
2998 let mut style_no = 1usize;
2999
3000 let segments: Vec<Segment> = {
3002 let mut buffer = self.record_buffer.lock().unwrap();
3003 let segments: Vec<Segment> = buffer
3004 .iter()
3005 .filter(|s| s.control.is_none())
3006 .cloned()
3007 .collect();
3008 if clear {
3009 buffer.clear();
3010 }
3011 segments
3012 };
3013
3014 for segment in &segments {
3016 let style = segment.style.unwrap_or_default();
3017 let rules = get_html_style_for_segment(&style, theme);
3018 if !rules.is_empty() && !classes.contains_key(&rules) {
3019 classes.insert(rules, style_no);
3020 style_no += 1;
3021 }
3022 }
3023
3024 let stylesheet: String = classes
3026 .iter()
3027 .map(|(css, rule_no)| format!(".r{} {{ {} }}", rule_no, css))
3028 .collect::<Vec<_>>()
3029 .join("\n");
3030
3031 #[derive(Debug, Clone, PartialEq, Eq)]
3033 enum OpenTag {
3034 Span {
3035 class_no: usize,
3036 },
3037 Link {
3038 class_no: Option<usize>,
3039 href: Arc<str>,
3040 },
3041 }
3042
3043 let mut code = String::new();
3044 let mut open: Option<OpenTag> = None;
3045
3046 let close_open = |code: &mut String, open: &mut Option<OpenTag>| {
3047 if let Some(tag) = open.take() {
3048 match tag {
3049 OpenTag::Span { .. } => code.push_str("</span>"),
3050 OpenTag::Link { .. } => code.push_str("</a>"),
3051 }
3052 }
3053 };
3054
3055 let open_tag = |code: &mut String, tag: &OpenTag| match tag {
3056 OpenTag::Span { class_no } => {
3057 code.push_str(&format!("<span class=\"r{}\">", class_no));
3058 }
3059 OpenTag::Link { class_no, href } => {
3060 let href_escaped = escape_html_attr(href);
3061 if let Some(class_no) = class_no {
3062 code.push_str(&format!(
3063 "<a class=\"r{}\" href=\"{}\">",
3064 class_no, href_escaped
3065 ));
3066 } else {
3067 code.push_str(&format!("<a href=\"{}\">", href_escaped));
3068 }
3069 }
3070 };
3071
3072 for segment in &segments {
3073 let text = segment.text.as_ref();
3074 if text.is_empty() {
3075 continue;
3076 }
3077
3078 let style = segment.style.unwrap_or_default();
3079 let rules = get_html_style_for_segment(&style, theme);
3080 let class_no = if rules.is_empty() {
3081 None
3082 } else {
3083 Some(classes[&rules])
3084 };
3085
3086 let href = segment.meta.as_ref().and_then(|m| m.link.as_ref()).cloned();
3087
3088 let desired: Option<OpenTag> = if let Some(href) = href {
3089 Some(OpenTag::Link { class_no, href })
3090 } else if let Some(class_no) = class_no {
3091 Some(OpenTag::Span { class_no })
3092 } else {
3093 None
3094 };
3095
3096 if desired != open {
3097 close_open(&mut code, &mut open);
3098 if let Some(tag) = &desired {
3099 open_tag(&mut code, tag);
3100 }
3101 open = desired;
3102 }
3103
3104 code.push_str(&escape_html_text(text));
3106 }
3107
3108 close_open(&mut code, &mut open);
3109
3110 code_format
3112 .replace("{stylesheet}", &stylesheet)
3113 .replace("{foreground}", &theme.foreground_color.hex())
3114 .replace("{background}", &theme.background_color.hex())
3115 .replace("{code}", &code)
3116 }
3117
3118 pub fn save_html(
3122 &mut self,
3123 path: &str,
3124 theme: Option<&TerminalTheme>,
3125 clear: bool,
3126 code_format: Option<&str>,
3127 ) -> io::Result<()> {
3128 let html = self.export_html(theme, clear, code_format);
3129 std::fs::write(path, html)
3130 }
3131
3132 pub fn save_svg(
3156 &mut self,
3157 path: &str,
3158 title: &str,
3159 theme: Option<&TerminalTheme>,
3160 clear: bool,
3161 font_aspect_ratio: f64,
3162 unique_id: Option<&str>,
3163 ) -> io::Result<()> {
3164 let svg = self.export_svg(title, theme, clear, None, font_aspect_ratio, unique_id);
3165 std::fs::write(path, svg)
3166 }
3167}
3168
3169pub(crate) fn get_svg_style_for_segment(style: &Style, theme: &TerminalTheme) -> String {
3175 let mut css_rules = Vec::new();
3176
3177 let fg_color = style
3179 .color
3180 .map(|c| resolve_color_for_svg(c, theme, true))
3181 .unwrap_or(theme.foreground_color);
3182
3183 let bg_color = style
3185 .bgcolor
3186 .map(|c| resolve_color_for_svg(c, theme, false))
3187 .unwrap_or(theme.background_color);
3188
3189 let (fg_color, bg_color) = if style.reverse.unwrap_or(false) {
3191 (bg_color, fg_color)
3192 } else {
3193 (fg_color, bg_color)
3194 };
3195
3196 let fg_color = if style.dim.unwrap_or(false) {
3198 blend_rgb_for_svg(fg_color, bg_color, 0.4)
3199 } else {
3200 fg_color
3201 };
3202
3203 css_rules.push(format!("fill: {}", fg_color.hex()));
3204
3205 if style.bold.unwrap_or(false) {
3206 css_rules.push("font-weight: bold".to_string());
3207 }
3208 if style.italic.unwrap_or(false) {
3209 css_rules.push("font-style: italic".to_string());
3210 }
3211 if style.underline.unwrap_or(false) {
3212 css_rules.push("text-decoration: underline".to_string());
3213 }
3214 if style.strike.unwrap_or(false) {
3215 css_rules.push("text-decoration: line-through".to_string());
3216 }
3217
3218 css_rules.join(";")
3219}
3220
3221fn get_html_style_for_segment(style: &Style, theme: &TerminalTheme) -> String {
3223 let mut css_rules: Vec<String> = Vec::new();
3224
3225 let fg_color = style
3227 .color
3228 .map(|c| resolve_color_for_svg(c, theme, true))
3229 .unwrap_or(theme.foreground_color);
3230
3231 let bg_color = style
3233 .bgcolor
3234 .map(|c| resolve_color_for_svg(c, theme, false))
3235 .unwrap_or(theme.background_color);
3236
3237 let (fg_color, bg_color) = if style.reverse.unwrap_or(false) {
3239 (bg_color, fg_color)
3240 } else {
3241 (fg_color, bg_color)
3242 };
3243
3244 let fg_color = if style.dim.unwrap_or(false) {
3246 blend_rgb_for_svg(fg_color, bg_color, 0.5)
3247 } else {
3248 fg_color
3249 };
3250
3251 if style.color.is_some() || style.reverse.unwrap_or(false) || style.dim.unwrap_or(false) {
3253 css_rules.push(format!("color: {}", fg_color.hex()));
3254 css_rules.push(format!("text-decoration-color: {}", fg_color.hex()));
3255 }
3256
3257 let has_background = if style.reverse.unwrap_or(false) {
3259 true
3260 } else {
3261 style.bgcolor.is_some() && !is_default_color(style.bgcolor)
3262 };
3263 if has_background {
3264 css_rules.push(format!("background-color: {}", bg_color.hex()));
3265 }
3266
3267 if style.bold.unwrap_or(false) {
3269 css_rules.push("font-weight: bold".to_string());
3270 }
3271 if style.italic.unwrap_or(false) {
3272 css_rules.push("font-style: italic".to_string());
3273 }
3274
3275 let mut decorations = Vec::new();
3276 if style.underline.unwrap_or(false) {
3277 decorations.push("underline");
3278 }
3279 if style.strike.unwrap_or(false) {
3280 decorations.push("line-through");
3281 }
3282 if !decorations.is_empty() {
3283 css_rules.push(format!("text-decoration: {}", decorations.join(" ")));
3284 }
3285
3286 css_rules.join("; ")
3287}
3288
3289pub(crate) fn resolve_color_for_svg(
3291 color: SimpleColor,
3292 theme: &TerminalTheme,
3293 is_foreground: bool,
3294) -> ColorTriplet {
3295 match color {
3296 SimpleColor::Default => {
3297 if is_foreground {
3298 theme.foreground_color
3299 } else {
3300 theme.background_color
3301 }
3302 }
3303 SimpleColor::Standard(index) => theme.get_ansi_color(index as usize),
3304 SimpleColor::EightBit(index) => {
3305 if let Some(triplet) = crate::color::EIGHT_BIT_PALETTE.get(index as usize) {
3307 triplet
3308 } else {
3309 theme.foreground_color
3310 }
3311 }
3312 SimpleColor::Rgb { r, g, b } => ColorTriplet::new(r, g, b),
3313 }
3314}
3315
3316pub(crate) fn is_default_color(color: Option<SimpleColor>) -> bool {
3318 matches!(color, None | Some(SimpleColor::Default))
3319}
3320
3321pub(crate) fn blend_rgb_for_svg(
3323 color: ColorTriplet,
3324 background: ColorTriplet,
3325 factor: f64,
3326) -> ColorTriplet {
3327 let r = (color.red as f64 + (background.red as f64 - color.red as f64) * factor) as u8;
3328 let g = (color.green as f64 + (background.green as f64 - color.green as f64) * factor) as u8;
3329 let b = (color.blue as f64 + (background.blue as f64 - color.blue as f64) * factor) as u8;
3330 ColorTriplet::new(r, g, b)
3331}
3332
3333pub(crate) fn adler32(data: &str) -> u32 {
3335 let mut a: u32 = 1;
3336 let mut b: u32 = 0;
3337 const MOD_ADLER: u32 = 65521;
3338
3339 for byte in data.bytes() {
3340 a = (a + byte as u32) % MOD_ADLER;
3341 b = (b + a) % MOD_ADLER;
3342 }
3343
3344 (b << 16) | a
3345}
3346
3347pub(crate) fn escape_text(text: &str) -> String {
3349 text.replace('&', "&")
3350 .replace('<', "<")
3351 .replace('>', ">")
3352 .replace(' ', " ")
3353}
3354
3355fn escape_html_text(text: &str) -> String {
3357 text.replace('&', "&")
3358 .replace('<', "<")
3359 .replace('>', ">")
3360}
3361
3362fn escape_html_attr(text: &str) -> String {
3364 text.replace('&', "&")
3365 .replace('<', "<")
3366 .replace('>', ">")
3367 .replace('"', """)
3368}
3369
3370pub(crate) fn format_number(value: f64) -> String {
3372 if value.fract() == 0.0 {
3373 format!("{}", value as i64)
3374 } else {
3375 format!("{:.2}", value)
3376 .trim_end_matches('0')
3377 .trim_end_matches('.')
3378 .to_string()
3379 }
3380}
3381
3382pub(crate) fn make_tag(name: &str, content: Option<&str>, attribs: &[(&str, &str)]) -> String {
3384 let attribs_str: String = attribs
3385 .iter()
3386 .map(|(k, v)| format!("{}=\"{}\"", k, v))
3387 .collect::<Vec<_>>()
3388 .join(" ");
3389
3390 if let Some(content) = content {
3391 format!("<{} {}>{}</{}>", name, attribs_str, content, name)
3392 } else {
3393 format!("<{} {}/>", name, attribs_str)
3394 }
3395}
3396
3397#[cfg(test)]
3402mod tests {
3403 use super::*;
3404 use crate::Control;
3405 use crate::StyleMeta;
3406
3407 #[test]
3410 fn test_justify_method_parse() {
3411 assert_eq!(JustifyMethod::parse("left"), Some(JustifyMethod::Left));
3412 assert_eq!(JustifyMethod::parse("CENTER"), Some(JustifyMethod::Center));
3413 assert_eq!(JustifyMethod::parse("Right"), Some(JustifyMethod::Right));
3414 assert_eq!(JustifyMethod::parse("full"), Some(JustifyMethod::Full));
3415 assert_eq!(
3416 JustifyMethod::parse("default"),
3417 Some(JustifyMethod::Default)
3418 );
3419 assert_eq!(JustifyMethod::parse("invalid"), None);
3420 }
3421
3422 #[test]
3425 fn test_overflow_method_parse() {
3426 assert_eq!(OverflowMethod::parse("fold"), Some(OverflowMethod::Fold));
3427 assert_eq!(OverflowMethod::parse("CROP"), Some(OverflowMethod::Crop));
3428 assert_eq!(
3429 OverflowMethod::parse("Ellipsis"),
3430 Some(OverflowMethod::Ellipsis)
3431 );
3432 assert_eq!(
3433 OverflowMethod::parse("ignore"),
3434 Some(OverflowMethod::Ignore)
3435 );
3436 assert_eq!(OverflowMethod::parse("invalid"), None);
3437 }
3438
3439 #[test]
3442 fn test_console_options_default() {
3443 let options = ConsoleOptions::default();
3444 assert_eq!(options.size, (80, 24));
3445 assert_eq!(options.min_width, 1);
3446 assert_eq!(options.max_width, 80);
3447 assert_eq!(options.max_height, 24);
3448 assert!(options.is_terminal);
3449 assert_eq!(options.encoding, "utf-8");
3450 }
3451
3452 #[test]
3453 fn test_console_options_ascii_only() {
3454 let options = ConsoleOptions {
3455 encoding: "utf-8".to_string(),
3456 ..Default::default()
3457 };
3458 assert!(!options.ascii_only());
3459
3460 let options = ConsoleOptions {
3461 encoding: "ascii".to_string(),
3462 ..Default::default()
3463 };
3464 assert!(options.ascii_only());
3465
3466 let options = ConsoleOptions {
3467 encoding: "latin-1".to_string(),
3468 ..Default::default()
3469 };
3470 assert!(options.ascii_only());
3471 }
3472
3473 #[test]
3474 fn test_console_options_update_width() {
3475 let options = ConsoleOptions::default();
3476 let updated = options.update_width(120);
3477 assert_eq!(updated.min_width, 120);
3478 assert_eq!(updated.max_width, 120);
3479 }
3480
3481 #[test]
3482 fn test_console_options_update_height() {
3483 let options = ConsoleOptions::default();
3484 let updated = options.update_height(40);
3485 assert_eq!(updated.max_height, 40);
3486 assert_eq!(updated.height, Some(40));
3487 }
3488
3489 #[test]
3490 fn test_console_options_update_dimensions() {
3491 let options = ConsoleOptions::default();
3492 let updated = options.update_dimensions(100, 50);
3493 assert_eq!(updated.min_width, 100);
3494 assert_eq!(updated.max_width, 100);
3495 assert_eq!(updated.max_height, 50);
3496 assert_eq!(updated.height, Some(50));
3497 }
3498
3499 #[test]
3500 fn test_console_options_reset_height() {
3501 let options = ConsoleOptions {
3502 height: Some(40),
3503 ..Default::default()
3504 };
3505 let reset = options.reset_height();
3506 assert_eq!(reset.height, None);
3507 }
3508
3509 #[test]
3512 fn test_console_capture() {
3513 let mut console = Console::capture();
3514 console.print_text("Hello, World!").unwrap();
3515 let output = console.get_captured();
3516 assert!(output.contains("Hello, World!"));
3517 }
3518
3519 #[test]
3520 fn test_console_capture_styled() {
3521 let mut console = Console::capture();
3522 let style = Style::new().with_bold(true);
3523 console.print_styled("Bold text", style).unwrap();
3524 let output = console.get_captured();
3525 assert!(output.contains("Bold text"));
3526 }
3527
3528 #[test]
3529 fn test_console_capture_clear() {
3530 let mut console = Console::capture();
3531 console.print_text("First").unwrap();
3532 console.clear_captured();
3533 console.print_text("Second").unwrap();
3534 let output = console.get_captured();
3535 assert!(!output.contains("First"));
3536 assert!(output.contains("Second"));
3537 }
3538
3539 #[test]
3540 fn test_console_capture_bytes() {
3541 let mut console = Console::capture();
3542 console.print_text("Test").unwrap();
3543 let bytes = console.get_captured_bytes();
3544 assert!(!bytes.is_empty());
3545 }
3546
3547 #[test]
3548 fn test_render_hook_runs_in_print_pipeline() {
3549 let mut console = Console::capture();
3550 console.push_render_hook(Box::new(|segments: &Segments| {
3551 Segments::from_iter(segments.iter().map(|seg| {
3552 if seg.control.is_some() {
3553 seg.clone()
3554 } else {
3555 Segment::new(seg.text.to_string().to_uppercase())
3556 }
3557 }))
3558 }));
3559
3560 console
3561 .print(&Text::plain("hooked"), None, None, None, false, "\n")
3562 .unwrap();
3563 let output = console.get_captured();
3564 assert!(output.contains("HOOKED"));
3565 }
3566
3567 #[test]
3568 fn test_render_hook_runs_for_live_renderables() {
3569 let mut console = Console::with_writer(
3570 Vec::new(),
3571 ConsoleOptions {
3572 is_terminal: true,
3573 ..Default::default()
3574 },
3575 );
3576 console.set_force_terminal(Some(true));
3577 if console.is_dumb_terminal() {
3578 return;
3579 }
3580 console.push_render_hook(Box::new(|segments: &Segments| {
3581 let mut out = Segments::new();
3582 for seg in segments.iter() {
3583 out.push(seg.clone());
3584 }
3585 out.push(Segment::new("!"));
3586 out
3587 }));
3588
3589 let (_id, _is_root) = console.live_start(
3590 Box::new(Text::plain("LIVE")),
3591 crate::live::VerticalOverflowMethod::Ellipsis,
3592 );
3593 console
3594 .print(&Control::new(), None, None, None, false, "")
3595 .unwrap();
3596
3597 let output = console.get_captured();
3598 assert!(
3599 output.contains("LIVE!"),
3600 "expected hooked live output in captured text, got: {:?}",
3601 output
3602 );
3603 }
3604
3605 #[test]
3606 fn test_console_status_honors_refresh_per_second() {
3607 let console = Console::capture();
3608 let status = console.status("Working...", None, None, None, Some(9.0));
3609 assert_eq!(status.refresh_per_second(), 9.0);
3610 }
3611
3612 #[test]
3613 fn test_live_wrap_emits_cursor_controls_after_first_render() {
3614 let mut console = Console::with_writer(
3615 Vec::new(),
3616 ConsoleOptions {
3617 is_terminal: true,
3618 ..Default::default()
3619 },
3620 );
3621 console.set_force_terminal(Some(true));
3622 if console.is_dumb_terminal() {
3623 return;
3625 }
3626
3627 let (_id, _is_root) = console.live_start(
3628 Box::new(Text::plain("LIVE")),
3629 crate::live::VerticalOverflowMethod::Ellipsis,
3630 );
3631
3632 console
3634 .print(&Text::plain("A"), None, None, None, false, "\n")
3635 .unwrap();
3636 console.clear_captured();
3637
3638 console
3642 .print(&Text::plain("B"), None, None, None, false, "\n")
3643 .unwrap();
3644 let out = console.get_captured();
3645 assert!(
3646 out.contains("\r"),
3647 "expected cursor repositioning (\\r) in second live render, got: {:?}",
3648 out,
3649 );
3650 }
3651
3652 #[test]
3653 fn test_set_alt_screen_emits_enable_and_home() {
3654 let mut console = Console::with_writer(
3655 Vec::new(),
3656 ConsoleOptions {
3657 is_terminal: true,
3658 ..Default::default()
3659 },
3660 );
3661 console.set_force_terminal(Some(true));
3662 if console.is_dumb_terminal() {
3663 return;
3665 }
3666
3667 console.set_alt_screen(true).unwrap();
3668 let out = console.get_captured();
3669 assert!(out.contains("\x1b[?1049h"));
3670 assert!(out.contains("\x1b[H"));
3671 }
3672
3673 #[test]
3674 fn test_print_segments_does_not_emit_osc8_when_not_terminal() {
3675 let mut console = Console::capture();
3676 let mut segments = Segments::new();
3677 segments.push(Segment::new_with_meta(
3678 "X",
3679 StyleMeta::with_link("https://example.com"),
3680 ));
3681 console.print_segments(&segments).unwrap();
3682 let out = console.get_captured();
3683 assert!(out.contains("X"));
3684 assert!(!out.contains("\x1b]8;"));
3685 }
3686
3687 #[test]
3688 fn test_print_segments_emits_osc8_for_segment_meta_link() {
3689 let mut console = Console::with_writer(
3690 Vec::new(),
3691 ConsoleOptions {
3692 is_terminal: true,
3693 ..Default::default()
3694 },
3695 );
3696 console.set_force_terminal(Some(true));
3697 if console.is_dumb_terminal() {
3698 return;
3700 }
3701
3702 let mut segments = Segments::new();
3703 segments.push(Segment::new_with_meta(
3704 "X",
3705 StyleMeta::with_link("https://example.com"),
3706 ));
3707 console.print_segments(&segments).unwrap();
3708 let out = console.get_captured();
3709 assert!(out.contains("\x1b]8;"));
3710 assert!(out.contains("https://example.com"));
3711 assert!(out.contains("\x1b]8;;\x1b\\"));
3712 }
3713
3714 #[test]
3715 fn test_print_segments_osc8_link_id_stable_per_console() {
3716 let mut console = Console::with_writer(
3717 Vec::new(),
3718 ConsoleOptions {
3719 is_terminal: true,
3720 ..Default::default()
3721 },
3722 );
3723 console.set_force_terminal(Some(true));
3724 if console.is_dumb_terminal() {
3725 return;
3726 }
3727
3728 let mut segments = Segments::new();
3729 segments.push(Segment::new_with_meta(
3730 "X",
3731 StyleMeta::with_link("https://example.com"),
3732 ));
3733
3734 console.print_segments(&segments).unwrap();
3735 let out1 = console.get_captured();
3736 assert!(out1.contains("id=richrs-1;https://example.com"));
3737
3738 console.clear_captured();
3739 console.print_segments(&segments).unwrap();
3740 let out2 = console.get_captured();
3741 assert!(out2.contains("id=richrs-1;https://example.com"));
3742 }
3743
3744 #[test]
3745 fn test_print_text_from_ansi_emits_osc8_lifecycle_from_style_meta() {
3746 let mut console = Console::with_writer(
3747 Vec::new(),
3748 ConsoleOptions {
3749 is_terminal: true,
3750 ..Default::default()
3751 },
3752 );
3753 console.set_force_terminal(Some(true));
3754 if console.is_dumb_terminal() {
3755 return;
3756 }
3757
3758 let text = Text::from_ansi("\x1b]8;id=src42;https://example.com\x07Link\x1b]8;;\x07 done");
3759 console.print(&text, None, None, None, false, "").unwrap();
3760 let out = console.get_captured();
3761
3762 let open = "\x1b]8;id=src42;https://example.com\x1b\\";
3763 let close = "\x1b]8;;\x1b\\";
3764 assert!(out.contains(open));
3765 assert!(out.contains(close));
3766
3767 let open_pos = out.find(open).unwrap();
3768 let link_text_pos = out.find("Link").unwrap();
3769 let close_pos = out.find(close).unwrap();
3770 let plain_pos = out.rfind(" done").unwrap();
3771 assert!(open_pos < link_text_pos);
3772 assert!(link_text_pos < close_pos);
3773 assert!(close_pos < plain_pos);
3774 }
3775
3776 #[test]
3777 fn test_print_segments_closes_hyperlink_before_tail_reset() {
3778 let mut console = Console::with_writer(
3779 Vec::new(),
3780 ConsoleOptions {
3781 is_terminal: true,
3782 ..Default::default()
3783 },
3784 );
3785 console.set_force_terminal(Some(true));
3786 if console.is_dumb_terminal() {
3787 return;
3788 }
3789
3790 let mut segments = Segments::new();
3791 segments.push(Segment::styled_with_meta(
3792 "X",
3793 Style::new().with_bold(true),
3794 StyleMeta::with_link("https://example.com"),
3795 ));
3796
3797 console.print_segments(&segments).unwrap();
3798 let out = console.get_captured();
3799 let close_pos = out.find("\x1b]8;;\x1b\\").unwrap();
3800 let reset_pos = out.rfind("\x1b[0m").unwrap();
3801 assert!(close_pos < reset_pos);
3802 }
3803
3804 #[test]
3805 fn test_parse_windows_render_mode_defaults_to_streaming() {
3806 assert_eq!(
3807 parse_windows_render_mode(None),
3808 WindowsRenderMode::Streaming
3809 );
3810 assert_eq!(
3811 parse_windows_render_mode(Some("invalid")),
3812 WindowsRenderMode::Streaming
3813 );
3814 }
3815
3816 #[test]
3817 fn test_parse_windows_render_mode_values() {
3818 assert_eq!(
3819 parse_windows_render_mode(Some("segment")),
3820 WindowsRenderMode::Segment
3821 );
3822 assert_eq!(
3823 parse_windows_render_mode(Some("streaming")),
3824 WindowsRenderMode::Streaming
3825 );
3826 assert_eq!(
3827 parse_windows_render_mode(Some(" StReAmInG ")),
3828 WindowsRenderMode::Streaming
3829 );
3830 }
3831
3832 #[test]
3835 fn test_console_width() {
3836 let console = Console::capture_with_options(ConsoleOptions {
3837 max_width: 120,
3838 ..Default::default()
3839 });
3840 assert_eq!(console.width(), 120);
3841 }
3842
3843 #[test]
3844 fn test_console_set_size() {
3845 let mut console = Console::capture();
3846 console.set_size(100, 50);
3847 assert_eq!(console.width(), 100);
3848 assert_eq!(console.height(), 50);
3849 assert_eq!(console.size(), (100, 50));
3850 }
3851
3852 #[test]
3853 fn test_console_force_terminal() {
3854 let mut console = Console::capture();
3855 assert!(!console.is_terminal()); console.set_force_terminal(Some(true));
3858 assert!(console.is_terminal());
3859
3860 console.set_force_terminal(Some(false));
3861 assert!(!console.is_terminal());
3862 }
3863
3864 #[test]
3865 fn test_console_quiet_mode() {
3866 let mut console = Console::capture();
3867 console.set_quiet(true);
3868 console.print_text("This should not appear").unwrap();
3869 assert!(console.get_captured().is_empty());
3870 }
3871
3872 #[test]
3873 fn test_console_markup_emoji_highlight() {
3874 let mut console = Console::capture();
3875
3876 assert!(console.is_markup_enabled());
3877 console.set_markup_enabled(false);
3878 assert!(!console.is_markup_enabled());
3879
3880 assert!(console.is_emoji_enabled());
3881 console.set_emoji_enabled(false);
3882 assert!(!console.is_emoji_enabled());
3883
3884 assert!(console.is_highlight_enabled());
3885 console.set_highlight_enabled(false);
3886 assert!(!console.is_highlight_enabled());
3887 }
3888
3889 #[test]
3890 fn test_console_tab_size() {
3891 let mut console = Console::capture();
3892 assert_eq!(console.tab_size(), 8);
3893 console.set_tab_size(4);
3894 assert_eq!(console.tab_size(), 4);
3895 }
3896
3897 #[test]
3898 fn test_console_encoding() {
3899 let mut console = Console::capture();
3900 assert_eq!(console.encoding(), "utf-8");
3901
3902 console.set_encoding("latin-1");
3903 assert_eq!(console.encoding(), "latin-1");
3904 assert_eq!(console.options().encoding, "latin-1");
3905 }
3906
3907 #[test]
3910 fn test_console_render_text() {
3911 let console = Console::with_options(ConsoleOptions::default());
3913 let text = Text::plain("Hello, World!");
3914 let segments = console.render(&text);
3915 assert!(!segments.is_empty());
3916
3917 let combined: String = segments.iter().map(|s| s.text.to_string()).collect();
3918 assert_eq!(combined, "Hello, World!");
3919 }
3920
3921 #[test]
3922 fn test_console_render_str() {
3923 let console = Console::capture();
3924 let text = console.render_str("Hello", None, None, None, None);
3925 assert_eq!(text.plain_text(), "Hello");
3926 }
3927
3928 #[test]
3929 fn test_console_render_str_with_emoji() {
3930 let console = Console::capture();
3931 let text = console.render_str(":smile:", None, Some(true), None, None);
3932 assert!(!text.plain_text().is_empty());
3934 }
3935
3936 #[test]
3939 fn test_console_print_renderable() {
3940 let mut console = Console::capture();
3941 let text = Text::plain("Hello");
3942 console.print(&text, None, None, None, false, "\n").unwrap();
3943 let output = console.get_captured();
3944 assert!(output.contains("Hello"));
3945 }
3946
3947 #[test]
3948 fn test_console_print_with_style() {
3949 let mut console = Console::capture();
3950 let text = Text::plain("Styled");
3951 let style = Style::new().with_bold(true);
3952 console
3953 .print(&text, Some(style), None, None, false, "\n")
3954 .unwrap();
3955 let output = console.get_captured();
3956 assert!(output.contains("Styled"));
3957 }
3958
3959 #[test]
3960 fn test_console_rule() {
3961 let mut console = Console::capture();
3962 console.rule(None).unwrap();
3963 let output = console.get_captured();
3964 assert!(output.contains("─"));
3965 }
3966
3967 #[test]
3968 fn test_console_rule_with_title() {
3969 let mut console = Console::capture();
3970 console.rule(Some("Title")).unwrap();
3971 let output = console.get_captured();
3972 assert!(output.contains("Title"));
3973 assert!(output.contains("─"));
3974 }
3975
3976 #[test]
3977 fn test_console_line() {
3978 let mut console = Console::capture();
3979 console.line(3).unwrap();
3980 let output = console.get_captured();
3981 assert_eq!(output.matches('\n').count(), 3);
3982 }
3983
3984 #[test]
3987 fn test_console_measure() {
3988 let console = Console::capture();
3989 let text = Text::plain("Hello World");
3990 let measurement = console.measure(&text, None);
3991 assert!(measurement.minimum > 0);
3992 assert!(measurement.maximum >= measurement.minimum);
3993 }
3994
3995 #[test]
3998 fn test_console_alt_screen_tracking() {
3999 let console = Console::capture();
4000 assert!(!console.is_alt_screen());
4001 }
4003
4004 #[test]
4007 fn test_console_theme_stack() {
4008 let mut console = Console::capture();
4009 let theme = Theme::default();
4010 console.push_theme(theme.clone());
4011 assert!(console.pop_theme().is_ok());
4012 }
4013
4014 #[test]
4017 fn test_color_system_detection_no_terminal() {
4018 let result = Console::<Stdout>::detect_color_system_static(false);
4019 assert!(result.is_none());
4020 }
4021
4022 #[test]
4025 fn test_console_setters_sync_to_options() {
4026 let mut console = Console::capture();
4027
4028 console.set_markup_enabled(false);
4030 assert!(!console.is_markup_enabled());
4031 assert!(!console.options().markup_enabled);
4032
4033 console.set_emoji_enabled(false);
4035 assert!(!console.is_emoji_enabled());
4036 assert!(!console.options().emoji_enabled);
4037
4038 console.set_highlight_enabled(false);
4040 assert!(!console.is_highlight_enabled());
4041 assert!(!console.options().highlight_enabled);
4042
4043 console.set_tab_size(4);
4045 assert_eq!(console.tab_size(), 4);
4046 assert_eq!(console.options().tab_size, 4);
4047
4048 console.set_encoding("cp1252");
4050 assert_eq!(console.encoding(), "cp1252");
4051 assert_eq!(console.options().encoding, "cp1252");
4052
4053 console.set_color_system(Some(ColorSystem::TrueColor));
4055 assert_eq!(console.color_system(), Some(ColorSystem::TrueColor));
4056 assert_eq!(console.options().color_system, Some(ColorSystem::TrueColor));
4057 }
4058
4059 #[test]
4060 fn test_console_options_with_state() {
4061 let mut console = Console::capture();
4062
4063 console.set_markup_enabled(false);
4065 console.set_tab_size(2);
4066
4067 let opts = console.options_with_state();
4069 assert!(!opts.markup_enabled);
4070 assert_eq!(opts.tab_size, 2);
4071 }
4072
4073 #[test]
4074 fn test_with_options_initializes_from_options() {
4075 let mut options = ConsoleOptions::default();
4077 options.markup_enabled = false;
4078 options.emoji_enabled = false;
4079 options.tab_size = 4;
4080 options.encoding = "ascii".to_string();
4081 options.color_system = Some(ColorSystem::Standard);
4082
4083 let console = Console::with_options(options);
4085
4086 assert!(!console.is_markup_enabled());
4088 assert!(!console.is_emoji_enabled());
4089 assert_eq!(console.tab_size(), 4);
4090 assert_eq!(console.encoding(), "ascii");
4091 assert_eq!(console.color_system(), Some(ColorSystem::Standard));
4092 }
4093
4094 #[test]
4095 fn test_sync_from_options() {
4096 let mut console = Console::capture();
4097
4098 console.options_mut().markup_enabled = false;
4100 console.options_mut().tab_size = 2;
4101
4102 assert!(console.is_markup_enabled()); assert_eq!(console.tab_size(), 8); console.sync_from_options();
4108
4109 assert!(!console.is_markup_enabled());
4111 assert_eq!(console.tab_size(), 2);
4112 }
4113
4114 #[test]
4115 fn test_sync_theme_to_options() {
4116 let mut console = Console::capture();
4117
4118 let mut custom_theme = Theme::empty();
4120 custom_theme.add_style("direct.style", Style::new().with_italic(true));
4121 console.theme_stack_mut().push_theme(custom_theme);
4122
4123 assert!(console.theme_stack().get_style("direct.style").is_some());
4125 assert!(
4126 console
4127 .options()
4128 .theme_stack
4129 .get_style("direct.style")
4130 .is_none()
4131 ); console.sync_theme_to_options();
4135
4136 assert!(
4138 console
4139 .options()
4140 .theme_stack
4141 .get_style("direct.style")
4142 .is_some()
4143 );
4144 }
4145
4146 #[test]
4147 fn test_nested_renderable_gets_state() {
4148 use crate::padding::Padding;
4152
4153 let mut console = Console::capture();
4154 console.set_markup_enabled(false);
4155
4156 let text = Text::plain("Hello");
4158 let padded = Padding::new(Box::new(text), 1);
4159
4160 let options = console.options().clone();
4162 let segments = padded.render(&Console::with_options(options.clone()), &options);
4163
4164 assert!(!segments.is_empty());
4166 }
4167
4168 #[test]
4169 fn test_theme_push_syncs_to_options() {
4170 let mut console = Console::capture();
4171
4172 let initial_depth = console.theme_stack().depth();
4174 let initial_opts_depth = console.options().theme_stack.depth();
4175 assert_eq!(initial_depth, initial_opts_depth);
4176
4177 let mut custom_theme = Theme::empty();
4179 custom_theme.add_style("test.style", Style::new().with_bold(true));
4180 console.push_theme(custom_theme);
4181
4182 assert_eq!(console.theme_stack().depth(), initial_depth + 1);
4184 assert_eq!(console.options().theme_stack.depth(), initial_depth + 1);
4185
4186 assert!(console.theme_stack().get_style("test.style").is_some());
4188 assert!(
4189 console
4190 .options()
4191 .theme_stack
4192 .get_style("test.style")
4193 .is_some()
4194 );
4195
4196 console.pop_theme().unwrap();
4198
4199 assert_eq!(console.theme_stack().depth(), initial_depth);
4201 assert_eq!(console.options().theme_stack.depth(), initial_depth);
4202 }
4203
4204 #[test]
4207 fn test_console_options_is_send_sync() {
4208 fn assert_send<T: Send>() {}
4209 fn assert_sync<T: Sync>() {}
4210 assert_send::<ConsoleOptions>();
4211 assert_sync::<ConsoleOptions>();
4212 }
4213
4214 #[test]
4217 fn test_pager_options_default() {
4218 let opts = PagerOptions::default();
4219 assert!(!opts.styles);
4220 }
4221
4222 #[test]
4223 fn test_pager_options_with_styles() {
4224 let opts = PagerOptions::new().with_styles(true);
4225 assert!(opts.styles);
4226 }
4227
4228 #[test]
4229 fn test_pager_context_captures_text() {
4230 let console = Console::new();
4231 let mut pager = console.pager(None);
4232
4233 pager.print_text("Hello").unwrap();
4234 pager.print_text("World").unwrap();
4235
4236 let buffer = pager.get_buffer_string();
4237 assert!(buffer.contains("Hello"));
4238 assert!(buffer.contains("World"));
4239
4240 pager.buffer.clear();
4242 }
4243
4244 #[test]
4245 fn test_pager_context_captures_renderable() {
4246 let console = Console::new();
4247 let mut pager = console.pager(None);
4248
4249 let text = Text::plain("Rendered text");
4250 pager.print(&text, None, None, None, false, "\n").unwrap();
4251
4252 let buffer = pager.get_buffer_string();
4253 assert!(buffer.contains("Rendered text"));
4254
4255 pager.buffer.clear();
4257 }
4258
4259 #[test]
4262 fn test_console_new_with_record() {
4263 let console = Console::new_with_record();
4264 assert!(console.is_recording());
4265 }
4266
4267 #[test]
4268 fn test_console_set_record() {
4269 let mut console = Console::new();
4270 assert!(!console.is_recording());
4271
4272 console.set_record(true);
4273 assert!(console.is_recording());
4274
4275 console.set_record(false);
4276 assert!(!console.is_recording());
4277 }
4278
4279 #[test]
4280 fn test_console_record_buffer() {
4281 let mut console = Console::new_with_record();
4282 console.options_mut().is_terminal = false;
4283 console.options_mut().max_width = 80;
4284
4285 console.print_text("Hello").unwrap();
4287
4288 let buffer = console.get_record_buffer();
4290 assert!(!buffer.is_empty());
4291
4292 let has_hello = buffer.iter().any(|s| s.text.contains("Hello"));
4294 assert!(has_hello, "Record buffer should contain 'Hello'");
4295
4296 console.clear_record_buffer();
4298 let buffer = console.get_record_buffer();
4299 assert!(buffer.is_empty());
4300 }
4301
4302 #[test]
4303 fn test_export_svg_basic() {
4304 let mut console = Console::new_with_record();
4305 console.options_mut().is_terminal = false;
4306 console.options_mut().max_width = 40;
4307
4308 console.print_text("Hello, World!").unwrap();
4309
4310 let svg = console.export_svg("Test", None, true, None, 0.61, None);
4311
4312 assert!(svg.contains("<svg"));
4314 assert!(svg.contains("</svg>"));
4315 assert!(svg.contains("Hello"));
4316 assert!(svg.contains("rich-terminal"));
4317
4318 let buffer = console.get_record_buffer();
4320 assert!(buffer.is_empty());
4321 }
4322
4323 #[test]
4324 fn test_export_svg_with_custom_title() {
4325 let mut console = Console::new_with_record();
4326 console.options_mut().is_terminal = false;
4327 console.options_mut().max_width = 40;
4328
4329 console.print_text("Test").unwrap();
4330
4331 let svg = console.export_svg("My Custom Title", None, true, None, 0.61, None);
4332
4333 assert!(svg.contains("My Custom Title"));
4334 }
4335
4336 #[test]
4337 fn test_export_svg_with_unique_id() {
4338 let mut console = Console::new_with_record();
4339 console.options_mut().is_terminal = false;
4340 console.options_mut().max_width = 40;
4341
4342 console.print_text("Test").unwrap();
4343
4344 let svg = console.export_svg("Test", None, true, None, 0.61, Some("my-unique-id"));
4345
4346 assert!(svg.contains("my-unique-id"));
4347 }
4348
4349 #[test]
4350 fn test_export_svg_escape_text() {
4351 let mut console = Console::new_with_record();
4352 console.options_mut().is_terminal = false;
4353 console.options_mut().max_width = 80;
4354
4355 console.print_text("<script>alert('XSS')</script>").unwrap();
4356
4357 let svg = console.export_svg("Test", None, true, None, 0.61, None);
4358
4359 assert!(svg.contains("<"));
4361 assert!(svg.contains(">"));
4362 assert!(!svg.contains("<script>"));
4363 }
4364
4365 #[test]
4366 fn test_export_svg_no_clear() {
4367 let mut console = Console::new_with_record();
4368 console.options_mut().is_terminal = false;
4369 console.options_mut().max_width = 40;
4370
4371 console.print_text("Hello").unwrap();
4372
4373 let _svg = console.export_svg("Test", None, false, None, 0.61, None);
4375
4376 let buffer = console.get_record_buffer();
4378 assert!(!buffer.is_empty());
4379 }
4380
4381 #[test]
4382 fn test_export_html_basic() {
4383 let mut console = Console::new_with_record();
4384 console.options_mut().is_terminal = false;
4385 console.options_mut().max_width = 40;
4386
4387 console.print_text("Hello, World!").unwrap();
4388
4389 let html = console.export_html(None, true, None);
4390 assert!(html.contains("<!DOCTYPE html>"));
4391 assert!(html.contains("Hello, World!"));
4392
4393 let buffer = console.get_record_buffer();
4395 assert!(buffer.is_empty());
4396 }
4397
4398 #[test]
4399 fn test_export_html_link_emits_anchor_tag() {
4400 let mut console = Console::new_with_record();
4401 console.options_mut().is_terminal = false;
4402
4403 let text =
4404 Text::from_markup("[link=https://textualize.io]Textualize.io[/link]", false).unwrap();
4405 console.print(&text, None, None, None, false, "\n").unwrap();
4406
4407 let html = console.export_html(None, true, None);
4408 assert!(html.contains("<a"));
4409 assert!(html.contains("href=\"https://textualize.io\""));
4410 assert!(html.contains("Textualize.io"));
4411 }
4412
4413 #[test]
4414 fn test_export_html_escapes_text() {
4415 let mut console = Console::new_with_record();
4416 console.options_mut().is_terminal = false;
4417
4418 console.print_text("<script>alert('XSS')</script>").unwrap();
4419 let html = console.export_html(None, true, None);
4420 assert!(html.contains("<script>"));
4421 assert!(!html.contains("<script>"));
4422 }
4423}