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 let previous_live_shape = self.live.shape;
1665 if !end.is_empty() {
1668 segments.push(Segment::new(end.to_string()));
1669 }
1670 end_to_write = "";
1671
1672 let (live_segments, full_redraw) = self.render_live_segments(&options);
1673 let mut wrapped = Segments::new();
1674 let cursor_controls = if full_redraw {
1675 self.live_position_cursor_for_shape(previous_live_shape, true)
1676 } else {
1677 self.live_position_cursor_for_shape(previous_live_shape, false)
1678 };
1679 for seg in cursor_controls.iter() {
1680 wrapped.push(seg.clone());
1681 }
1682 for seg in segments.into_iter() {
1683 wrapped.push(seg);
1684 }
1685 for seg in live_segments.into_iter() {
1686 wrapped.push(seg);
1687 }
1688 segments = wrapped;
1689 }
1690
1691 let should_disable_wrap = self.options.disable_line_wrap && atty::is(atty::Stream::Stdout);
1692 if should_disable_wrap {
1693 write!(self.writer, "\x1b[?7l")?;
1696 }
1697
1698 if self.record {
1700 if let Ok(mut buffer) = self.record_buffer.lock() {
1701 for seg in segments.iter() {
1702 buffer.push(seg.clone());
1703 }
1704 if !end_to_write.is_empty() {
1705 buffer.push(Segment::new(end_to_write.to_string()));
1706 }
1707 }
1708 }
1709
1710 let result = (|| {
1711 self.print_segments(&segments)?;
1713
1714 if !end_to_write.is_empty() {
1716 write!(self.writer, "{}", end_to_write)?;
1717 }
1718
1719 self.writer.flush()
1720 })();
1721
1722 if should_disable_wrap {
1723 let _ = write!(self.writer, "\x1b[?7h");
1725 }
1726
1727 result
1728 }
1729
1730 pub fn log<R: Renderable + ?Sized>(
1754 &mut self,
1755 renderable: &R,
1756 file: Option<&str>,
1757 line: Option<u32>,
1758 ) -> io::Result<()> {
1759 if self.quiet {
1760 return Ok(());
1761 }
1762
1763 let now = SystemTime::now();
1765 let duration = now
1766 .duration_since(SystemTime::UNIX_EPOCH)
1767 .unwrap_or_default();
1768 let secs = duration.as_secs();
1769 let hours = (secs / 3600) % 24;
1770 let minutes = (secs / 60) % 60;
1771 let seconds = secs % 60;
1772
1773 let timestamp = format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds);
1775 let time_style = self
1776 .theme_stack
1777 .get_style("log.time")
1778 .unwrap_or_else(|| Style::new().with_dim(true));
1779 let time_text = Text::styled(×tamp, time_style);
1780
1781 let mut grid = Table::grid().with_padding(0, 1).with_expand(true);
1783
1784 grid.add_column(Column::new().style(time_style).no_wrap(true));
1786
1787 let message_style = self
1789 .theme_stack
1790 .get_style("log.message")
1791 .unwrap_or_default();
1792 grid.add_column(Column::new().style(message_style).ratio(1));
1793
1794 let has_path = file.is_some();
1796 if has_path {
1797 let path_style = self
1798 .theme_stack
1799 .get_style("log.path")
1800 .unwrap_or_else(|| Style::new().with_dim(true));
1801 grid.add_column(Column::new().style(path_style).no_wrap(true));
1802 }
1803
1804 let mut cells: Vec<Box<dyn Renderable + Send + Sync>> = vec![Box::new(time_text)];
1806
1807 let options = self.options.clone();
1810 let temp_console = Console::<Stdout>::with_options(options.clone());
1811 let segments = renderable.render(&temp_console, &options);
1812
1813 let mut message_text = Text::plain("");
1815 for seg in segments.iter() {
1816 if seg.control.is_none() {
1817 message_text.append(&*seg.text, seg.style);
1818 }
1819 }
1820 cells.push(Box::new(message_text));
1821
1822 if let Some(f) = file {
1824 let filename = f.rsplit(['/', '\\']).next().unwrap_or(f);
1826 let path_text = if let Some(l) = line {
1827 Text::plain(format!("{}:{}", filename, l))
1828 } else {
1829 Text::plain(filename)
1830 };
1831 cells.push(Box::new(path_text));
1832 }
1833
1834 grid.add_row(Row::new(cells));
1835
1836 self.print(&grid, None, None, None, false, "\n")
1838 }
1839
1840 pub fn rule(&mut self, title: Option<&str>) -> io::Result<()> {
1842 if self.quiet {
1843 return Ok(());
1844 }
1845
1846 let width = self.width();
1847 match title {
1848 Some(t) => {
1849 let title_width = crate::cells::cell_len(t);
1851 let padding = (width.saturating_sub(title_width + 2)) / 2;
1852 let line: String = "─".repeat(padding);
1853 writeln!(self.writer, "{} {} {}", line, t, line)?;
1854 }
1855 None => {
1856 let line: String = "─".repeat(width);
1857 writeln!(self.writer, "{}", line)?;
1858 }
1859 }
1860 self.writer.flush()
1861 }
1862
1863 pub fn line(&mut self, count: usize) -> io::Result<()> {
1865 if self.quiet {
1866 return Ok(());
1867 }
1868
1869 if self.is_terminal() && !self.is_dumb_terminal() && self.has_live() {
1870 for _ in 0..count {
1871 self.print(&Text::plain(""), None, None, None, false, "\n")?;
1872 }
1873 return Ok(());
1874 }
1875
1876 for _ in 0..count {
1877 writeln!(self.writer)?;
1878 }
1879 self.writer.flush()
1880 }
1881
1882 pub fn clear(&mut self) -> io::Result<()> {
1888 if !self.is_terminal() {
1889 return Ok(());
1890 }
1891 execute!(self.writer, ct::Clear(ClearType::All))?;
1892 execute!(self.writer, cursor::MoveTo(0, 0))?;
1893 self.writer.flush()
1894 }
1895
1896 pub fn clear_line(&mut self) -> io::Result<()> {
1898 if !self.is_terminal() {
1899 return Ok(());
1900 }
1901 execute!(self.writer, ct::Clear(ClearType::CurrentLine))?;
1902 self.writer.flush()
1903 }
1904
1905 pub fn move_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
1907 if !self.is_terminal() {
1908 return Ok(());
1909 }
1910 execute!(self.writer, cursor::MoveTo(x, y))?;
1911 self.writer.flush()
1912 }
1913
1914 pub fn show_cursor(&mut self, show: bool) -> io::Result<bool> {
1916 if !self.is_terminal() {
1917 return Ok(false);
1918 }
1919 if show {
1920 execute!(self.writer, cursor::Show)?;
1921 } else {
1922 execute!(self.writer, cursor::Hide)?;
1923 }
1924 self.writer.flush()?;
1925 Ok(true)
1926 }
1927
1928 pub fn enter_alt_screen(&mut self) -> io::Result<bool> {
1933 if !self.is_terminal() || self.legacy_windows {
1934 return Ok(false);
1935 }
1936 self.set_alt_screen(true)
1937 }
1938
1939 pub fn leave_alt_screen(&mut self) -> io::Result<bool> {
1941 if !self.is_terminal() || !self.is_alt_screen {
1942 return Ok(false);
1943 }
1944 self.set_alt_screen(false)
1945 }
1946
1947 pub fn is_alt_screen(&self) -> bool {
1949 self.is_alt_screen
1950 }
1951
1952 pub fn set_alt_screen(&mut self, enable: bool) -> io::Result<bool> {
1957 if !self.is_terminal() || self.legacy_windows {
1958 return Ok(false);
1959 }
1960 if enable == self.is_alt_screen {
1961 return Ok(false);
1962 }
1963
1964 let mut segs = Segments::new();
1965 if enable {
1966 segs.push(Segment::control(ControlType::EnableAltScreen));
1967 segs.push(Segment::control(ControlType::Home));
1968 self.is_alt_screen = true;
1969 } else {
1970 segs.push(Segment::control(ControlType::DisableAltScreen));
1971 self.is_alt_screen = false;
1972 }
1973 self.print_segments(&segs)?;
1974 Ok(true)
1975 }
1976
1977 pub fn screen(
1998 &mut self,
1999 hide_cursor: bool,
2000 style: Option<Style>,
2001 ) -> io::Result<crate::screen_context::ScreenContext<'_, W>> {
2002 crate::screen_context::ScreenContext::new(self, hide_cursor, style)
2003 }
2004
2005 pub fn set_window_title(&mut self, title: &str) -> io::Result<bool> {
2007 if !self.is_terminal() {
2008 return Ok(false);
2009 }
2010 execute!(self.writer, ct::SetTitle(title))?;
2011 self.writer.flush()?;
2012 Ok(true)
2013 }
2014
2015 pub fn bell(&mut self) -> io::Result<()> {
2017 write!(self.writer, "\x07")?;
2018 self.writer.flush()
2019 }
2020
2021 pub fn live_start(
2026 &mut self,
2027 renderable: Box<dyn crate::Renderable + Send + Sync>,
2028 vertical_overflow: crate::live::VerticalOverflowMethod,
2029 ) -> (usize, bool) {
2030 let is_root = self.live.stack.is_empty();
2031 let id = self.live.next_id;
2032 self.live.next_id += 1;
2033
2034 self.live.entries.insert(
2035 id,
2036 LiveEntry {
2037 renderable,
2038 vertical_overflow: vertical_overflow.into(),
2039 },
2040 );
2041 self.live.stack.push(id);
2042 (id, is_root)
2043 }
2044
2045 pub fn live_update(&mut self, id: usize, renderable: Box<dyn crate::Renderable + Send + Sync>) {
2046 if let Some(entry) = self.live.entries.get_mut(&id) {
2047 entry.renderable = renderable;
2048 }
2049 }
2050
2051 pub fn live_set_vertical_overflow(
2052 &mut self,
2053 id: usize,
2054 vertical_overflow: crate::live::VerticalOverflowMethod,
2055 ) {
2056 if let Some(entry) = self.live.entries.get_mut(&id) {
2057 entry.vertical_overflow = vertical_overflow.into();
2058 }
2059 }
2060
2061 pub fn live_stop(&mut self, id: usize) -> Option<Box<dyn crate::Renderable + Send + Sync>> {
2062 self.live.stack.retain(|&x| x != id);
2063 let entry = self.live.entries.remove(&id);
2064 if self.live.stack.is_empty() {
2065 self.live.shape = None;
2066 self.live.buffer = None;
2067 }
2068 entry.map(|e| e.renderable)
2069 }
2070
2071 pub fn live_clear(&mut self) {
2072 self.live.stack.clear();
2073 self.live.entries.clear();
2074 self.live.shape = None;
2075 self.live.buffer = None;
2076 }
2077
2078 fn has_live(&self) -> bool {
2079 !self.live.stack.is_empty()
2080 }
2081
2082 fn live_root(&self) -> Option<&LiveEntry> {
2083 let id = *self.live.stack.first()?;
2084 self.live.entries.get(&id)
2085 }
2086
2087 fn live_position_cursor_for_shape(
2088 &self,
2089 shape: Option<(usize, usize)>,
2090 erase: bool,
2091 ) -> Segments {
2092 let Some((_, height)) = shape else {
2093 return Segments::new();
2094 };
2095 if height == 0 {
2096 return Segments::new();
2097 }
2098 let mut controls = Vec::new();
2099 controls.push(Segment::control(ControlType::CarriageReturn));
2100 if erase {
2101 controls.push(Segment::control(ControlType::EraseInLine(2)));
2102 }
2103 for _ in 0..height.saturating_sub(1) {
2104 controls.push(Segment::control(ControlType::CursorUp(1)));
2105 if erase {
2106 controls.push(Segment::control(ControlType::CarriageReturn));
2107 controls.push(Segment::control(ControlType::EraseInLine(2)));
2108 }
2109 }
2110 Segments::from_iter(controls)
2111 }
2112
2113 pub(crate) fn live_restore_cursor(&self) -> Segments {
2114 let Some((_, height)) = self.live.shape else {
2115 return Segments::new();
2116 };
2117 if height == 0 {
2118 return Segments::new();
2119 }
2120 let mut controls = Vec::new();
2121 controls.push(Segment::control(ControlType::CarriageReturn));
2122 for _ in 0..height {
2123 controls.push(Segment::control(ControlType::CursorUp(1)));
2124 controls.push(Segment::control(ControlType::CarriageReturn));
2125 controls.push(Segment::control(ControlType::EraseInLine(2)));
2126 }
2127 Segments::from_iter(controls)
2128 }
2129
2130 fn render_live_segments(&mut self, options: &ConsoleOptions) -> (Segments, bool) {
2131 let root = match self.live_root() {
2132 Some(root) => root,
2133 None => return (Segments::new(), false),
2134 };
2135
2136 let mut lines: Vec<Vec<Segment>> = Vec::new();
2137 for id in self.live.stack.iter() {
2138 if let Some(entry) = self.live.entries.get(id) {
2139 let mut rendered =
2140 self.render_lines(entry.renderable.as_ref(), Some(options), None, false, false);
2141 lines.append(&mut rendered);
2142 }
2143 }
2144
2145 let max_height = options.size.1;
2146 if max_height > 0 && lines.len() > max_height {
2147 match root.vertical_overflow {
2148 LiveVerticalOverflow::Visible => {}
2149 LiveVerticalOverflow::Crop => {
2150 lines.truncate(max_height);
2151 }
2152 LiveVerticalOverflow::Ellipsis => {
2153 lines.truncate(max_height.saturating_sub(1));
2154 let style = options.get_style("live.ellipsis").unwrap_or_default();
2155 let ellipsis = Text::styled("...", style).center(options.max_width);
2156 let ellipsis_lines =
2157 self.render_lines(&ellipsis, Some(options), None, false, false);
2158 if let Some(first) = ellipsis_lines.into_iter().next() {
2159 lines.push(first);
2160 }
2161 }
2162 }
2163 }
2164
2165 let shape = Segment::get_shape(&lines);
2166 self.live.shape = Some(shape);
2167
2168 let width = options.max_width.max(1);
2169 let height = shape.1.max(1);
2170 let current_buffer = ScreenBuffer::from_lines(&lines, width, height, None);
2171
2172 let use_diff = self.live.buffer.as_ref().is_some_and(|previous| {
2173 previous.width == current_buffer.width && previous.height == current_buffer.height
2174 });
2175
2176 if use_diff {
2177 let previous = self.live.buffer.as_ref().expect("checked above");
2178 let diff = current_buffer.diff_to_segments_from_origin(previous);
2179 self.live.buffer = Some(current_buffer);
2180 return (diff, false);
2181 }
2182
2183 self.live.buffer = Some(current_buffer);
2184
2185 let mut out = Segments::new();
2186 let new_line = Segment::line();
2187 for (i, line) in lines.into_iter().enumerate() {
2188 for seg in line {
2189 out.push(seg);
2190 }
2191 if i + 1 < shape.1 {
2192 out.push(new_line.clone());
2193 }
2194 }
2195 (out, true)
2196 }
2197
2198 pub fn input(&mut self, prompt: &Text, password: bool) -> io::Result<String> {
2232 use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
2233 use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
2234
2235 self.print(prompt, None, None, None, false, "")?;
2237 self.writer.flush()?;
2238
2239 if password && self.is_terminal() && !self.is_dumb_terminal() {
2241 enable_raw_mode()?;
2242
2243 let result = (|| -> io::Result<String> {
2244 let mut input = String::new();
2245
2246 loop {
2247 if let Event::Key(KeyEvent {
2248 code, modifiers, ..
2249 }) = event::read()?
2250 {
2251 match code {
2252 KeyCode::Enter => {
2253 write!(self.writer, "\r\n")?;
2255 self.writer.flush()?;
2256 return Ok(input);
2257 }
2258 KeyCode::Backspace => {
2259 input.pop();
2260 }
2261 KeyCode::Char(c) => {
2262 if c == 'c' && modifiers.contains(KeyModifiers::CONTROL) {
2264 write!(self.writer, "\r\n")?;
2265 self.writer.flush()?;
2266 return Err(io::Error::new(
2267 io::ErrorKind::Interrupted,
2268 "Input cancelled",
2269 ));
2270 }
2271 if c == 'd' && modifiers.contains(KeyModifiers::CONTROL) {
2273 write!(self.writer, "\r\n")?;
2274 self.writer.flush()?;
2275 return Err(io::Error::new(
2276 io::ErrorKind::UnexpectedEof,
2277 "EOF",
2278 ));
2279 }
2280 input.push(c);
2281 }
2282 KeyCode::Esc => {
2283 write!(self.writer, "\r\n")?;
2285 self.writer.flush()?;
2286 return Err(io::Error::new(
2287 io::ErrorKind::Interrupted,
2288 "Input cancelled",
2289 ));
2290 }
2291 _ => {}
2292 }
2293 }
2294 }
2295 })();
2296
2297 let _ = disable_raw_mode();
2299 result
2300 } else {
2301 let mut input = String::new();
2303 io::stdin().read_line(&mut input)?;
2304 if input.ends_with('\n') {
2306 input.pop();
2307 if input.ends_with('\r') {
2308 input.pop();
2309 }
2310 }
2311 Ok(input)
2312 }
2313 }
2314
2315 pub fn measure<R: Renderable + ?Sized>(
2330 &self,
2331 renderable: &R,
2332 options: Option<&ConsoleOptions>,
2333 ) -> crate::measure::Measurement {
2334 let measure_opts = options.cloned().unwrap_or_else(|| self.options.clone());
2336
2337 let temp_console = Console::<Stdout>::with_options(measure_opts.clone());
2340 renderable.measure(&temp_console, &measure_opts)
2341 }
2342
2343 pub fn out(
2352 &mut self,
2353 text: &str,
2354 style: Option<Style>,
2355 _highlight: Option<bool>,
2356 ) -> io::Result<()> {
2357 if self.quiet {
2358 return Ok(());
2359 }
2360 self.print(
2361 &Text::plain(text),
2362 style,
2363 None,
2364 Some(OverflowMethod::Ignore),
2365 true, "\n",
2367 )
2368 }
2369
2370 pub fn export_text(&self, clear: bool, styles: bool) -> String {
2375 let mut buffer = self.record_buffer.lock().unwrap();
2376 let text = if styles {
2377 buffer
2378 .iter()
2379 .filter(|s| s.control.is_none())
2380 .map(|s| {
2381 if let Some(style) = s.style {
2382 if let Some(color_system) = self.color_system {
2383 style.render(&s.text, color_system)
2384 } else {
2385 s.text.to_string()
2386 }
2387 } else {
2388 s.text.to_string()
2389 }
2390 })
2391 .collect::<String>()
2392 } else {
2393 buffer
2394 .iter()
2395 .filter(|s| s.control.is_none())
2396 .map(|s| s.text.to_string())
2397 .collect::<String>()
2398 };
2399 if clear {
2400 buffer.clear();
2401 }
2402 text
2403 }
2404
2405 pub fn save_text(&self, path: &str, clear: bool, styles: bool) -> io::Result<()> {
2407 let text = self.export_text(clear, styles);
2408 std::fs::write(path, text)
2409 }
2410
2411 pub fn push_render_hook(&mut self, hook: Box<dyn Fn(&Segments) -> Segments + Send + Sync>) {
2413 self.render_hooks.push(hook);
2414 }
2415
2416 pub fn pop_render_hook(&mut self) {
2418 self.render_hooks.pop();
2419 }
2420
2421 pub fn status(
2426 &self,
2427 status: &str,
2428 spinner: Option<&str>,
2429 spinner_style: Option<Style>,
2430 speed: Option<f64>,
2431 refresh_per_second: Option<f64>,
2432 ) -> crate::status::Status {
2433 crate::status::Status::with_options(
2434 status,
2435 spinner.unwrap_or("dots"),
2436 spinner_style,
2437 speed.unwrap_or(1.0),
2438 refresh_per_second.unwrap_or(12.5),
2439 )
2440 }
2441
2442 pub fn print_json(
2446 &mut self,
2447 json: &str,
2448 indent: usize,
2449 highlight: bool,
2450 sort_keys: bool,
2451 ) -> io::Result<()> {
2452 let json_renderable = crate::json::Json::new(json, indent, highlight, sort_keys);
2453 self.print(&json_renderable, None, None, None, true, "\n")
2454 }
2455}
2456
2457impl Console<Stdout> {
2460 pub fn render<R: Renderable + ?Sized>(&self, renderable: &R) -> Segments {
2462 self.apply_render_hooks(renderable.render(self, &self.options))
2463 }
2464
2465 pub fn render_with_options<R: Renderable + ?Sized>(
2467 &self,
2468 renderable: &R,
2469 options: &ConsoleOptions,
2470 ) -> Segments {
2471 self.apply_render_hooks(renderable.render(self, options))
2472 }
2473
2474 pub fn update_screen_lines(
2478 &mut self,
2479 lines: &[Vec<Segment>],
2480 x: u16,
2481 y: u16,
2482 ) -> io::Result<()> {
2483 if !self.is_alt_screen() {
2484 return Err(io::Error::new(
2485 io::ErrorKind::Other,
2486 "Alt screen must be enabled to call update_screen_lines",
2487 ));
2488 }
2489
2490 let mut segments = Segments::new();
2491 for (offset, line) in lines.iter().enumerate() {
2492 segments.push(Segment::control(ControlType::MoveTo {
2493 x,
2494 y: y.saturating_add(offset as u16),
2495 }));
2496 segments.extend(line.iter().cloned());
2497 }
2498 self.print_segments(&segments)?;
2499 Ok(())
2500 }
2501}
2502
2503#[derive(Debug, Clone, Default)]
2509pub struct PagerOptions {
2510 pub styles: bool,
2512}
2513
2514impl PagerOptions {
2515 pub fn new() -> Self {
2517 Self::default()
2518 }
2519
2520 pub fn with_styles(mut self, styles: bool) -> Self {
2522 self.styles = styles;
2523 self
2524 }
2525}
2526
2527pub struct PagerContext {
2545 buffer: Vec<u8>,
2547 options: PagerOptions,
2549 console_options: ConsoleOptions,
2551}
2552
2553impl PagerContext {
2554 fn new(console_options: ConsoleOptions, options: Option<PagerOptions>) -> Self {
2556 let options = options.unwrap_or_default();
2557 let mut console_options = console_options;
2559 if !options.styles {
2560 console_options.is_terminal = false;
2561 console_options.color_system = None;
2562 }
2563 Self {
2564 buffer: Vec::new(),
2565 options,
2566 console_options,
2567 }
2568 }
2569
2570 pub fn print_text(&mut self, text: &str) -> io::Result<()> {
2572 writeln!(self.buffer, "{}", text)
2573 }
2574
2575 pub fn print<R: crate::Renderable + ?Sized>(
2577 &mut self,
2578 renderable: &R,
2579 style: Option<Style>,
2580 justify: Option<JustifyMethod>,
2581 overflow: Option<OverflowMethod>,
2582 no_wrap: bool,
2583 end: &str,
2584 ) -> io::Result<()> {
2585 let mut console = Console::with_writer(Vec::new(), self.console_options.clone());
2587
2588 console.print(renderable, style, justify, overflow, no_wrap, end)?;
2590
2591 self.buffer.extend_from_slice(console.get_captured_bytes());
2593 Ok(())
2594 }
2595
2596 pub fn get_buffer(&self) -> &[u8] {
2598 &self.buffer
2599 }
2600
2601 pub fn get_buffer_string(&self) -> String {
2603 String::from_utf8_lossy(&self.buffer).to_string()
2604 }
2605
2606 pub fn show(&self) -> io::Result<()> {
2608 use crate::pager::{Pager, SystemPager};
2609 let pager = SystemPager::with_styles(self.options.styles);
2610 let content = self.get_buffer_string();
2611 pager.show(&content)
2612 }
2613}
2614
2615impl Drop for PagerContext {
2616 fn drop(&mut self) {
2617 if !self.buffer.is_empty() {
2618 let _ = self.show();
2619 }
2620 }
2621}
2622
2623impl Console<Stdout> {
2624 pub fn pager(&self, options: Option<PagerOptions>) -> PagerContext {
2646 PagerContext::new(self.options.clone(), options)
2647 }
2648
2649 pub fn is_recording(&self) -> bool {
2655 self.record
2656 }
2657
2658 pub fn set_record(&mut self, record: bool) {
2663 self.record = record;
2664 }
2665
2666 pub fn clear_record_buffer(&mut self) {
2668 if let Ok(mut buffer) = self.record_buffer.lock() {
2669 buffer.clear();
2670 }
2671 }
2672
2673 pub fn get_record_buffer(&self) -> Vec<Segment> {
2677 self.record_buffer
2678 .lock()
2679 .map(|buf| buf.clone())
2680 .unwrap_or_default()
2681 }
2682
2683 pub fn export_svg(
2708 &mut self,
2709 title: &str,
2710 theme: Option<&TerminalTheme>,
2711 clear: bool,
2712 code_format: Option<&str>,
2713 font_aspect_ratio: f64,
2714 unique_id: Option<&str>,
2715 ) -> String {
2716 let theme = theme.unwrap_or(&*SVG_EXPORT_THEME);
2717 let code_format = code_format.unwrap_or(CONSOLE_SVG_FORMAT);
2718
2719 let mut classes: HashMap<String, usize> = HashMap::new();
2721 let mut style_no = 1usize;
2722
2723 let width = self.width();
2724 let char_height = 20.0;
2725 let char_width = char_height * font_aspect_ratio;
2726 let line_height = char_height * 1.22;
2727
2728 let margin_top = 1.0;
2729 let margin_right = 1.0;
2730 let margin_bottom = 1.0;
2731 let margin_left = 1.0;
2732
2733 let padding_top = 40.0;
2734 let padding_right = 8.0;
2735 let padding_bottom = 8.0;
2736 let padding_left = 8.0;
2737
2738 let padding_width = padding_left + padding_right;
2739 let padding_height = padding_top + padding_bottom;
2740 let margin_width = margin_left + margin_right;
2741 let margin_height = margin_top + margin_bottom;
2742
2743 let mut text_backgrounds: Vec<String> = Vec::new();
2744 let mut text_group: Vec<String> = Vec::new();
2745
2746 let segments: Vec<Segment> = {
2748 let mut buffer = self.record_buffer.lock().unwrap();
2749 let segments: Vec<Segment> = buffer
2750 .iter()
2751 .filter(|s| s.control.is_none())
2752 .cloned()
2753 .collect();
2754 if clear {
2755 buffer.clear();
2756 }
2757 segments
2758 };
2759
2760 let unique_id = unique_id.map(|s| s.to_string()).unwrap_or_else(|| {
2762 let content: String = segments
2763 .iter()
2764 .map(|s| format!("{:?}", s))
2765 .collect::<Vec<_>>()
2766 .join("");
2767 let hash = adler32(&format!("{}{}", content, title));
2768 format!("terminal-{}", hash)
2769 });
2770
2771 let lines =
2773 Segment::split_and_crop_lines(Segments::from_iter(segments), width, None, false, false);
2774
2775 let mut y = 0usize;
2776 for line in &lines {
2777 let mut x = 0usize;
2778
2779 for segment in line {
2780 let style = segment.style.unwrap_or_default();
2781 let rules = get_svg_style_for_segment(&style, theme);
2782
2783 if !classes.contains_key(&rules) {
2784 classes.insert(rules.clone(), style_no);
2785 style_no += 1;
2786 }
2787 let class_name = format!("r{}", classes[&rules]);
2788
2789 let has_background = if style.reverse.unwrap_or(false) {
2791 true
2792 } else {
2793 style.bgcolor.is_some() && !is_default_color(style.bgcolor)
2794 };
2795
2796 let background = if style.reverse.unwrap_or(false) {
2797 style
2798 .color
2799 .map(|c| resolve_color_for_svg(c, theme, true))
2800 .unwrap_or(theme.foreground_color)
2801 } else {
2802 style
2803 .bgcolor
2804 .map(|c| resolve_color_for_svg(c, theme, false))
2805 .unwrap_or(theme.background_color)
2806 };
2807
2808 let text_length = cell_len(&segment.text);
2809
2810 if has_background {
2811 text_backgrounds.push(make_tag(
2812 "rect",
2813 None,
2814 &[
2815 ("fill", &background.hex()),
2816 ("x", &format_number(x as f64 * char_width)),
2817 ("y", &format_number(y as f64 * line_height + 1.5)),
2818 ("width", &format_number(char_width * text_length as f64)),
2819 ("height", &format_number(line_height + 0.25)),
2820 ("shape-rendering", "crispEdges"),
2821 ],
2822 ));
2823 }
2824
2825 if !segment.text.chars().all(|c| c == ' ') {
2827 text_group.push(make_tag(
2828 "text",
2829 Some(&escape_text(&segment.text)),
2830 &[
2831 ("class", &format!("{}-{}", unique_id, class_name)),
2832 ("x", &format_number(x as f64 * char_width)),
2833 ("y", &format_number(y as f64 * line_height + char_height)),
2834 (
2835 "textLength",
2836 &format_number(char_width * text_length as f64),
2837 ),
2838 ("clip-path", &format!("url(#{}-line-{})", unique_id, y)),
2839 ],
2840 ));
2841 }
2842
2843 x += text_length;
2844 }
2845 y += 1;
2846 }
2847
2848 let line_offsets: Vec<f64> = (0..y)
2850 .map(|line_no| line_no as f64 * line_height + 1.5)
2851 .collect();
2852
2853 let lines_svg: String = line_offsets
2854 .iter()
2855 .enumerate()
2856 .map(|(line_no, offset)| {
2857 format!(
2858 r#"<clipPath id="{}-line-{}">
2859 {}
2860 </clipPath>"#,
2861 unique_id,
2862 line_no,
2863 make_tag(
2864 "rect",
2865 None,
2866 &[
2867 ("x", "0"),
2868 ("y", &format_number(*offset)),
2869 ("width", &format_number(char_width * width as f64)),
2870 ("height", &format_number(line_height + 0.25)),
2871 ],
2872 )
2873 )
2874 })
2875 .collect::<Vec<_>>()
2876 .join("\n");
2877
2878 let styles: String = classes
2880 .iter()
2881 .map(|(css, rule_no)| format!(".{}-r{} {{ {} }}", unique_id, rule_no, css))
2882 .collect::<Vec<_>>()
2883 .join("\n");
2884
2885 let backgrounds = text_backgrounds.join("");
2886 let matrix = text_group.join("\n");
2887
2888 let terminal_width = (width as f64 * char_width + padding_width).ceil();
2889 let terminal_height = (y as f64 + 1.0) * line_height + padding_height;
2890
2891 let mut chrome = make_tag(
2893 "rect",
2894 None,
2895 &[
2896 ("fill", &theme.background_color.hex()),
2897 ("stroke", "rgba(255,255,255,0.35)"),
2898 ("stroke-width", "1"),
2899 ("x", &format_number(margin_left)),
2900 ("y", &format_number(margin_top)),
2901 ("width", &format_number(terminal_width)),
2902 ("height", &format_number(terminal_height)),
2903 ("rx", "8"),
2904 ],
2905 );
2906
2907 if !title.is_empty() {
2909 chrome.push_str(&make_tag(
2910 "text",
2911 Some(&escape_text(title)),
2912 &[
2913 ("class", &format!("{}-title", unique_id)),
2914 ("fill", &theme.foreground_color.hex()),
2915 ("text-anchor", "middle"),
2916 ("x", &format_number(terminal_width / 2.0)),
2917 ("y", &format_number(margin_top + char_height + 6.0)),
2918 ],
2919 ));
2920 }
2921
2922 chrome.push_str(
2924 r##"
2925 <g transform="translate(26,22)">
2926 <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
2927 <circle cx="22" cy="0" r="7" fill="#febc2e"/>
2928 <circle cx="44" cy="0" r="7" fill="#28c840"/>
2929 </g>
2930 "##,
2931 );
2932
2933 code_format
2935 .replace("{unique_id}", &unique_id)
2936 .replace("{char_width}", &format_number(char_width))
2937 .replace("{char_height}", &format_number(char_height))
2938 .replace("{line_height}", &format_number(line_height))
2939 .replace(
2940 "{terminal_width}",
2941 &format_number(char_width * width as f64 - 1.0),
2942 )
2943 .replace(
2944 "{terminal_height}",
2945 &format_number((y as f64 + 1.0) * line_height - 1.0),
2946 )
2947 .replace("{width}", &format_number(terminal_width + margin_width))
2948 .replace("{height}", &format_number(terminal_height + margin_height))
2949 .replace("{terminal_x}", &format_number(margin_left + padding_left))
2950 .replace("{terminal_y}", &format_number(margin_top + padding_top))
2951 .replace("{styles}", &styles)
2952 .replace("{chrome}", &chrome)
2953 .replace("{backgrounds}", &backgrounds)
2954 .replace("{matrix}", &matrix)
2955 .replace("{lines}", &lines_svg)
2956 }
2957
2958 pub fn export_html(
2984 &mut self,
2985 theme: Option<&TerminalTheme>,
2986 clear: bool,
2987 code_format: Option<&str>,
2988 ) -> String {
2989 let theme = theme.unwrap_or(&*DEFAULT_TERMINAL_THEME);
2990 let code_format = code_format.unwrap_or(CONSOLE_HTML_FORMAT);
2991
2992 let mut classes: HashMap<String, usize> = HashMap::new();
2994 let mut style_no = 1usize;
2995
2996 let segments: Vec<Segment> = {
2998 let mut buffer = self.record_buffer.lock().unwrap();
2999 let segments: Vec<Segment> = buffer
3000 .iter()
3001 .filter(|s| s.control.is_none())
3002 .cloned()
3003 .collect();
3004 if clear {
3005 buffer.clear();
3006 }
3007 segments
3008 };
3009
3010 for segment in &segments {
3012 let style = segment.style.unwrap_or_default();
3013 let rules = get_html_style_for_segment(&style, theme);
3014 if !rules.is_empty() && !classes.contains_key(&rules) {
3015 classes.insert(rules, style_no);
3016 style_no += 1;
3017 }
3018 }
3019
3020 let stylesheet: String = classes
3022 .iter()
3023 .map(|(css, rule_no)| format!(".r{} {{ {} }}", rule_no, css))
3024 .collect::<Vec<_>>()
3025 .join("\n");
3026
3027 #[derive(Debug, Clone, PartialEq, Eq)]
3029 enum OpenTag {
3030 Span {
3031 class_no: usize,
3032 },
3033 Link {
3034 class_no: Option<usize>,
3035 href: Arc<str>,
3036 },
3037 }
3038
3039 let mut code = String::new();
3040 let mut open: Option<OpenTag> = None;
3041
3042 let close_open = |code: &mut String, open: &mut Option<OpenTag>| {
3043 if let Some(tag) = open.take() {
3044 match tag {
3045 OpenTag::Span { .. } => code.push_str("</span>"),
3046 OpenTag::Link { .. } => code.push_str("</a>"),
3047 }
3048 }
3049 };
3050
3051 let open_tag = |code: &mut String, tag: &OpenTag| match tag {
3052 OpenTag::Span { class_no } => {
3053 code.push_str(&format!("<span class=\"r{}\">", class_no));
3054 }
3055 OpenTag::Link { class_no, href } => {
3056 let href_escaped = escape_html_attr(href);
3057 if let Some(class_no) = class_no {
3058 code.push_str(&format!(
3059 "<a class=\"r{}\" href=\"{}\">",
3060 class_no, href_escaped
3061 ));
3062 } else {
3063 code.push_str(&format!("<a href=\"{}\">", href_escaped));
3064 }
3065 }
3066 };
3067
3068 for segment in &segments {
3069 let text = segment.text.as_ref();
3070 if text.is_empty() {
3071 continue;
3072 }
3073
3074 let style = segment.style.unwrap_or_default();
3075 let rules = get_html_style_for_segment(&style, theme);
3076 let class_no = if rules.is_empty() {
3077 None
3078 } else {
3079 Some(classes[&rules])
3080 };
3081
3082 let href = segment.meta.as_ref().and_then(|m| m.link.as_ref()).cloned();
3083
3084 let desired: Option<OpenTag> = if let Some(href) = href {
3085 Some(OpenTag::Link { class_no, href })
3086 } else if let Some(class_no) = class_no {
3087 Some(OpenTag::Span { class_no })
3088 } else {
3089 None
3090 };
3091
3092 if desired != open {
3093 close_open(&mut code, &mut open);
3094 if let Some(tag) = &desired {
3095 open_tag(&mut code, tag);
3096 }
3097 open = desired;
3098 }
3099
3100 code.push_str(&escape_html_text(text));
3102 }
3103
3104 close_open(&mut code, &mut open);
3105
3106 code_format
3108 .replace("{stylesheet}", &stylesheet)
3109 .replace("{foreground}", &theme.foreground_color.hex())
3110 .replace("{background}", &theme.background_color.hex())
3111 .replace("{code}", &code)
3112 }
3113
3114 pub fn save_html(
3118 &mut self,
3119 path: &str,
3120 theme: Option<&TerminalTheme>,
3121 clear: bool,
3122 code_format: Option<&str>,
3123 ) -> io::Result<()> {
3124 let html = self.export_html(theme, clear, code_format);
3125 std::fs::write(path, html)
3126 }
3127
3128 pub fn save_svg(
3152 &mut self,
3153 path: &str,
3154 title: &str,
3155 theme: Option<&TerminalTheme>,
3156 clear: bool,
3157 font_aspect_ratio: f64,
3158 unique_id: Option<&str>,
3159 ) -> io::Result<()> {
3160 let svg = self.export_svg(title, theme, clear, None, font_aspect_ratio, unique_id);
3161 std::fs::write(path, svg)
3162 }
3163}
3164
3165pub(crate) fn get_svg_style_for_segment(style: &Style, theme: &TerminalTheme) -> String {
3171 let mut css_rules = Vec::new();
3172
3173 let fg_color = style
3175 .color
3176 .map(|c| resolve_color_for_svg(c, theme, true))
3177 .unwrap_or(theme.foreground_color);
3178
3179 let bg_color = style
3181 .bgcolor
3182 .map(|c| resolve_color_for_svg(c, theme, false))
3183 .unwrap_or(theme.background_color);
3184
3185 let (fg_color, bg_color) = if style.reverse.unwrap_or(false) {
3187 (bg_color, fg_color)
3188 } else {
3189 (fg_color, bg_color)
3190 };
3191
3192 let fg_color = if style.dim.unwrap_or(false) {
3194 blend_rgb_for_svg(fg_color, bg_color, 0.4)
3195 } else {
3196 fg_color
3197 };
3198
3199 css_rules.push(format!("fill: {}", fg_color.hex()));
3200
3201 if style.bold.unwrap_or(false) {
3202 css_rules.push("font-weight: bold".to_string());
3203 }
3204 if style.italic.unwrap_or(false) {
3205 css_rules.push("font-style: italic".to_string());
3206 }
3207 if style.underline.unwrap_or(false) {
3208 css_rules.push("text-decoration: underline".to_string());
3209 }
3210 if style.strike.unwrap_or(false) {
3211 css_rules.push("text-decoration: line-through".to_string());
3212 }
3213
3214 css_rules.join(";")
3215}
3216
3217fn get_html_style_for_segment(style: &Style, theme: &TerminalTheme) -> String {
3219 let mut css_rules: Vec<String> = Vec::new();
3220
3221 let fg_color = style
3223 .color
3224 .map(|c| resolve_color_for_svg(c, theme, true))
3225 .unwrap_or(theme.foreground_color);
3226
3227 let bg_color = style
3229 .bgcolor
3230 .map(|c| resolve_color_for_svg(c, theme, false))
3231 .unwrap_or(theme.background_color);
3232
3233 let (fg_color, bg_color) = if style.reverse.unwrap_or(false) {
3235 (bg_color, fg_color)
3236 } else {
3237 (fg_color, bg_color)
3238 };
3239
3240 let fg_color = if style.dim.unwrap_or(false) {
3242 blend_rgb_for_svg(fg_color, bg_color, 0.5)
3243 } else {
3244 fg_color
3245 };
3246
3247 if style.color.is_some() || style.reverse.unwrap_or(false) || style.dim.unwrap_or(false) {
3249 css_rules.push(format!("color: {}", fg_color.hex()));
3250 css_rules.push(format!("text-decoration-color: {}", fg_color.hex()));
3251 }
3252
3253 let has_background = if style.reverse.unwrap_or(false) {
3255 true
3256 } else {
3257 style.bgcolor.is_some() && !is_default_color(style.bgcolor)
3258 };
3259 if has_background {
3260 css_rules.push(format!("background-color: {}", bg_color.hex()));
3261 }
3262
3263 if style.bold.unwrap_or(false) {
3265 css_rules.push("font-weight: bold".to_string());
3266 }
3267 if style.italic.unwrap_or(false) {
3268 css_rules.push("font-style: italic".to_string());
3269 }
3270
3271 let mut decorations = Vec::new();
3272 if style.underline.unwrap_or(false) {
3273 decorations.push("underline");
3274 }
3275 if style.strike.unwrap_or(false) {
3276 decorations.push("line-through");
3277 }
3278 if !decorations.is_empty() {
3279 css_rules.push(format!("text-decoration: {}", decorations.join(" ")));
3280 }
3281
3282 css_rules.join("; ")
3283}
3284
3285pub(crate) fn resolve_color_for_svg(
3287 color: SimpleColor,
3288 theme: &TerminalTheme,
3289 is_foreground: bool,
3290) -> ColorTriplet {
3291 match color {
3292 SimpleColor::Default => {
3293 if is_foreground {
3294 theme.foreground_color
3295 } else {
3296 theme.background_color
3297 }
3298 }
3299 SimpleColor::Standard(index) => theme.get_ansi_color(index as usize),
3300 SimpleColor::EightBit(index) => {
3301 if let Some(triplet) = crate::color::EIGHT_BIT_PALETTE.get(index as usize) {
3303 triplet
3304 } else {
3305 theme.foreground_color
3306 }
3307 }
3308 SimpleColor::Rgb { r, g, b } => ColorTriplet::new(r, g, b),
3309 }
3310}
3311
3312pub(crate) fn is_default_color(color: Option<SimpleColor>) -> bool {
3314 matches!(color, None | Some(SimpleColor::Default))
3315}
3316
3317pub(crate) fn blend_rgb_for_svg(
3319 color: ColorTriplet,
3320 background: ColorTriplet,
3321 factor: f64,
3322) -> ColorTriplet {
3323 let r = (color.red as f64 + (background.red as f64 - color.red as f64) * factor) as u8;
3324 let g = (color.green as f64 + (background.green as f64 - color.green as f64) * factor) as u8;
3325 let b = (color.blue as f64 + (background.blue as f64 - color.blue as f64) * factor) as u8;
3326 ColorTriplet::new(r, g, b)
3327}
3328
3329pub(crate) fn adler32(data: &str) -> u32 {
3331 let mut a: u32 = 1;
3332 let mut b: u32 = 0;
3333 const MOD_ADLER: u32 = 65521;
3334
3335 for byte in data.bytes() {
3336 a = (a + byte as u32) % MOD_ADLER;
3337 b = (b + a) % MOD_ADLER;
3338 }
3339
3340 (b << 16) | a
3341}
3342
3343pub(crate) fn escape_text(text: &str) -> String {
3345 text.replace('&', "&")
3346 .replace('<', "<")
3347 .replace('>', ">")
3348 .replace(' ', " ")
3349}
3350
3351fn escape_html_text(text: &str) -> String {
3353 text.replace('&', "&")
3354 .replace('<', "<")
3355 .replace('>', ">")
3356}
3357
3358fn escape_html_attr(text: &str) -> String {
3360 text.replace('&', "&")
3361 .replace('<', "<")
3362 .replace('>', ">")
3363 .replace('"', """)
3364}
3365
3366pub(crate) fn format_number(value: f64) -> String {
3368 if value.fract() == 0.0 {
3369 format!("{}", value as i64)
3370 } else {
3371 format!("{:.2}", value)
3372 .trim_end_matches('0')
3373 .trim_end_matches('.')
3374 .to_string()
3375 }
3376}
3377
3378pub(crate) fn make_tag(name: &str, content: Option<&str>, attribs: &[(&str, &str)]) -> String {
3380 let attribs_str: String = attribs
3381 .iter()
3382 .map(|(k, v)| format!("{}=\"{}\"", k, v))
3383 .collect::<Vec<_>>()
3384 .join(" ");
3385
3386 if let Some(content) = content {
3387 format!("<{} {}>{}</{}>", name, attribs_str, content, name)
3388 } else {
3389 format!("<{} {}/>", name, attribs_str)
3390 }
3391}
3392
3393#[cfg(test)]
3398mod tests {
3399 use super::*;
3400 use crate::Control;
3401 use crate::StyleMeta;
3402
3403 #[test]
3406 fn test_justify_method_parse() {
3407 assert_eq!(JustifyMethod::parse("left"), Some(JustifyMethod::Left));
3408 assert_eq!(JustifyMethod::parse("CENTER"), Some(JustifyMethod::Center));
3409 assert_eq!(JustifyMethod::parse("Right"), Some(JustifyMethod::Right));
3410 assert_eq!(JustifyMethod::parse("full"), Some(JustifyMethod::Full));
3411 assert_eq!(
3412 JustifyMethod::parse("default"),
3413 Some(JustifyMethod::Default)
3414 );
3415 assert_eq!(JustifyMethod::parse("invalid"), None);
3416 }
3417
3418 #[test]
3421 fn test_overflow_method_parse() {
3422 assert_eq!(OverflowMethod::parse("fold"), Some(OverflowMethod::Fold));
3423 assert_eq!(OverflowMethod::parse("CROP"), Some(OverflowMethod::Crop));
3424 assert_eq!(
3425 OverflowMethod::parse("Ellipsis"),
3426 Some(OverflowMethod::Ellipsis)
3427 );
3428 assert_eq!(
3429 OverflowMethod::parse("ignore"),
3430 Some(OverflowMethod::Ignore)
3431 );
3432 assert_eq!(OverflowMethod::parse("invalid"), None);
3433 }
3434
3435 #[test]
3438 fn test_console_options_default() {
3439 let options = ConsoleOptions::default();
3440 assert_eq!(options.size, (80, 24));
3441 assert_eq!(options.min_width, 1);
3442 assert_eq!(options.max_width, 80);
3443 assert_eq!(options.max_height, 24);
3444 assert!(options.is_terminal);
3445 assert_eq!(options.encoding, "utf-8");
3446 }
3447
3448 #[test]
3449 fn test_console_options_ascii_only() {
3450 let options = ConsoleOptions {
3451 encoding: "utf-8".to_string(),
3452 ..Default::default()
3453 };
3454 assert!(!options.ascii_only());
3455
3456 let options = ConsoleOptions {
3457 encoding: "ascii".to_string(),
3458 ..Default::default()
3459 };
3460 assert!(options.ascii_only());
3461
3462 let options = ConsoleOptions {
3463 encoding: "latin-1".to_string(),
3464 ..Default::default()
3465 };
3466 assert!(options.ascii_only());
3467 }
3468
3469 #[test]
3470 fn test_console_options_update_width() {
3471 let options = ConsoleOptions::default();
3472 let updated = options.update_width(120);
3473 assert_eq!(updated.min_width, 120);
3474 assert_eq!(updated.max_width, 120);
3475 }
3476
3477 #[test]
3478 fn test_console_options_update_height() {
3479 let options = ConsoleOptions::default();
3480 let updated = options.update_height(40);
3481 assert_eq!(updated.max_height, 40);
3482 assert_eq!(updated.height, Some(40));
3483 }
3484
3485 #[test]
3486 fn test_console_options_update_dimensions() {
3487 let options = ConsoleOptions::default();
3488 let updated = options.update_dimensions(100, 50);
3489 assert_eq!(updated.min_width, 100);
3490 assert_eq!(updated.max_width, 100);
3491 assert_eq!(updated.max_height, 50);
3492 assert_eq!(updated.height, Some(50));
3493 }
3494
3495 #[test]
3496 fn test_console_options_reset_height() {
3497 let options = ConsoleOptions {
3498 height: Some(40),
3499 ..Default::default()
3500 };
3501 let reset = options.reset_height();
3502 assert_eq!(reset.height, None);
3503 }
3504
3505 #[test]
3508 fn test_console_capture() {
3509 let mut console = Console::capture();
3510 console.print_text("Hello, World!").unwrap();
3511 let output = console.get_captured();
3512 assert!(output.contains("Hello, World!"));
3513 }
3514
3515 #[test]
3516 fn test_console_capture_styled() {
3517 let mut console = Console::capture();
3518 let style = Style::new().with_bold(true);
3519 console.print_styled("Bold text", style).unwrap();
3520 let output = console.get_captured();
3521 assert!(output.contains("Bold text"));
3522 }
3523
3524 #[test]
3525 fn test_console_capture_clear() {
3526 let mut console = Console::capture();
3527 console.print_text("First").unwrap();
3528 console.clear_captured();
3529 console.print_text("Second").unwrap();
3530 let output = console.get_captured();
3531 assert!(!output.contains("First"));
3532 assert!(output.contains("Second"));
3533 }
3534
3535 #[test]
3536 fn test_console_capture_bytes() {
3537 let mut console = Console::capture();
3538 console.print_text("Test").unwrap();
3539 let bytes = console.get_captured_bytes();
3540 assert!(!bytes.is_empty());
3541 }
3542
3543 #[test]
3544 fn test_render_hook_runs_in_print_pipeline() {
3545 let mut console = Console::capture();
3546 console.push_render_hook(Box::new(|segments: &Segments| {
3547 Segments::from_iter(segments.iter().map(|seg| {
3548 if seg.control.is_some() {
3549 seg.clone()
3550 } else {
3551 Segment::new(seg.text.to_string().to_uppercase())
3552 }
3553 }))
3554 }));
3555
3556 console
3557 .print(&Text::plain("hooked"), None, None, None, false, "\n")
3558 .unwrap();
3559 let output = console.get_captured();
3560 assert!(output.contains("HOOKED"));
3561 }
3562
3563 #[test]
3564 fn test_render_hook_runs_for_live_renderables() {
3565 let mut console = Console::with_writer(
3566 Vec::new(),
3567 ConsoleOptions {
3568 is_terminal: true,
3569 ..Default::default()
3570 },
3571 );
3572 console.set_force_terminal(Some(true));
3573 if console.is_dumb_terminal() {
3574 return;
3575 }
3576 console.push_render_hook(Box::new(|segments: &Segments| {
3577 let mut out = Segments::new();
3578 for seg in segments.iter() {
3579 out.push(seg.clone());
3580 }
3581 out.push(Segment::new("!"));
3582 out
3583 }));
3584
3585 let (_id, _is_root) = console.live_start(
3586 Box::new(Text::plain("LIVE")),
3587 crate::live::VerticalOverflowMethod::Ellipsis,
3588 );
3589 console
3590 .print(&Control::new(), None, None, None, false, "")
3591 .unwrap();
3592
3593 let output = console.get_captured();
3594 assert!(
3595 output.contains("LIVE!"),
3596 "expected hooked live output in captured text, got: {:?}",
3597 output
3598 );
3599 }
3600
3601 #[test]
3602 fn test_console_status_honors_refresh_per_second() {
3603 let console = Console::capture();
3604 let status = console.status("Working...", None, None, None, Some(9.0));
3605 assert_eq!(status.refresh_per_second(), 9.0);
3606 }
3607
3608 #[test]
3609 fn test_live_wrap_emits_cursor_controls_after_first_render() {
3610 let mut console = Console::with_writer(
3611 Vec::new(),
3612 ConsoleOptions {
3613 is_terminal: true,
3614 ..Default::default()
3615 },
3616 );
3617 console.set_force_terminal(Some(true));
3618 if console.is_dumb_terminal() {
3619 return;
3621 }
3622
3623 let (_id, _is_root) = console.live_start(
3624 Box::new(Text::plain("LIVE")),
3625 crate::live::VerticalOverflowMethod::Ellipsis,
3626 );
3627
3628 console
3630 .print(&Text::plain("A"), None, None, None, false, "\n")
3631 .unwrap();
3632 console.clear_captured();
3633
3634 console
3638 .print(&Text::plain("B"), None, None, None, false, "\n")
3639 .unwrap();
3640 let out = console.get_captured();
3641 assert!(
3642 out.contains("\r"),
3643 "expected cursor repositioning (\\r) in second live render, got: {:?}",
3644 out,
3645 );
3646 }
3647
3648 #[test]
3649 fn test_live_full_redraw_repositions_from_previous_shape() {
3650 let mut console = Console::with_writer(
3651 Vec::new(),
3652 ConsoleOptions {
3653 is_terminal: true,
3654 ..Default::default()
3655 },
3656 );
3657 console.set_force_terminal(Some(true));
3658 if console.is_dumb_terminal() {
3659 return;
3660 }
3661
3662 let (id, _is_root) = console.live_start(
3663 Box::new(Text::plain("one line")),
3664 crate::live::VerticalOverflowMethod::Ellipsis,
3665 );
3666
3667 console
3669 .print(&Control::new(), None, None, None, false, "")
3670 .unwrap();
3671 console.clear_captured();
3672
3673 console.live_update(id, Box::new(Text::plain("line 1\nline 2")));
3676 console
3677 .print(&Control::new(), None, None, None, false, "")
3678 .unwrap();
3679
3680 let out = console.get_captured();
3681 assert!(
3682 !out.contains("\x1b[1A"),
3683 "did not expect cursor-up for previous 1-line frame, got: {:?}",
3684 out,
3685 );
3686 }
3687
3688 #[test]
3689 fn test_set_alt_screen_emits_enable_and_home() {
3690 let mut console = Console::with_writer(
3691 Vec::new(),
3692 ConsoleOptions {
3693 is_terminal: true,
3694 ..Default::default()
3695 },
3696 );
3697 console.set_force_terminal(Some(true));
3698 if console.is_dumb_terminal() {
3699 return;
3701 }
3702
3703 console.set_alt_screen(true).unwrap();
3704 let out = console.get_captured();
3705 assert!(out.contains("\x1b[?1049h"));
3706 assert!(out.contains("\x1b[H"));
3707 }
3708
3709 #[test]
3710 fn test_print_segments_does_not_emit_osc8_when_not_terminal() {
3711 let mut console = Console::capture();
3712 let mut segments = Segments::new();
3713 segments.push(Segment::new_with_meta(
3714 "X",
3715 StyleMeta::with_link("https://example.com"),
3716 ));
3717 console.print_segments(&segments).unwrap();
3718 let out = console.get_captured();
3719 assert!(out.contains("X"));
3720 assert!(!out.contains("\x1b]8;"));
3721 }
3722
3723 #[test]
3724 fn test_print_segments_emits_osc8_for_segment_meta_link() {
3725 let mut console = Console::with_writer(
3726 Vec::new(),
3727 ConsoleOptions {
3728 is_terminal: true,
3729 ..Default::default()
3730 },
3731 );
3732 console.set_force_terminal(Some(true));
3733 if console.is_dumb_terminal() {
3734 return;
3736 }
3737
3738 let mut segments = Segments::new();
3739 segments.push(Segment::new_with_meta(
3740 "X",
3741 StyleMeta::with_link("https://example.com"),
3742 ));
3743 console.print_segments(&segments).unwrap();
3744 let out = console.get_captured();
3745 assert!(out.contains("\x1b]8;"));
3746 assert!(out.contains("https://example.com"));
3747 assert!(out.contains("\x1b]8;;\x1b\\"));
3748 }
3749
3750 #[test]
3751 fn test_print_segments_osc8_link_id_stable_per_console() {
3752 let mut console = Console::with_writer(
3753 Vec::new(),
3754 ConsoleOptions {
3755 is_terminal: true,
3756 ..Default::default()
3757 },
3758 );
3759 console.set_force_terminal(Some(true));
3760 if console.is_dumb_terminal() {
3761 return;
3762 }
3763
3764 let mut segments = Segments::new();
3765 segments.push(Segment::new_with_meta(
3766 "X",
3767 StyleMeta::with_link("https://example.com"),
3768 ));
3769
3770 console.print_segments(&segments).unwrap();
3771 let out1 = console.get_captured();
3772 assert!(out1.contains("id=richrs-1;https://example.com"));
3773
3774 console.clear_captured();
3775 console.print_segments(&segments).unwrap();
3776 let out2 = console.get_captured();
3777 assert!(out2.contains("id=richrs-1;https://example.com"));
3778 }
3779
3780 #[test]
3781 fn test_print_text_from_ansi_emits_osc8_lifecycle_from_style_meta() {
3782 let mut console = Console::with_writer(
3783 Vec::new(),
3784 ConsoleOptions {
3785 is_terminal: true,
3786 ..Default::default()
3787 },
3788 );
3789 console.set_force_terminal(Some(true));
3790 if console.is_dumb_terminal() {
3791 return;
3792 }
3793
3794 let text = Text::from_ansi("\x1b]8;id=src42;https://example.com\x07Link\x1b]8;;\x07 done");
3795 console.print(&text, None, None, None, false, "").unwrap();
3796 let out = console.get_captured();
3797
3798 let open = "\x1b]8;id=src42;https://example.com\x1b\\";
3799 let close = "\x1b]8;;\x1b\\";
3800 assert!(out.contains(open));
3801 assert!(out.contains(close));
3802
3803 let open_pos = out.find(open).unwrap();
3804 let link_text_pos = out.find("Link").unwrap();
3805 let close_pos = out.find(close).unwrap();
3806 let plain_pos = out.rfind(" done").unwrap();
3807 assert!(open_pos < link_text_pos);
3808 assert!(link_text_pos < close_pos);
3809 assert!(close_pos < plain_pos);
3810 }
3811
3812 #[test]
3813 fn test_print_segments_closes_hyperlink_before_tail_reset() {
3814 let mut console = Console::with_writer(
3815 Vec::new(),
3816 ConsoleOptions {
3817 is_terminal: true,
3818 ..Default::default()
3819 },
3820 );
3821 console.set_force_terminal(Some(true));
3822 if console.is_dumb_terminal() {
3823 return;
3824 }
3825
3826 let mut segments = Segments::new();
3827 segments.push(Segment::styled_with_meta(
3828 "X",
3829 Style::new().with_bold(true),
3830 StyleMeta::with_link("https://example.com"),
3831 ));
3832
3833 console.print_segments(&segments).unwrap();
3834 let out = console.get_captured();
3835 let close_pos = out.find("\x1b]8;;\x1b\\").unwrap();
3836 let reset_pos = out.rfind("\x1b[0m").unwrap();
3837 assert!(close_pos < reset_pos);
3838 }
3839
3840 #[test]
3841 fn test_parse_windows_render_mode_defaults_to_streaming() {
3842 assert_eq!(
3843 parse_windows_render_mode(None),
3844 WindowsRenderMode::Streaming
3845 );
3846 assert_eq!(
3847 parse_windows_render_mode(Some("invalid")),
3848 WindowsRenderMode::Streaming
3849 );
3850 }
3851
3852 #[test]
3853 fn test_parse_windows_render_mode_values() {
3854 assert_eq!(
3855 parse_windows_render_mode(Some("segment")),
3856 WindowsRenderMode::Segment
3857 );
3858 assert_eq!(
3859 parse_windows_render_mode(Some("streaming")),
3860 WindowsRenderMode::Streaming
3861 );
3862 assert_eq!(
3863 parse_windows_render_mode(Some(" StReAmInG ")),
3864 WindowsRenderMode::Streaming
3865 );
3866 }
3867
3868 #[test]
3871 fn test_console_width() {
3872 let console = Console::capture_with_options(ConsoleOptions {
3873 max_width: 120,
3874 ..Default::default()
3875 });
3876 assert_eq!(console.width(), 120);
3877 }
3878
3879 #[test]
3880 fn test_console_set_size() {
3881 let mut console = Console::capture();
3882 console.set_size(100, 50);
3883 assert_eq!(console.width(), 100);
3884 assert_eq!(console.height(), 50);
3885 assert_eq!(console.size(), (100, 50));
3886 }
3887
3888 #[test]
3889 fn test_console_force_terminal() {
3890 let mut console = Console::capture();
3891 assert!(!console.is_terminal()); console.set_force_terminal(Some(true));
3894 assert!(console.is_terminal());
3895
3896 console.set_force_terminal(Some(false));
3897 assert!(!console.is_terminal());
3898 }
3899
3900 #[test]
3901 fn test_console_quiet_mode() {
3902 let mut console = Console::capture();
3903 console.set_quiet(true);
3904 console.print_text("This should not appear").unwrap();
3905 assert!(console.get_captured().is_empty());
3906 }
3907
3908 #[test]
3909 fn test_console_markup_emoji_highlight() {
3910 let mut console = Console::capture();
3911
3912 assert!(console.is_markup_enabled());
3913 console.set_markup_enabled(false);
3914 assert!(!console.is_markup_enabled());
3915
3916 assert!(console.is_emoji_enabled());
3917 console.set_emoji_enabled(false);
3918 assert!(!console.is_emoji_enabled());
3919
3920 assert!(console.is_highlight_enabled());
3921 console.set_highlight_enabled(false);
3922 assert!(!console.is_highlight_enabled());
3923 }
3924
3925 #[test]
3926 fn test_console_tab_size() {
3927 let mut console = Console::capture();
3928 assert_eq!(console.tab_size(), 8);
3929 console.set_tab_size(4);
3930 assert_eq!(console.tab_size(), 4);
3931 }
3932
3933 #[test]
3934 fn test_console_encoding() {
3935 let mut console = Console::capture();
3936 assert_eq!(console.encoding(), "utf-8");
3937
3938 console.set_encoding("latin-1");
3939 assert_eq!(console.encoding(), "latin-1");
3940 assert_eq!(console.options().encoding, "latin-1");
3941 }
3942
3943 #[test]
3946 fn test_console_render_text() {
3947 let console = Console::with_options(ConsoleOptions::default());
3949 let text = Text::plain("Hello, World!");
3950 let segments = console.render(&text);
3951 assert!(!segments.is_empty());
3952
3953 let combined: String = segments.iter().map(|s| s.text.to_string()).collect();
3954 assert_eq!(combined, "Hello, World!");
3955 }
3956
3957 #[test]
3958 fn test_console_render_str() {
3959 let console = Console::capture();
3960 let text = console.render_str("Hello", None, None, None, None);
3961 assert_eq!(text.plain_text(), "Hello");
3962 }
3963
3964 #[test]
3965 fn test_console_render_str_with_emoji() {
3966 let console = Console::capture();
3967 let text = console.render_str(":smile:", None, Some(true), None, None);
3968 assert!(!text.plain_text().is_empty());
3970 }
3971
3972 #[test]
3975 fn test_console_print_renderable() {
3976 let mut console = Console::capture();
3977 let text = Text::plain("Hello");
3978 console.print(&text, None, None, None, false, "\n").unwrap();
3979 let output = console.get_captured();
3980 assert!(output.contains("Hello"));
3981 }
3982
3983 #[test]
3984 fn test_console_print_with_style() {
3985 let mut console = Console::capture();
3986 let text = Text::plain("Styled");
3987 let style = Style::new().with_bold(true);
3988 console
3989 .print(&text, Some(style), None, None, false, "\n")
3990 .unwrap();
3991 let output = console.get_captured();
3992 assert!(output.contains("Styled"));
3993 }
3994
3995 #[test]
3996 fn test_console_rule() {
3997 let mut console = Console::capture();
3998 console.rule(None).unwrap();
3999 let output = console.get_captured();
4000 assert!(output.contains("─"));
4001 }
4002
4003 #[test]
4004 fn test_console_rule_with_title() {
4005 let mut console = Console::capture();
4006 console.rule(Some("Title")).unwrap();
4007 let output = console.get_captured();
4008 assert!(output.contains("Title"));
4009 assert!(output.contains("─"));
4010 }
4011
4012 #[test]
4013 fn test_console_line() {
4014 let mut console = Console::capture();
4015 console.line(3).unwrap();
4016 let output = console.get_captured();
4017 assert_eq!(output.matches('\n').count(), 3);
4018 }
4019
4020 #[test]
4023 fn test_console_measure() {
4024 let console = Console::capture();
4025 let text = Text::plain("Hello World");
4026 let measurement = console.measure(&text, None);
4027 assert!(measurement.minimum > 0);
4028 assert!(measurement.maximum >= measurement.minimum);
4029 }
4030
4031 #[test]
4034 fn test_console_alt_screen_tracking() {
4035 let console = Console::capture();
4036 assert!(!console.is_alt_screen());
4037 }
4039
4040 #[test]
4043 fn test_console_theme_stack() {
4044 let mut console = Console::capture();
4045 let theme = Theme::default();
4046 console.push_theme(theme.clone());
4047 assert!(console.pop_theme().is_ok());
4048 }
4049
4050 #[test]
4053 fn test_color_system_detection_no_terminal() {
4054 let result = Console::<Stdout>::detect_color_system_static(false);
4055 assert!(result.is_none());
4056 }
4057
4058 #[test]
4061 fn test_console_setters_sync_to_options() {
4062 let mut console = Console::capture();
4063
4064 console.set_markup_enabled(false);
4066 assert!(!console.is_markup_enabled());
4067 assert!(!console.options().markup_enabled);
4068
4069 console.set_emoji_enabled(false);
4071 assert!(!console.is_emoji_enabled());
4072 assert!(!console.options().emoji_enabled);
4073
4074 console.set_highlight_enabled(false);
4076 assert!(!console.is_highlight_enabled());
4077 assert!(!console.options().highlight_enabled);
4078
4079 console.set_tab_size(4);
4081 assert_eq!(console.tab_size(), 4);
4082 assert_eq!(console.options().tab_size, 4);
4083
4084 console.set_encoding("cp1252");
4086 assert_eq!(console.encoding(), "cp1252");
4087 assert_eq!(console.options().encoding, "cp1252");
4088
4089 console.set_color_system(Some(ColorSystem::TrueColor));
4091 assert_eq!(console.color_system(), Some(ColorSystem::TrueColor));
4092 assert_eq!(console.options().color_system, Some(ColorSystem::TrueColor));
4093 }
4094
4095 #[test]
4096 fn test_console_options_with_state() {
4097 let mut console = Console::capture();
4098
4099 console.set_markup_enabled(false);
4101 console.set_tab_size(2);
4102
4103 let opts = console.options_with_state();
4105 assert!(!opts.markup_enabled);
4106 assert_eq!(opts.tab_size, 2);
4107 }
4108
4109 #[test]
4110 fn test_with_options_initializes_from_options() {
4111 let mut options = ConsoleOptions::default();
4113 options.markup_enabled = false;
4114 options.emoji_enabled = false;
4115 options.tab_size = 4;
4116 options.encoding = "ascii".to_string();
4117 options.color_system = Some(ColorSystem::Standard);
4118
4119 let console = Console::with_options(options);
4121
4122 assert!(!console.is_markup_enabled());
4124 assert!(!console.is_emoji_enabled());
4125 assert_eq!(console.tab_size(), 4);
4126 assert_eq!(console.encoding(), "ascii");
4127 assert_eq!(console.color_system(), Some(ColorSystem::Standard));
4128 }
4129
4130 #[test]
4131 fn test_sync_from_options() {
4132 let mut console = Console::capture();
4133
4134 console.options_mut().markup_enabled = false;
4136 console.options_mut().tab_size = 2;
4137
4138 assert!(console.is_markup_enabled()); assert_eq!(console.tab_size(), 8); console.sync_from_options();
4144
4145 assert!(!console.is_markup_enabled());
4147 assert_eq!(console.tab_size(), 2);
4148 }
4149
4150 #[test]
4151 fn test_sync_theme_to_options() {
4152 let mut console = Console::capture();
4153
4154 let mut custom_theme = Theme::empty();
4156 custom_theme.add_style("direct.style", Style::new().with_italic(true));
4157 console.theme_stack_mut().push_theme(custom_theme);
4158
4159 assert!(console.theme_stack().get_style("direct.style").is_some());
4161 assert!(
4162 console
4163 .options()
4164 .theme_stack
4165 .get_style("direct.style")
4166 .is_none()
4167 ); console.sync_theme_to_options();
4171
4172 assert!(
4174 console
4175 .options()
4176 .theme_stack
4177 .get_style("direct.style")
4178 .is_some()
4179 );
4180 }
4181
4182 #[test]
4183 fn test_nested_renderable_gets_state() {
4184 use crate::padding::Padding;
4188
4189 let mut console = Console::capture();
4190 console.set_markup_enabled(false);
4191
4192 let text = Text::plain("Hello");
4194 let padded = Padding::new(Box::new(text), 1);
4195
4196 let options = console.options().clone();
4198 let segments = padded.render(&Console::with_options(options.clone()), &options);
4199
4200 assert!(!segments.is_empty());
4202 }
4203
4204 #[test]
4205 fn test_theme_push_syncs_to_options() {
4206 let mut console = Console::capture();
4207
4208 let initial_depth = console.theme_stack().depth();
4210 let initial_opts_depth = console.options().theme_stack.depth();
4211 assert_eq!(initial_depth, initial_opts_depth);
4212
4213 let mut custom_theme = Theme::empty();
4215 custom_theme.add_style("test.style", Style::new().with_bold(true));
4216 console.push_theme(custom_theme);
4217
4218 assert_eq!(console.theme_stack().depth(), initial_depth + 1);
4220 assert_eq!(console.options().theme_stack.depth(), initial_depth + 1);
4221
4222 assert!(console.theme_stack().get_style("test.style").is_some());
4224 assert!(
4225 console
4226 .options()
4227 .theme_stack
4228 .get_style("test.style")
4229 .is_some()
4230 );
4231
4232 console.pop_theme().unwrap();
4234
4235 assert_eq!(console.theme_stack().depth(), initial_depth);
4237 assert_eq!(console.options().theme_stack.depth(), initial_depth);
4238 }
4239
4240 #[test]
4243 fn test_console_options_is_send_sync() {
4244 fn assert_send<T: Send>() {}
4245 fn assert_sync<T: Sync>() {}
4246 assert_send::<ConsoleOptions>();
4247 assert_sync::<ConsoleOptions>();
4248 }
4249
4250 #[test]
4253 fn test_pager_options_default() {
4254 let opts = PagerOptions::default();
4255 assert!(!opts.styles);
4256 }
4257
4258 #[test]
4259 fn test_pager_options_with_styles() {
4260 let opts = PagerOptions::new().with_styles(true);
4261 assert!(opts.styles);
4262 }
4263
4264 #[test]
4265 fn test_pager_context_captures_text() {
4266 let console = Console::new();
4267 let mut pager = console.pager(None);
4268
4269 pager.print_text("Hello").unwrap();
4270 pager.print_text("World").unwrap();
4271
4272 let buffer = pager.get_buffer_string();
4273 assert!(buffer.contains("Hello"));
4274 assert!(buffer.contains("World"));
4275
4276 pager.buffer.clear();
4278 }
4279
4280 #[test]
4281 fn test_pager_context_captures_renderable() {
4282 let console = Console::new();
4283 let mut pager = console.pager(None);
4284
4285 let text = Text::plain("Rendered text");
4286 pager.print(&text, None, None, None, false, "\n").unwrap();
4287
4288 let buffer = pager.get_buffer_string();
4289 assert!(buffer.contains("Rendered text"));
4290
4291 pager.buffer.clear();
4293 }
4294
4295 #[test]
4298 fn test_console_new_with_record() {
4299 let console = Console::new_with_record();
4300 assert!(console.is_recording());
4301 }
4302
4303 #[test]
4304 fn test_console_set_record() {
4305 let mut console = Console::new();
4306 assert!(!console.is_recording());
4307
4308 console.set_record(true);
4309 assert!(console.is_recording());
4310
4311 console.set_record(false);
4312 assert!(!console.is_recording());
4313 }
4314
4315 #[test]
4316 fn test_console_record_buffer() {
4317 let mut console = Console::new_with_record();
4318 console.options_mut().is_terminal = false;
4319 console.options_mut().max_width = 80;
4320
4321 console.print_text("Hello").unwrap();
4323
4324 let buffer = console.get_record_buffer();
4326 assert!(!buffer.is_empty());
4327
4328 let has_hello = buffer.iter().any(|s| s.text.contains("Hello"));
4330 assert!(has_hello, "Record buffer should contain 'Hello'");
4331
4332 console.clear_record_buffer();
4334 let buffer = console.get_record_buffer();
4335 assert!(buffer.is_empty());
4336 }
4337
4338 #[test]
4339 fn test_export_svg_basic() {
4340 let mut console = Console::new_with_record();
4341 console.options_mut().is_terminal = false;
4342 console.options_mut().max_width = 40;
4343
4344 console.print_text("Hello, World!").unwrap();
4345
4346 let svg = console.export_svg("Test", None, true, None, 0.61, None);
4347
4348 assert!(svg.contains("<svg"));
4350 assert!(svg.contains("</svg>"));
4351 assert!(svg.contains("Hello"));
4352 assert!(svg.contains("rich-terminal"));
4353
4354 let buffer = console.get_record_buffer();
4356 assert!(buffer.is_empty());
4357 }
4358
4359 #[test]
4360 fn test_export_svg_with_custom_title() {
4361 let mut console = Console::new_with_record();
4362 console.options_mut().is_terminal = false;
4363 console.options_mut().max_width = 40;
4364
4365 console.print_text("Test").unwrap();
4366
4367 let svg = console.export_svg("My Custom Title", None, true, None, 0.61, None);
4368
4369 assert!(svg.contains("My Custom Title"));
4370 }
4371
4372 #[test]
4373 fn test_export_svg_with_unique_id() {
4374 let mut console = Console::new_with_record();
4375 console.options_mut().is_terminal = false;
4376 console.options_mut().max_width = 40;
4377
4378 console.print_text("Test").unwrap();
4379
4380 let svg = console.export_svg("Test", None, true, None, 0.61, Some("my-unique-id"));
4381
4382 assert!(svg.contains("my-unique-id"));
4383 }
4384
4385 #[test]
4386 fn test_export_svg_escape_text() {
4387 let mut console = Console::new_with_record();
4388 console.options_mut().is_terminal = false;
4389 console.options_mut().max_width = 80;
4390
4391 console.print_text("<script>alert('XSS')</script>").unwrap();
4392
4393 let svg = console.export_svg("Test", None, true, None, 0.61, None);
4394
4395 assert!(svg.contains("<"));
4397 assert!(svg.contains(">"));
4398 assert!(!svg.contains("<script>"));
4399 }
4400
4401 #[test]
4402 fn test_export_svg_no_clear() {
4403 let mut console = Console::new_with_record();
4404 console.options_mut().is_terminal = false;
4405 console.options_mut().max_width = 40;
4406
4407 console.print_text("Hello").unwrap();
4408
4409 let _svg = console.export_svg("Test", None, false, None, 0.61, None);
4411
4412 let buffer = console.get_record_buffer();
4414 assert!(!buffer.is_empty());
4415 }
4416
4417 #[test]
4418 fn test_export_html_basic() {
4419 let mut console = Console::new_with_record();
4420 console.options_mut().is_terminal = false;
4421 console.options_mut().max_width = 40;
4422
4423 console.print_text("Hello, World!").unwrap();
4424
4425 let html = console.export_html(None, true, None);
4426 assert!(html.contains("<!DOCTYPE html>"));
4427 assert!(html.contains("Hello, World!"));
4428
4429 let buffer = console.get_record_buffer();
4431 assert!(buffer.is_empty());
4432 }
4433
4434 #[test]
4435 fn test_export_html_link_emits_anchor_tag() {
4436 let mut console = Console::new_with_record();
4437 console.options_mut().is_terminal = false;
4438
4439 let text =
4440 Text::from_markup("[link=https://textualize.io]Textualize.io[/link]", false).unwrap();
4441 console.print(&text, None, None, None, false, "\n").unwrap();
4442
4443 let html = console.export_html(None, true, None);
4444 assert!(html.contains("<a"));
4445 assert!(html.contains("href=\"https://textualize.io\""));
4446 assert!(html.contains("Textualize.io"));
4447 }
4448
4449 #[test]
4450 fn test_export_html_escapes_text() {
4451 let mut console = Console::new_with_record();
4452 console.options_mut().is_terminal = false;
4453
4454 console.print_text("<script>alert('XSS')</script>").unwrap();
4455 let html = console.export_html(None, true, None);
4456 assert!(html.contains("<script>"));
4457 assert!(!html.contains("<script>"));
4458 }
4459}