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 render_environment: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
91 color_system: Option<ColorSystem>,
92 width: usize,
93 height: usize,
94 is_terminal: bool,
95 no_color: bool,
96 emoji: bool,
97 highlight: bool,
98 legacy_windows: bool,
99 safe_box: bool,
100 ascii_only: bool,
101 theme: Theme,
102 base_style: Style,
103 highlighters: Vec<Box<dyn Highlighter + Send>>,
104 record_buffer: std::cell::RefCell<Vec<Segment>>,
107 capturing: std::cell::Cell<bool>,
108}
109
110impl Default for Console {
111 fn default() -> Self {
112 Console::new()
113 }
114}
115
116impl Console {
117 pub fn new() -> Self {
119 ConsoleBuilder::new().build()
120 }
121
122 pub fn builder() -> ConsoleBuilder {
124 ConsoleBuilder::new()
125 }
126
127 pub fn color_system(&self) -> Option<ColorSystem> {
129 if self.no_color {
130 None
131 } else {
132 self.color_system
133 }
134 }
135
136 pub fn width(&self) -> usize {
138 self.width
139 }
140
141 pub fn height(&self) -> usize {
144 self.height
145 }
146
147 pub fn is_terminal(&self) -> bool {
149 self.is_terminal
150 }
151
152 pub fn legacy_windows(&self) -> bool {
154 self.legacy_windows
155 }
156
157 pub fn safe_box(&self) -> bool {
159 self.safe_box
160 }
161
162 pub fn ascii_only(&self) -> bool {
164 self.ascii_only
165 }
166
167 pub fn theme(&self) -> &Theme {
169 &self.theme
170 }
171
172 pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
175 self.theme.get_style(style)
176 }
177
178 pub fn base_style(&self) -> &Style {
180 &self.base_style
181 }
182
183 pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
187 self.highlighters.push(highlighter);
188 }
189
190 pub fn options(&self) -> ConsoleOptions {
192 ConsoleOptions {
193 min_width: 1,
194 max_width: self.width,
195 height: None,
196 justify: Justify::Default,
197 overflow: None,
198 no_wrap: None,
199 }
200 }
201
202 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
209 let segments = self.render_segments(renderable);
210 self.segments_to_string(&segments)
211 }
212
213 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
216 let mut options = self.options();
217 if options.justify == Justify::Default {
218 let measurement = renderable.measure(self, &options);
219 options.max_width = measurement.maximum.min(options.max_width).max(1);
220 }
221 let segments = renderable.rich_render(self, &options);
222 Segment::crop_lines(&segments, self.width)
227 }
228
229 fn emit(&self, segments: Vec<Segment>) {
232 if segments.is_empty() {
233 return;
234 }
235 if self.capturing.get() {
236 let mut buffer = self.record_buffer.borrow_mut();
237 buffer.extend(segments);
238 buffer.push(Segment::line());
239 return;
240 }
241 let mut output = self.segments_to_string(&segments);
242 output.push('\n');
243 let stdout = std::io::stdout();
244 let mut lock = stdout.lock();
245 let _ = write!(lock, "{output}");
246 }
247
248 pub fn render_lines(
254 &self,
255 renderable: &dyn Renderable,
256 options: &ConsoleOptions,
257 pad: bool,
258 ) -> Vec<Vec<Segment>> {
259 let segments = renderable.rich_render(self, options);
260 let mut lines = Segment::split_lines(&segments);
261 if pad {
262 for line in &mut lines {
263 *line = Segment::adjust_line_length(line, options.max_width, Some(Style::new()));
264 }
265 }
266 if let Some(height) = options.height {
270 lines.truncate(height);
271 while lines.len() < height {
272 lines.push(if pad {
273 vec![Segment::new(
274 " ".repeat(options.max_width),
275 Some(Style::new()),
276 )]
277 } else {
278 Vec::new()
279 });
280 }
281 }
282 lines
283 }
284
285 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
289 let segments = self.render_segments(renderable);
290 let mut out = self.segments_to_string(&segments);
291 if !segments.is_empty() {
292 out.push('\n');
293 }
294 out
295 }
296
297 pub fn print(&self, renderable: &dyn Renderable) {
299 let segments = self.render_segments(renderable);
300 self.emit(segments);
301 }
302
303 pub fn control(&self, control: &crate::control::Control) {
308 if !self.is_terminal {
309 return;
310 }
311 let text = control.as_str();
312 if !text.is_empty() {
313 let stdout = std::io::stdout();
314 let mut lock = stdout.lock();
315 let _ = write!(lock, "{text}");
316 }
317 }
318
319 pub fn show_cursor(&self, show: bool) {
321 self.control(&crate::control::Control::show_cursor(show));
322 }
323
324 pub fn clear(&self) {
326 self.control(&crate::control::Control::clear());
327 }
328
329 pub fn bell(&self) {
331 self.control(&crate::control::Control::bell());
332 }
333
334 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
341 let segments = self.record(f);
342 self.segments_to_string(&segments)
343 }
344
345 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
348 let segments = self.record(f);
349 segments_to_plain(&segments)
350 }
351
352 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
360 self.page_with(&crate::pager::SystemPager, styles, f)
361 }
362
363 pub fn page_with(
366 &self,
367 pager: &dyn crate::pager::Pager,
368 styles: bool,
369 f: impl FnOnce(&Console),
370 ) -> std::io::Result<()> {
371 let segments = self.record(f);
372 let content = if styles {
373 self.segments_to_string(&segments)
374 } else {
375 segments_to_plain(&segments)
376 };
377 pager.show(&content)
378 }
379
380 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
384 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
385 }
386
387 pub fn export_html_themed(
393 &self,
394 theme: &crate::terminal_theme::TerminalTheme,
395 f: impl FnOnce(&Console),
396 ) -> String {
397 let segments = self.record(f);
398 crate::export::export_html_inline(&segments, theme)
399 }
400
401 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
405 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
406 }
407
408 pub fn export_html_classes_themed(
411 &self,
412 theme: &crate::terminal_theme::TerminalTheme,
413 f: impl FnOnce(&Console),
414 ) -> String {
415 let segments = self.record(f);
416 crate::export::export_html_classes(&segments, theme)
417 }
418
419 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
430 self.export_svg_themed(
431 &crate::terminal_theme::SVG_EXPORT_THEME,
432 title,
433 unique_id,
434 f,
435 )
436 }
437
438 pub fn export_svg_themed(
441 &self,
442 theme: &crate::terminal_theme::TerminalTheme,
443 title: &str,
444 unique_id: &str,
445 f: impl FnOnce(&Console),
446 ) -> String {
447 let segments = self.record(f);
448 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
449 }
450
451 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
468 self.record(f)
469 }
470
471 fn record(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
474 let previous = std::mem::take(&mut *self.record_buffer.borrow_mut());
475 let was_capturing = self.capturing.replace(true);
476 f(self);
477 let captured = std::mem::replace(&mut *self.record_buffer.borrow_mut(), previous);
478 self.capturing.set(was_capturing);
479 captured
480 }
481
482 pub fn print_str(&self, content: &str) {
485 let text = self.build_text(content);
486 self.print(&text);
487 }
488
489 pub fn render_str_to_string(&self, content: &str) -> String {
491 let text = self.build_text(content);
492 self.render_to_string(&text)
493 }
494
495 pub fn build_text(&self, content: &str) -> Text {
499 self.try_build_text(content)
504 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
505 }
506
507 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
511 let expanded = self.expand_emoji(content);
512 let markup = Text::from_markup(&expanded)?;
513
514 let mut text = self.decorate(Text::new(markup.plain()));
525 for span in markup.spans() {
526 text.push_span(span.clone());
527 }
528 Ok(text)
529 }
530
531 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
533 self.print(&self.try_build_text(content)?);
534 Ok(())
535 }
536
537 pub fn try_print_justified(
540 &self,
541 content: &str,
542 justify: Justify,
543 ) -> crate::errors::Result<()> {
544 let text = self.try_build_text(content)?;
545 let mut options = self.options();
546 options.justify = justify;
547 self.emit(text.rich_render(self, &options));
548 Ok(())
549 }
550
551 pub(crate) fn expand_emoji(&self, content: &str) -> String {
554 if self.emoji {
555 crate::emoji::replace(content)
556 } else {
557 content.to_string()
558 }
559 }
560
561 fn decorate(&self, mut text: Text) -> Text {
564 for highlighter in &self.highlighters {
565 highlighter.highlight(&mut text);
566 }
567 if self.highlight {
568 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
569 }
570 text
571 }
572
573 pub fn print_justified(&self, content: &str, justify: Justify) {
576 let text = self.build_text(content);
577 let mut options = self.options();
578 options.justify = justify;
579 let segments = text.rich_render(self, &options);
580 self.emit(segments);
581 }
582
583 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
588 let text = self.build_text(content);
589 let mut options = self.options();
590 options.justify = justify;
591 let segments = text.rich_render(self, &options);
592 self.segments_to_string(&segments)
593 }
594
595 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
598 let system = self.color_system();
599 let mut out = String::new();
600 for segment in segments {
601 if segment.control && !self.is_terminal {
604 continue;
605 }
606 match (&segment.style, system) {
607 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
608 _ => out.push_str(&segment.text),
609 }
610 }
611 out
612 }
613}
614
615fn segments_to_plain(segments: &[Segment]) -> String {
618 segments
619 .iter()
620 .filter(|s| !s.control)
621 .map(|s| s.text.as_str())
622 .collect()
623}
624
625impl Renderable for Text {
626 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
627 if self.is_empty() {
630 return vec![Segment::new("", None)];
631 }
632 let justify = if self.get_justify() != Justify::Default {
635 self.get_justify()
636 } else {
637 options.justify
638 };
639 let overflow = self
643 .get_overflow()
644 .or(options.overflow)
645 .unwrap_or(Overflow::Fold);
646 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
647 self.render_joined_wrapped(
648 console.theme(),
649 console.base_style(),
650 options.max_width,
651 justify,
652 overflow,
653 no_wrap,
654 )
655 }
656
657 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
658 let (minimum, maximum) = self.measurement();
659 crate::measure::Measurement::new(
660 minimum.min(options.max_width),
661 maximum.min(options.max_width),
662 )
663 }
664}
665
666pub struct ConsoleBuilder {
668 force_terminal: Option<bool>,
669 color_system: Option<ColorSystem>,
670 color_system_set: bool,
671 width: Option<usize>,
672 height: Option<usize>,
673 no_color: Option<bool>,
674 emoji: Option<bool>,
675 highlight: Option<bool>,
676 legacy_windows: Option<bool>,
677 safe_box: Option<bool>,
678 ascii_only: Option<bool>,
679 theme: Option<Theme>,
680}
681
682impl ConsoleBuilder {
683 fn new() -> Self {
684 ConsoleBuilder {
685 force_terminal: None,
686 color_system: None,
687 color_system_set: false,
688 width: None,
689 height: None,
690 no_color: None,
691 emoji: None,
692 highlight: None,
693 legacy_windows: None,
694 safe_box: None,
695 ascii_only: None,
696 theme: None,
697 }
698 }
699
700 pub fn force_terminal(mut self, value: bool) -> Self {
701 self.force_terminal = Some(value);
702 self
703 }
704
705 pub fn legacy_windows(mut self, value: bool) -> Self {
707 self.legacy_windows = Some(value);
708 self
709 }
710
711 pub fn safe_box(mut self, value: bool) -> Self {
713 self.safe_box = Some(value);
714 self
715 }
716
717 pub fn ascii_only(mut self, value: bool) -> Self {
719 self.ascii_only = Some(value);
720 self
721 }
722
723 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
725 self.color_system = system;
726 self.color_system_set = true;
727 self
728 }
729
730 pub fn width(mut self, width: usize) -> Self {
731 self.width = Some(width);
732 self
733 }
734
735 pub fn height(mut self, height: usize) -> Self {
737 self.height = Some(height);
738 self
739 }
740
741 pub fn no_color(mut self, value: bool) -> Self {
742 self.no_color = Some(value);
743 self
744 }
745
746 pub fn emoji(mut self, value: bool) -> Self {
748 self.emoji = Some(value);
749 self
750 }
751
752 pub fn highlight(mut self, value: bool) -> Self {
755 self.highlight = Some(value);
756 self
757 }
758
759 pub fn theme(mut self, theme: Theme) -> Self {
760 self.theme = Some(theme);
761 self
762 }
763
764 pub fn build(self) -> Console {
765 let is_terminal = self
766 .force_terminal
767 .unwrap_or_else(|| std::io::stdout().is_terminal());
768 let no_color = self
773 .no_color
774 .unwrap_or_else(|| std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()));
775 let color_system = if self.color_system_set {
776 self.color_system
777 } else if is_terminal {
778 Some(detect_color_system())
779 } else {
780 None
781 };
782 let width = self.width.unwrap_or_else(detect_width);
783 let height = self.height.unwrap_or_else(detect_height);
784 Console {
785 render_environment: None,
786 color_system,
787 width,
788 height,
789 is_terminal,
790 no_color,
791 emoji: self.emoji.unwrap_or(true),
792 highlight: self.highlight.unwrap_or(true),
797 legacy_windows: self.legacy_windows.unwrap_or(false),
798 safe_box: self.safe_box.unwrap_or(true),
799 ascii_only: self.ascii_only.unwrap_or(false),
800 theme: self.theme.unwrap_or_else(Theme::default_theme),
801 base_style: Style::new(),
802 highlighters: Vec::new(),
803 record_buffer: std::cell::RefCell::new(Vec::new()),
804 capturing: std::cell::Cell::new(false),
805 }
806 }
807}
808
809fn detect_color_system() -> ColorSystem {
821 if let Some(colorterm) = std::env::var_os("COLORTERM") {
822 let colorterm = colorterm.to_string_lossy().to_ascii_lowercase();
823 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
824 return ColorSystem::Truecolor;
825 }
826 }
827
828 #[cfg(windows)]
840 {
841 let _ = anstyle_query::windows::enable_ansi_colors();
842 ColorSystem::Truecolor
843 }
844
845 #[cfg(not(windows))]
848 {
849 if let Some(term) = std::env::var_os("TERM") {
850 if term.to_string_lossy().contains("256") {
851 return ColorSystem::EightBit;
852 }
853 }
854 ColorSystem::Standard
855 }
856}
857
858fn detect_width() -> usize {
860 if let Some(columns) = std::env::var_os("COLUMNS") {
861 if let Ok(value) = columns.to_string_lossy().trim().parse::<usize>() {
862 if value > 0 {
863 return value;
864 }
865 }
866 }
867 if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
868 if w > 0 {
869 return w as usize;
870 }
871 }
872 DEFAULT_WIDTH
873}
874
875fn detect_height() -> usize {
877 if let Some(lines) = std::env::var_os("LINES") {
878 if let Ok(value) = lines.to_string_lossy().trim().parse::<usize>() {
879 if value > 0 {
880 return value;
881 }
882 }
883 }
884 if let Some((_, terminal_size::Height(h))) = terminal_size::terminal_size() {
885 if h > 0 {
886 return h as usize;
887 }
888 }
889 DEFAULT_HEIGHT
890}
891
892impl crate::protocol::ConsoleEnvironment for Console {
893 fn set_render_environment(
894 &mut self,
895 value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
896 ) {
897 self.render_environment = value;
898 }
899 fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
900 self.render_environment.as_deref()
901 }
902}
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907
908 fn test_console() -> Console {
909 Console::builder()
910 .force_terminal(true)
911 .color_system(Some(ColorSystem::Truecolor))
912 .width(80)
913 .no_color(false)
914 .build()
915 }
916
917 #[test]
920 fn empty_text_and_empty_renderables_have_distinct_endings() {
921 let console = Console::builder().force_terminal(false).build();
922 assert_eq!(console.render_export(&Text::new("")), "\n");
923 assert_eq!(
924 console.render_export(&crate::markdown::Markdown::new("")),
925 ""
926 );
927 assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
928 }
929
930 #[test]
931 fn try_build_text_reports_bad_markup() {
932 let console = test_console();
933
934 let err = console
935 .try_build_text("[/nope]")
936 .expect_err("an unmatched closing tag must be an error");
937 assert!(
938 matches!(err, crate::errors::RichError::Markup(_)),
939 "{err:?}"
940 );
941 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
943
944 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
945 assert_eq!(strict.plain(), "hi");
946 assert_eq!(
947 strict.spans().len(),
948 console.build_text("[bold]hi[/]").spans().len()
949 );
950 }
951
952 #[test]
960 fn unknown_tag_names_render_as_no_ops() {
961 let console = test_console();
962 let text = console
963 .try_build_text("[nope]x[/]")
964 .expect("an unknown tag name is not a syntax error");
965 assert_eq!(console.render_to_string(&text), "x");
966 assert_eq!(
967 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
968 "x"
969 );
970
971 assert!(console.try_build_text("[bold]a[/italic]").is_err());
973 assert!(console.try_build_text("[/nope]").is_err());
974 }
975
976 #[test]
979 fn markup_styles_bind_at_render_not_at_parse() {
980 let themed = |definition: &str| {
981 let mut theme = Theme::default_theme();
982 theme.insert("accent", Style::parse(definition).unwrap());
983 Console::builder()
984 .force_terminal(true)
985 .color_system(Some(ColorSystem::Truecolor))
986 .width(80)
987 .no_color(false)
988 .theme(theme)
989 .build()
990 };
991 let red = themed("bold red");
992 let green = themed("underline green");
993
994 let text = red.build_text("[accent]hi[/]");
996 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
997 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
999 }
1000
1001 #[test]
1004 fn try_build_text_expands_emoji_like_build_text() {
1005 let console = test_console();
1006 assert_eq!(
1007 console
1008 .try_build_text(":rocket: go")
1009 .expect("valid")
1010 .plain(),
1011 console.build_text(":rocket: go").plain()
1012 );
1013 }
1014
1015 #[test]
1016 fn renders_markup_string() {
1017 let console = test_console();
1018 assert_eq!(
1019 console.render_str_to_string("[bold red]hi[/]"),
1020 "\x1b[1;31mhi\x1b[0m"
1021 );
1022 }
1023
1024 #[test]
1025 fn print_justify_pads_to_width() {
1026 let console = Console::builder()
1027 .force_terminal(true)
1028 .color_system(Some(ColorSystem::Truecolor))
1029 .width(10)
1030 .build();
1031 assert_eq!(
1033 console.render_justified_to_string("hi", Justify::Left),
1034 "hi "
1035 );
1036 assert_eq!(
1037 console.render_justified_to_string("hi", Justify::Center),
1038 " hi "
1039 );
1040 assert_eq!(
1041 console.render_justified_to_string("hi", Justify::Right),
1042 " hi"
1043 );
1044 }
1045
1046 #[test]
1047 fn capture_records_ansi_instead_of_stdout() {
1048 let console = Console::builder()
1049 .force_terminal(true)
1050 .color_system(Some(ColorSystem::Truecolor))
1051 .width(20)
1052 .build();
1053 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
1055 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
1056 }
1057
1058 #[test]
1059 fn themed_exports_use_the_given_palette() {
1060 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
1061
1062 let console = Console::builder()
1063 .force_terminal(true)
1064 .color_system(Some(ColorSystem::Truecolor))
1065 .width(20)
1066 .no_color(false)
1067 .build();
1068 let render = |c: &Console| c.print_str("hi");
1069
1070 let monokai = console.export_html_themed(&MONOKAI, render);
1073 assert!(
1074 monokai.contains("#0c0c0c"),
1075 "monokai bg missing:\n{monokai}"
1076 );
1077
1078 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
1079 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
1080 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
1081
1082 let classes = console.export_html_classes_themed(&MONOKAI, render);
1084 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
1085 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
1086 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
1087
1088 assert!(console.export_html(render).contains("#ffffff"));
1090 }
1091
1092 #[test]
1093 fn page_with_honors_the_styles_flag() {
1094 use std::sync::Mutex;
1095
1096 #[derive(Default)]
1097 struct Recorder(Mutex<String>);
1098 impl crate::pager::Pager for Recorder {
1099 fn show(&self, content: &str) -> std::io::Result<()> {
1100 *self.0.lock().unwrap() = content.to_string();
1101 Ok(())
1102 }
1103 }
1104
1105 let console = Console::builder()
1106 .force_terminal(true)
1107 .color_system(Some(ColorSystem::Truecolor))
1108 .width(20)
1109 .no_color(false)
1110 .build();
1111
1112 let plain = Recorder::default();
1114 console
1115 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
1116 .unwrap();
1117 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
1118
1119 let styled = Recorder::default();
1121 console
1122 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
1123 .unwrap();
1124 assert_eq!(
1125 styled.0.lock().unwrap().as_str(),
1126 "\x1b[1;31mhi\x1b[0m there\n"
1127 );
1128 }
1129
1130 #[test]
1131 fn export_text_strips_styles() {
1132 let console = Console::builder()
1133 .force_terminal(true)
1134 .color_system(Some(ColorSystem::Truecolor))
1135 .width(20)
1136 .build();
1137 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
1139 assert_eq!(out, "hi there\n");
1140 }
1141
1142 #[test]
1143 fn export_html_matches_upstream() {
1144 let console = Console::builder()
1145 .force_terminal(true)
1146 .color_system(Some(ColorSystem::Truecolor))
1147 .width(20)
1148 .no_color(false)
1149 .build();
1150 let html = console.export_html(|c| {
1151 c.print_str("[bold red]hi[/] there");
1152 c.print_str("plain line");
1153 });
1154 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
1158 assert_eq!(html, expected);
1159 }
1160
1161 #[test]
1162 fn export_html_classes_matches_upstream() {
1163 let console = Console::builder()
1164 .force_terminal(true)
1165 .color_system(Some(ColorSystem::Truecolor))
1166 .width(20)
1167 .no_color(false)
1168 .build();
1169 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
1170 let expected =
1173 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
1174 assert_eq!(html, expected);
1175 }
1176
1177 #[test]
1178 fn capture_matches_direct_render() {
1179 let console = test_console();
1180 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
1181 assert_eq!(
1182 console.capture(|c| c.print(&panel)),
1183 console.render_export(&panel)
1184 );
1185 }
1186
1187 #[test]
1188 fn no_color_strips_styles() {
1189 let console = Console::builder()
1190 .force_terminal(true)
1191 .color_system(None)
1192 .build();
1193 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
1194 }
1195}