1use std::io::{IsTerminal, Write};
9
10use crate::color::ColorSystem;
11use crate::protocol::{Highlighter, Renderable};
12use crate::segment::Segment;
13use crate::style::Style;
14use crate::text::Text;
15use crate::theme::Theme;
16
17const DEFAULT_WIDTH: usize = 80;
18const DEFAULT_HEIGHT: usize = 25;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum Justify {
24 #[default]
26 Default,
27 Left,
28 Center,
29 Right,
30 Full,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum Overflow {
37 #[default]
39 Fold,
40 Crop,
42 Ellipsis,
44 Ignore,
46}
47
48#[derive(Debug, Clone)]
53pub struct ConsoleOptions {
54 pub min_width: usize,
55 pub max_width: usize,
56 pub height: Option<usize>,
57 pub justify: Justify,
58 pub overflow: Option<Overflow>,
61 pub no_wrap: Option<bool>,
64}
65
66impl ConsoleOptions {
67 pub fn update_width(&self, width: usize) -> ConsoleOptions {
70 let mut options = self.clone();
73 options.min_width = width;
74 options.max_width = width;
75 options
76 }
77
78 pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
81 let mut options = self.update_width(width);
82 options.height = Some(height);
83 options
84 }
85}
86
87pub struct Console {
90 color_system: Option<ColorSystem>,
91 width: usize,
92 height: usize,
93 is_terminal: bool,
94 no_color: bool,
95 emoji: bool,
96 highlight: bool,
97 legacy_windows: bool,
98 safe_box: bool,
99 ascii_only: bool,
100 theme: Theme,
101 base_style: Style,
102 highlighters: Vec<Box<dyn Highlighter + Send>>,
103 record_buffer: std::cell::RefCell<Vec<Segment>>,
106 capturing: std::cell::Cell<bool>,
107}
108
109impl Default for Console {
110 fn default() -> Self {
111 Console::new()
112 }
113}
114
115impl Console {
116 pub fn new() -> Self {
118 ConsoleBuilder::new().build()
119 }
120
121 pub fn builder() -> ConsoleBuilder {
123 ConsoleBuilder::new()
124 }
125
126 pub fn color_system(&self) -> Option<ColorSystem> {
128 if self.no_color {
129 None
130 } else {
131 self.color_system
132 }
133 }
134
135 pub fn width(&self) -> usize {
137 self.width
138 }
139
140 pub fn height(&self) -> usize {
143 self.height
144 }
145
146 pub fn is_terminal(&self) -> bool {
148 self.is_terminal
149 }
150
151 pub fn legacy_windows(&self) -> bool {
153 self.legacy_windows
154 }
155
156 pub fn safe_box(&self) -> bool {
158 self.safe_box
159 }
160
161 pub fn ascii_only(&self) -> bool {
163 self.ascii_only
164 }
165
166 pub fn theme(&self) -> &Theme {
168 &self.theme
169 }
170
171 pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
174 self.theme.get_style(style)
175 }
176
177 pub fn base_style(&self) -> &Style {
179 &self.base_style
180 }
181
182 pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
186 self.highlighters.push(highlighter);
187 }
188
189 pub fn options(&self) -> ConsoleOptions {
191 ConsoleOptions {
192 min_width: 1,
193 max_width: self.width,
194 height: None,
195 justify: Justify::Default,
196 overflow: None,
197 no_wrap: None,
198 }
199 }
200
201 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
208 let segments = self.render_segments(renderable);
209 self.segments_to_string(&segments)
210 }
211
212 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
215 let mut options = self.options();
216 if options.justify == Justify::Default {
217 let measurement = renderable.measure(self, &options);
218 options.max_width = measurement.maximum.min(options.max_width).max(1);
219 }
220 let segments = renderable.rich_render(self, &options);
221 Segment::crop_lines(&segments, self.width)
226 }
227
228 fn emit(&self, segments: Vec<Segment>) {
231 if segments.is_empty() {
232 return;
233 }
234 if self.capturing.get() {
235 let mut buffer = self.record_buffer.borrow_mut();
236 buffer.extend(segments);
237 buffer.push(Segment::line());
238 return;
239 }
240 let mut output = self.segments_to_string(&segments);
241 output.push('\n');
242 let stdout = std::io::stdout();
243 let mut lock = stdout.lock();
244 let _ = write!(lock, "{output}");
245 }
246
247 pub fn render_lines(
253 &self,
254 renderable: &dyn Renderable,
255 options: &ConsoleOptions,
256 pad: bool,
257 ) -> Vec<Vec<Segment>> {
258 let segments = renderable.rich_render(self, options);
259 let mut lines = Segment::split_lines(&segments);
260 if pad {
261 for line in &mut lines {
262 *line = Segment::adjust_line_length(line, options.max_width, Some(Style::new()));
263 }
264 }
265 if let Some(height) = options.height {
269 lines.truncate(height);
270 while lines.len() < height {
271 lines.push(if pad {
272 vec![Segment::new(
273 " ".repeat(options.max_width),
274 Some(Style::new()),
275 )]
276 } else {
277 Vec::new()
278 });
279 }
280 }
281 lines
282 }
283
284 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
288 let segments = self.render_segments(renderable);
289 let mut out = self.segments_to_string(&segments);
290 if !segments.is_empty() {
291 out.push('\n');
292 }
293 out
294 }
295
296 pub fn print(&self, renderable: &dyn Renderable) {
298 let segments = self.render_segments(renderable);
299 self.emit(segments);
300 }
301
302 pub fn control(&self, control: &crate::control::Control) {
307 if !self.is_terminal {
308 return;
309 }
310 let text = control.as_str();
311 if !text.is_empty() {
312 let stdout = std::io::stdout();
313 let mut lock = stdout.lock();
314 let _ = write!(lock, "{text}");
315 }
316 }
317
318 pub fn show_cursor(&self, show: bool) {
320 self.control(&crate::control::Control::show_cursor(show));
321 }
322
323 pub fn clear(&self) {
325 self.control(&crate::control::Control::clear());
326 }
327
328 pub fn bell(&self) {
330 self.control(&crate::control::Control::bell());
331 }
332
333 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
340 let segments = self.record(f);
341 self.segments_to_string(&segments)
342 }
343
344 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
347 let segments = self.record(f);
348 segments_to_plain(&segments)
349 }
350
351 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
359 self.page_with(&crate::pager::SystemPager, styles, f)
360 }
361
362 pub fn page_with(
365 &self,
366 pager: &dyn crate::pager::Pager,
367 styles: bool,
368 f: impl FnOnce(&Console),
369 ) -> std::io::Result<()> {
370 let segments = self.record(f);
371 let content = if styles {
372 self.segments_to_string(&segments)
373 } else {
374 segments_to_plain(&segments)
375 };
376 pager.show(&content)
377 }
378
379 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
383 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
384 }
385
386 pub fn export_html_themed(
392 &self,
393 theme: &crate::terminal_theme::TerminalTheme,
394 f: impl FnOnce(&Console),
395 ) -> String {
396 let segments = self.record(f);
397 crate::export::export_html_inline(&segments, theme)
398 }
399
400 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
404 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
405 }
406
407 pub fn export_html_classes_themed(
410 &self,
411 theme: &crate::terminal_theme::TerminalTheme,
412 f: impl FnOnce(&Console),
413 ) -> String {
414 let segments = self.record(f);
415 crate::export::export_html_classes(&segments, theme)
416 }
417
418 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
429 self.export_svg_themed(
430 &crate::terminal_theme::SVG_EXPORT_THEME,
431 title,
432 unique_id,
433 f,
434 )
435 }
436
437 pub fn export_svg_themed(
440 &self,
441 theme: &crate::terminal_theme::TerminalTheme,
442 title: &str,
443 unique_id: &str,
444 f: impl FnOnce(&Console),
445 ) -> String {
446 let segments = self.record(f);
447 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
448 }
449
450 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
467 self.record(f)
468 }
469
470 fn record(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
473 let previous = std::mem::take(&mut *self.record_buffer.borrow_mut());
474 let was_capturing = self.capturing.replace(true);
475 f(self);
476 let captured = std::mem::replace(&mut *self.record_buffer.borrow_mut(), previous);
477 self.capturing.set(was_capturing);
478 captured
479 }
480
481 pub fn print_str(&self, content: &str) {
484 let text = self.build_text(content);
485 self.print(&text);
486 }
487
488 pub fn render_str_to_string(&self, content: &str) -> String {
490 let text = self.build_text(content);
491 self.render_to_string(&text)
492 }
493
494 pub fn build_text(&self, content: &str) -> Text {
498 self.try_build_text(content)
503 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
504 }
505
506 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
510 let expanded = self.expand_emoji(content);
511 let markup = Text::from_markup(&expanded)?;
512
513 let mut text = self.decorate(Text::new(markup.plain()));
524 for span in markup.spans() {
525 text.push_span(span.clone());
526 }
527 Ok(text)
528 }
529
530 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
532 self.print(&self.try_build_text(content)?);
533 Ok(())
534 }
535
536 pub fn try_print_justified(
539 &self,
540 content: &str,
541 justify: Justify,
542 ) -> crate::errors::Result<()> {
543 let text = self.try_build_text(content)?;
544 let mut options = self.options();
545 options.justify = justify;
546 self.emit(text.rich_render(self, &options));
547 Ok(())
548 }
549
550 pub(crate) fn expand_emoji(&self, content: &str) -> String {
553 if self.emoji {
554 crate::emoji::replace(content)
555 } else {
556 content.to_string()
557 }
558 }
559
560 fn decorate(&self, mut text: Text) -> Text {
563 for highlighter in &self.highlighters {
564 highlighter.highlight(&mut text);
565 }
566 if self.highlight {
567 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
568 }
569 text
570 }
571
572 pub fn print_justified(&self, content: &str, justify: Justify) {
575 let text = self.build_text(content);
576 let mut options = self.options();
577 options.justify = justify;
578 let segments = text.rich_render(self, &options);
579 self.emit(segments);
580 }
581
582 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
587 let text = self.build_text(content);
588 let mut options = self.options();
589 options.justify = justify;
590 let segments = text.rich_render(self, &options);
591 self.segments_to_string(&segments)
592 }
593
594 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
597 let system = self.color_system();
598 let mut out = String::new();
599 for segment in segments {
600 if segment.control && !self.is_terminal {
603 continue;
604 }
605 match (&segment.style, system) {
606 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
607 _ => out.push_str(&segment.text),
608 }
609 }
610 out
611 }
612}
613
614fn segments_to_plain(segments: &[Segment]) -> String {
617 segments
618 .iter()
619 .filter(|s| !s.control)
620 .map(|s| s.text.as_str())
621 .collect()
622}
623
624impl Renderable for Text {
625 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
626 if self.is_empty() {
629 return vec![Segment::new("", None)];
630 }
631 let justify = if self.get_justify() != Justify::Default {
634 self.get_justify()
635 } else {
636 options.justify
637 };
638 let overflow = self
642 .get_overflow()
643 .or(options.overflow)
644 .unwrap_or(Overflow::Fold);
645 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
646 self.render_joined_wrapped(
647 console.theme(),
648 console.base_style(),
649 options.max_width,
650 justify,
651 overflow,
652 no_wrap,
653 )
654 }
655
656 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
657 let (minimum, maximum) = self.measurement();
658 crate::measure::Measurement::new(
659 minimum.min(options.max_width),
660 maximum.min(options.max_width),
661 )
662 }
663}
664
665pub struct ConsoleBuilder {
667 force_terminal: Option<bool>,
668 color_system: Option<ColorSystem>,
669 color_system_set: bool,
670 width: Option<usize>,
671 height: Option<usize>,
672 no_color: Option<bool>,
673 emoji: Option<bool>,
674 highlight: Option<bool>,
675 legacy_windows: Option<bool>,
676 safe_box: Option<bool>,
677 ascii_only: Option<bool>,
678 theme: Option<Theme>,
679}
680
681impl ConsoleBuilder {
682 fn new() -> Self {
683 ConsoleBuilder {
684 force_terminal: None,
685 color_system: None,
686 color_system_set: false,
687 width: None,
688 height: None,
689 no_color: None,
690 emoji: None,
691 highlight: None,
692 legacy_windows: None,
693 safe_box: None,
694 ascii_only: None,
695 theme: None,
696 }
697 }
698
699 pub fn force_terminal(mut self, value: bool) -> Self {
700 self.force_terminal = Some(value);
701 self
702 }
703
704 pub fn legacy_windows(mut self, value: bool) -> Self {
706 self.legacy_windows = Some(value);
707 self
708 }
709
710 pub fn safe_box(mut self, value: bool) -> Self {
712 self.safe_box = Some(value);
713 self
714 }
715
716 pub fn ascii_only(mut self, value: bool) -> Self {
718 self.ascii_only = Some(value);
719 self
720 }
721
722 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
724 self.color_system = system;
725 self.color_system_set = true;
726 self
727 }
728
729 pub fn width(mut self, width: usize) -> Self {
730 self.width = Some(width);
731 self
732 }
733
734 pub fn height(mut self, height: usize) -> Self {
736 self.height = Some(height);
737 self
738 }
739
740 pub fn no_color(mut self, value: bool) -> Self {
741 self.no_color = Some(value);
742 self
743 }
744
745 pub fn emoji(mut self, value: bool) -> Self {
747 self.emoji = Some(value);
748 self
749 }
750
751 pub fn highlight(mut self, value: bool) -> Self {
754 self.highlight = Some(value);
755 self
756 }
757
758 pub fn theme(mut self, theme: Theme) -> Self {
759 self.theme = Some(theme);
760 self
761 }
762
763 pub fn build(self) -> Console {
764 let is_terminal = self
765 .force_terminal
766 .unwrap_or_else(|| std::io::stdout().is_terminal());
767 let no_color = self
772 .no_color
773 .unwrap_or_else(|| std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()));
774 let color_system = if self.color_system_set {
775 self.color_system
776 } else if is_terminal {
777 Some(detect_color_system())
778 } else {
779 None
780 };
781 let width = self.width.unwrap_or_else(detect_width);
782 let height = self.height.unwrap_or_else(detect_height);
783 Console {
784 color_system,
785 width,
786 height,
787 is_terminal,
788 no_color,
789 emoji: self.emoji.unwrap_or(true),
790 highlight: self.highlight.unwrap_or(true),
795 legacy_windows: self.legacy_windows.unwrap_or(false),
796 safe_box: self.safe_box.unwrap_or(true),
797 ascii_only: self.ascii_only.unwrap_or(false),
798 theme: self.theme.unwrap_or_else(Theme::default_theme),
799 base_style: Style::new(),
800 highlighters: Vec::new(),
801 record_buffer: std::cell::RefCell::new(Vec::new()),
802 capturing: std::cell::Cell::new(false),
803 }
804 }
805}
806
807fn detect_color_system() -> ColorSystem {
819 if let Some(colorterm) = std::env::var_os("COLORTERM") {
820 let colorterm = colorterm.to_string_lossy().to_ascii_lowercase();
821 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
822 return ColorSystem::Truecolor;
823 }
824 }
825
826 #[cfg(windows)]
838 {
839 let _ = anstyle_query::windows::enable_ansi_colors();
840 ColorSystem::Truecolor
841 }
842
843 #[cfg(not(windows))]
846 {
847 if let Some(term) = std::env::var_os("TERM") {
848 if term.to_string_lossy().contains("256") {
849 return ColorSystem::EightBit;
850 }
851 }
852 ColorSystem::Standard
853 }
854}
855
856fn detect_width() -> usize {
858 if let Some(columns) = std::env::var_os("COLUMNS") {
859 if let Ok(value) = columns.to_string_lossy().trim().parse::<usize>() {
860 if value > 0 {
861 return value;
862 }
863 }
864 }
865 if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
866 if w > 0 {
867 return w as usize;
868 }
869 }
870 DEFAULT_WIDTH
871}
872
873fn detect_height() -> usize {
875 if let Some(lines) = std::env::var_os("LINES") {
876 if let Ok(value) = lines.to_string_lossy().trim().parse::<usize>() {
877 if value > 0 {
878 return value;
879 }
880 }
881 }
882 if let Some((_, terminal_size::Height(h))) = terminal_size::terminal_size() {
883 if h > 0 {
884 return h as usize;
885 }
886 }
887 DEFAULT_HEIGHT
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 fn test_console() -> Console {
895 Console::builder()
896 .force_terminal(true)
897 .color_system(Some(ColorSystem::Truecolor))
898 .width(80)
899 .no_color(false)
900 .build()
901 }
902
903 #[test]
906 fn empty_text_and_empty_renderables_have_distinct_endings() {
907 let console = Console::builder().force_terminal(false).build();
908 assert_eq!(console.render_export(&Text::new("")), "\n");
909 assert_eq!(
910 console.render_export(&crate::markdown::Markdown::new("")),
911 ""
912 );
913 assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
914 }
915
916 #[test]
917 fn try_build_text_reports_bad_markup() {
918 let console = test_console();
919
920 let err = console
921 .try_build_text("[/nope]")
922 .expect_err("an unmatched closing tag must be an error");
923 assert!(
924 matches!(err, crate::errors::RichError::Markup(_)),
925 "{err:?}"
926 );
927 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
929
930 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
931 assert_eq!(strict.plain(), "hi");
932 assert_eq!(
933 strict.spans().len(),
934 console.build_text("[bold]hi[/]").spans().len()
935 );
936 }
937
938 #[test]
946 fn unknown_tag_names_render_as_no_ops() {
947 let console = test_console();
948 let text = console
949 .try_build_text("[nope]x[/]")
950 .expect("an unknown tag name is not a syntax error");
951 assert_eq!(console.render_to_string(&text), "x");
952 assert_eq!(
953 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
954 "x"
955 );
956
957 assert!(console.try_build_text("[bold]a[/italic]").is_err());
959 assert!(console.try_build_text("[/nope]").is_err());
960 }
961
962 #[test]
965 fn markup_styles_bind_at_render_not_at_parse() {
966 let themed = |definition: &str| {
967 let mut theme = Theme::default_theme();
968 theme.insert("accent", Style::parse(definition).unwrap());
969 Console::builder()
970 .force_terminal(true)
971 .color_system(Some(ColorSystem::Truecolor))
972 .width(80)
973 .no_color(false)
974 .theme(theme)
975 .build()
976 };
977 let red = themed("bold red");
978 let green = themed("underline green");
979
980 let text = red.build_text("[accent]hi[/]");
982 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
983 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
985 }
986
987 #[test]
990 fn try_build_text_expands_emoji_like_build_text() {
991 let console = test_console();
992 assert_eq!(
993 console
994 .try_build_text(":rocket: go")
995 .expect("valid")
996 .plain(),
997 console.build_text(":rocket: go").plain()
998 );
999 }
1000
1001 #[test]
1002 fn renders_markup_string() {
1003 let console = test_console();
1004 assert_eq!(
1005 console.render_str_to_string("[bold red]hi[/]"),
1006 "\x1b[1;31mhi\x1b[0m"
1007 );
1008 }
1009
1010 #[test]
1011 fn print_justify_pads_to_width() {
1012 let console = Console::builder()
1013 .force_terminal(true)
1014 .color_system(Some(ColorSystem::Truecolor))
1015 .width(10)
1016 .build();
1017 assert_eq!(
1019 console.render_justified_to_string("hi", Justify::Left),
1020 "hi "
1021 );
1022 assert_eq!(
1023 console.render_justified_to_string("hi", Justify::Center),
1024 " hi "
1025 );
1026 assert_eq!(
1027 console.render_justified_to_string("hi", Justify::Right),
1028 " hi"
1029 );
1030 }
1031
1032 #[test]
1033 fn capture_records_ansi_instead_of_stdout() {
1034 let console = Console::builder()
1035 .force_terminal(true)
1036 .color_system(Some(ColorSystem::Truecolor))
1037 .width(20)
1038 .build();
1039 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
1041 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
1042 }
1043
1044 #[test]
1045 fn themed_exports_use_the_given_palette() {
1046 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
1047
1048 let console = Console::builder()
1049 .force_terminal(true)
1050 .color_system(Some(ColorSystem::Truecolor))
1051 .width(20)
1052 .no_color(false)
1053 .build();
1054 let render = |c: &Console| c.print_str("hi");
1055
1056 let monokai = console.export_html_themed(&MONOKAI, render);
1059 assert!(
1060 monokai.contains("#0c0c0c"),
1061 "monokai bg missing:\n{monokai}"
1062 );
1063
1064 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
1065 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
1066 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
1067
1068 let classes = console.export_html_classes_themed(&MONOKAI, render);
1070 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
1071 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
1072 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
1073
1074 assert!(console.export_html(render).contains("#ffffff"));
1076 }
1077
1078 #[test]
1079 fn page_with_honors_the_styles_flag() {
1080 use std::sync::Mutex;
1081
1082 #[derive(Default)]
1083 struct Recorder(Mutex<String>);
1084 impl crate::pager::Pager for Recorder {
1085 fn show(&self, content: &str) -> std::io::Result<()> {
1086 *self.0.lock().unwrap() = content.to_string();
1087 Ok(())
1088 }
1089 }
1090
1091 let console = Console::builder()
1092 .force_terminal(true)
1093 .color_system(Some(ColorSystem::Truecolor))
1094 .width(20)
1095 .no_color(false)
1096 .build();
1097
1098 let plain = Recorder::default();
1100 console
1101 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
1102 .unwrap();
1103 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
1104
1105 let styled = Recorder::default();
1107 console
1108 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
1109 .unwrap();
1110 assert_eq!(
1111 styled.0.lock().unwrap().as_str(),
1112 "\x1b[1;31mhi\x1b[0m there\n"
1113 );
1114 }
1115
1116 #[test]
1117 fn export_text_strips_styles() {
1118 let console = Console::builder()
1119 .force_terminal(true)
1120 .color_system(Some(ColorSystem::Truecolor))
1121 .width(20)
1122 .build();
1123 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
1125 assert_eq!(out, "hi there\n");
1126 }
1127
1128 #[test]
1129 fn export_html_matches_upstream() {
1130 let console = Console::builder()
1131 .force_terminal(true)
1132 .color_system(Some(ColorSystem::Truecolor))
1133 .width(20)
1134 .no_color(false)
1135 .build();
1136 let html = console.export_html(|c| {
1137 c.print_str("[bold red]hi[/] there");
1138 c.print_str("plain line");
1139 });
1140 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
1144 assert_eq!(html, expected);
1145 }
1146
1147 #[test]
1148 fn export_html_classes_matches_upstream() {
1149 let console = Console::builder()
1150 .force_terminal(true)
1151 .color_system(Some(ColorSystem::Truecolor))
1152 .width(20)
1153 .no_color(false)
1154 .build();
1155 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
1156 let expected =
1159 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
1160 assert_eq!(html, expected);
1161 }
1162
1163 #[test]
1164 fn capture_matches_direct_render() {
1165 let console = test_console();
1166 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
1167 assert_eq!(
1168 console.capture(|c| c.print(&panel)),
1169 console.render_export(&panel)
1170 );
1171 }
1172
1173 #[test]
1174 fn no_color_strips_styles() {
1175 let console = Console::builder()
1176 .force_terminal(true)
1177 .color_system(None)
1178 .build();
1179 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
1180 }
1181}