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_stack: Vec<Theme>,
104 base_style: Style,
105 highlighters: Vec<Box<dyn Highlighter + Send>>,
106 record_buffer: std::cell::RefCell<Vec<Segment>>,
109 capturing: std::cell::Cell<bool>,
110}
111
112pub struct ThemeContext<'a> {
115 console: &'a mut Console,
116}
117
118impl std::ops::Deref for ThemeContext<'_> {
119 type Target = Console;
120
121 fn deref(&self) -> &Console {
122 self.console
123 }
124}
125
126impl std::ops::DerefMut for ThemeContext<'_> {
127 fn deref_mut(&mut self) -> &mut Console {
128 self.console
129 }
130}
131
132impl Drop for ThemeContext<'_> {
133 fn drop(&mut self) {
134 let _ = self.console.pop_theme();
137 }
138}
139
140impl Default for Console {
141 fn default() -> Self {
142 Console::new()
143 }
144}
145
146impl Console {
147 pub fn new() -> Self {
149 ConsoleBuilder::new().build()
150 }
151
152 pub fn builder() -> ConsoleBuilder {
154 ConsoleBuilder::new()
155 }
156
157 pub fn color_system(&self) -> Option<ColorSystem> {
159 if self.no_color {
160 None
161 } else {
162 self.color_system
163 }
164 }
165
166 pub fn width(&self) -> usize {
168 self.width
169 }
170
171 pub fn height(&self) -> usize {
174 self.height
175 }
176
177 pub fn is_terminal(&self) -> bool {
179 self.is_terminal
180 }
181
182 pub fn legacy_windows(&self) -> bool {
184 self.legacy_windows
185 }
186
187 pub fn safe_box(&self) -> bool {
189 self.safe_box
190 }
191
192 pub fn ascii_only(&self) -> bool {
194 self.ascii_only
195 }
196
197 pub fn theme(&self) -> &Theme {
199 self.theme_stack
200 .last()
201 .expect("the theme stack always holds its base theme")
202 }
203
204 pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
207 self.theme().get_style(style)
208 }
209
210 pub fn push_theme(&mut self, theme: Theme, inherit: bool) {
216 let top = if inherit {
217 let mut merged = self.theme().clone();
218 merged.extend_from(&theme);
219 merged
220 } else {
221 theme
222 };
223 self.theme_stack.push(top);
224 }
225
226 pub fn pop_theme(&mut self) -> crate::errors::Result<()> {
230 if self.theme_stack.len() == 1 {
231 return Err(crate::errors::RichError::ThemeStack(
232 "Unable to pop base theme".to_string(),
233 ));
234 }
235 self.theme_stack.pop();
236 Ok(())
237 }
238
239 pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
264 self.push_theme(theme, true);
265 ThemeContext { console: self }
266 }
267
268 pub fn base_style(&self) -> &Style {
270 &self.base_style
271 }
272
273 pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
277 self.highlighters.push(highlighter);
278 }
279
280 pub fn options(&self) -> ConsoleOptions {
282 ConsoleOptions {
283 min_width: 1,
284 max_width: self.width,
285 height: None,
286 justify: Justify::Default,
287 overflow: None,
288 no_wrap: None,
289 }
290 }
291
292 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
299 let segments = self.render_segments(renderable);
300 self.segments_to_string(&segments)
301 }
302
303 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
306 let mut options = self.options();
307 if options.justify == Justify::Default && renderable.fit_to_measurement() {
308 let measurement = renderable.measure(self, &options);
309 options.max_width = measurement.maximum.min(options.max_width).max(1);
310 }
311 let segments = renderable.rich_render(self, &options);
312 Segment::crop_lines(&segments, self.width)
317 }
318
319 fn emit(&self, segments: Vec<Segment>) {
322 if segments.is_empty() {
323 return;
324 }
325 if self.capturing.get() {
326 let mut buffer = self.record_buffer.borrow_mut();
327 buffer.extend(segments);
328 buffer.push(Segment::line());
329 return;
330 }
331 let mut output = self.segments_to_string(&segments);
332 output.push('\n');
333 let stdout = std::io::stdout();
334 let mut lock = stdout.lock();
335 let _ = write!(lock, "{output}");
336 }
337
338 pub fn render_lines(
344 &self,
345 renderable: &dyn Renderable,
346 options: &ConsoleOptions,
347 pad: bool,
348 ) -> Vec<Vec<Segment>> {
349 let segments = renderable.rich_render(self, options);
350 let mut lines = Segment::split_lines(&segments);
351 if pad {
352 for line in &mut lines {
353 *line = Segment::adjust_line_length(line, options.max_width, Some(Style::new()));
354 }
355 }
356 if let Some(height) = options.height {
360 lines.truncate(height);
361 while lines.len() < height {
362 lines.push(if pad {
363 vec![Segment::new(
364 " ".repeat(options.max_width),
365 Some(Style::new()),
366 )]
367 } else {
368 Vec::new()
369 });
370 }
371 }
372 lines
373 }
374
375 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
379 let segments = self.render_segments(renderable);
380 let mut out = self.segments_to_string(&segments);
381 if !segments.is_empty() {
382 out.push('\n');
383 }
384 out
385 }
386
387 pub fn print(&self, renderable: &dyn Renderable) {
389 let segments = self.render_segments(renderable);
390 self.emit(segments);
391 }
392
393 pub fn control(&self, control: &crate::control::Control) {
398 if !self.is_terminal {
399 return;
400 }
401 let text = control.as_str();
402 if !text.is_empty() {
403 let stdout = std::io::stdout();
404 let mut lock = stdout.lock();
405 let _ = write!(lock, "{text}");
406 }
407 }
408
409 pub fn show_cursor(&self, show: bool) {
411 self.control(&crate::control::Control::show_cursor(show));
412 }
413
414 pub fn clear(&self) {
416 self.control(&crate::control::Control::clear());
417 }
418
419 pub fn bell(&self) {
421 self.control(&crate::control::Control::bell());
422 }
423
424 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
431 let segments = self.record(f);
432 self.segments_to_string(&segments)
433 }
434
435 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
438 let segments = self.record(f);
439 segments_to_plain(&segments)
440 }
441
442 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
450 self.page_with(&crate::pager::SystemPager, styles, f)
451 }
452
453 pub fn page_with(
456 &self,
457 pager: &dyn crate::pager::Pager,
458 styles: bool,
459 f: impl FnOnce(&Console),
460 ) -> std::io::Result<()> {
461 let segments = self.record(f);
462 let content = if styles {
463 self.segments_to_string(&segments)
464 } else {
465 segments_to_plain(&segments)
466 };
467 pager.show(&content)
468 }
469
470 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
474 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
475 }
476
477 pub fn export_html_themed(
483 &self,
484 theme: &crate::terminal_theme::TerminalTheme,
485 f: impl FnOnce(&Console),
486 ) -> String {
487 let segments = self.record(f);
488 crate::export::export_html_inline(&segments, theme)
489 }
490
491 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
495 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
496 }
497
498 pub fn export_html_classes_themed(
501 &self,
502 theme: &crate::terminal_theme::TerminalTheme,
503 f: impl FnOnce(&Console),
504 ) -> String {
505 let segments = self.record(f);
506 crate::export::export_html_classes(&segments, theme)
507 }
508
509 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
520 self.export_svg_themed(
521 &crate::terminal_theme::SVG_EXPORT_THEME,
522 title,
523 unique_id,
524 f,
525 )
526 }
527
528 pub fn export_svg_themed(
531 &self,
532 theme: &crate::terminal_theme::TerminalTheme,
533 title: &str,
534 unique_id: &str,
535 f: impl FnOnce(&Console),
536 ) -> String {
537 let segments = self.record(f);
538 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
539 }
540
541 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
558 self.record(f)
559 }
560
561 fn record(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
564 let previous = std::mem::take(&mut *self.record_buffer.borrow_mut());
565 let was_capturing = self.capturing.replace(true);
566 f(self);
567 let captured = std::mem::replace(&mut *self.record_buffer.borrow_mut(), previous);
568 self.capturing.set(was_capturing);
569 captured
570 }
571
572 pub fn print_str(&self, content: &str) {
575 let text = self.build_text(content);
576 self.print(&text);
577 }
578
579 pub fn render_str_to_string(&self, content: &str) -> String {
581 let text = self.build_text(content);
582 self.render_to_string(&text)
583 }
584
585 pub fn build_text(&self, content: &str) -> Text {
589 self.try_build_text(content)
594 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
595 }
596
597 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
601 let expanded = self.expand_emoji(content);
602 let markup = Text::from_markup(&expanded)?;
603
604 let mut text = self.decorate(Text::new(markup.plain()));
615 for span in markup.spans() {
616 text.push_span(span.clone());
617 }
618 Ok(text)
619 }
620
621 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
623 self.print(&self.try_build_text(content)?);
624 Ok(())
625 }
626
627 pub fn try_print_justified(
630 &self,
631 content: &str,
632 justify: Justify,
633 ) -> crate::errors::Result<()> {
634 let text = self.try_build_text(content)?;
635 let mut options = self.options();
636 options.justify = justify;
637 self.emit(text.rich_render(self, &options));
638 Ok(())
639 }
640
641 pub(crate) fn expand_emoji(&self, content: &str) -> String {
644 if self.emoji {
645 crate::emoji::replace(content)
646 } else {
647 content.to_string()
648 }
649 }
650
651 fn decorate(&self, mut text: Text) -> Text {
654 for highlighter in &self.highlighters {
655 highlighter.highlight(&mut text);
656 }
657 if self.highlight {
658 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
659 }
660 text
661 }
662
663 pub fn print_justified(&self, content: &str, justify: Justify) {
666 let text = self.build_text(content);
667 let mut options = self.options();
668 options.justify = justify;
669 let segments = text.rich_render(self, &options);
670 self.emit(segments);
671 }
672
673 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
678 let text = self.build_text(content);
679 let mut options = self.options();
680 options.justify = justify;
681 let segments = text.rich_render(self, &options);
682 self.segments_to_string(&segments)
683 }
684
685 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
688 let system = self.color_system();
689 let mut out = String::new();
690 for segment in segments {
691 if segment.control && !self.is_terminal {
694 continue;
695 }
696 match (&segment.style, system) {
697 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
698 _ => out.push_str(&segment.text),
699 }
700 }
701 out
702 }
703}
704
705fn segments_to_plain(segments: &[Segment]) -> String {
708 segments
709 .iter()
710 .filter(|s| !s.control)
711 .map(|s| s.text.as_str())
712 .collect()
713}
714
715impl Renderable for Text {
716 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
717 if self.is_empty() {
720 return vec![Segment::new("", None)];
721 }
722 let justify = if self.get_justify() != Justify::Default {
725 self.get_justify()
726 } else {
727 options.justify
728 };
729 let overflow = self
733 .get_overflow()
734 .or(options.overflow)
735 .unwrap_or(Overflow::Fold);
736 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
737 self.render_joined_wrapped(
738 console.theme(),
739 console.base_style(),
740 options.max_width,
741 justify,
742 overflow,
743 no_wrap,
744 )
745 }
746
747 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
748 let (minimum, maximum) = self.measurement();
749 crate::measure::Measurement::new(
750 minimum.min(options.max_width),
751 maximum.min(options.max_width),
752 )
753 }
754}
755
756pub struct ConsoleBuilder {
758 force_terminal: Option<bool>,
759 color_system: Option<ColorSystem>,
760 color_system_set: bool,
761 width: Option<usize>,
762 height: Option<usize>,
763 no_color: Option<bool>,
764 emoji: Option<bool>,
765 highlight: Option<bool>,
766 legacy_windows: Option<bool>,
767 safe_box: Option<bool>,
768 ascii_only: Option<bool>,
769 theme: Option<Theme>,
770}
771
772impl ConsoleBuilder {
773 fn new() -> Self {
774 ConsoleBuilder {
775 force_terminal: None,
776 color_system: None,
777 color_system_set: false,
778 width: None,
779 height: None,
780 no_color: None,
781 emoji: None,
782 highlight: None,
783 legacy_windows: None,
784 safe_box: None,
785 ascii_only: None,
786 theme: None,
787 }
788 }
789
790 pub fn force_terminal(mut self, value: bool) -> Self {
791 self.force_terminal = Some(value);
792 self
793 }
794
795 pub fn legacy_windows(mut self, value: bool) -> Self {
797 self.legacy_windows = Some(value);
798 self
799 }
800
801 pub fn safe_box(mut self, value: bool) -> Self {
803 self.safe_box = Some(value);
804 self
805 }
806
807 pub fn ascii_only(mut self, value: bool) -> Self {
809 self.ascii_only = Some(value);
810 self
811 }
812
813 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
815 self.color_system = system;
816 self.color_system_set = true;
817 self
818 }
819
820 pub fn width(mut self, width: usize) -> Self {
821 self.width = Some(width);
822 self
823 }
824
825 pub fn height(mut self, height: usize) -> Self {
827 self.height = Some(height);
828 self
829 }
830
831 pub fn no_color(mut self, value: bool) -> Self {
832 self.no_color = Some(value);
833 self
834 }
835
836 pub fn emoji(mut self, value: bool) -> Self {
838 self.emoji = Some(value);
839 self
840 }
841
842 pub fn highlight(mut self, value: bool) -> Self {
845 self.highlight = Some(value);
846 self
847 }
848
849 pub fn theme(mut self, theme: Theme) -> Self {
850 self.theme = Some(theme);
851 self
852 }
853
854 pub fn build(self) -> Console {
855 let is_terminal = self
856 .force_terminal
857 .unwrap_or_else(|| std::io::stdout().is_terminal());
858 let no_color = self
863 .no_color
864 .unwrap_or_else(|| std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()));
865 let color_system = if self.color_system_set {
866 self.color_system
867 } else if is_terminal {
868 Some(detect_color_system())
869 } else {
870 None
871 };
872 let width = self.width.unwrap_or_else(detect_width);
873 let height = self.height.unwrap_or_else(detect_height);
874 Console {
875 render_environment: None,
876 color_system,
877 width,
878 height,
879 is_terminal,
880 no_color,
881 emoji: self.emoji.unwrap_or(true),
882 highlight: self.highlight.unwrap_or(true),
887 legacy_windows: self.legacy_windows.unwrap_or(false),
888 safe_box: self.safe_box.unwrap_or(true),
889 ascii_only: self.ascii_only.unwrap_or(false),
890 theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
891 base_style: Style::new(),
892 highlighters: Vec::new(),
893 record_buffer: std::cell::RefCell::new(Vec::new()),
894 capturing: std::cell::Cell::new(false),
895 }
896 }
897}
898
899fn detect_color_system() -> ColorSystem {
911 if let Some(colorterm) = std::env::var_os("COLORTERM") {
912 let colorterm = colorterm.to_string_lossy().to_ascii_lowercase();
913 if colorterm.contains("truecolor") || colorterm.contains("24bit") {
914 return ColorSystem::Truecolor;
915 }
916 }
917
918 #[cfg(windows)]
930 {
931 let _ = anstyle_query::windows::enable_ansi_colors();
932 ColorSystem::Truecolor
933 }
934
935 #[cfg(not(windows))]
938 {
939 if let Some(term) = std::env::var_os("TERM") {
940 if term.to_string_lossy().contains("256") {
941 return ColorSystem::EightBit;
942 }
943 }
944 ColorSystem::Standard
945 }
946}
947
948fn detect_width() -> usize {
950 if let Some(columns) = std::env::var_os("COLUMNS") {
951 if let Ok(value) = columns.to_string_lossy().trim().parse::<usize>() {
952 if value > 0 {
953 return value;
954 }
955 }
956 }
957 if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
958 if w > 0 {
959 return w as usize;
960 }
961 }
962 DEFAULT_WIDTH
963}
964
965fn detect_height() -> usize {
967 if let Some(lines) = std::env::var_os("LINES") {
968 if let Ok(value) = lines.to_string_lossy().trim().parse::<usize>() {
969 if value > 0 {
970 return value;
971 }
972 }
973 }
974 if let Some((_, terminal_size::Height(h))) = terminal_size::terminal_size() {
975 if h > 0 {
976 return h as usize;
977 }
978 }
979 DEFAULT_HEIGHT
980}
981
982impl crate::protocol::ConsoleEnvironment for Console {
983 fn set_render_environment(
984 &mut self,
985 value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
986 ) {
987 self.render_environment = value;
988 }
989 fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
990 self.render_environment.as_deref()
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use super::*;
997
998 fn test_console() -> Console {
999 Console::builder()
1000 .force_terminal(true)
1001 .color_system(Some(ColorSystem::Truecolor))
1002 .width(80)
1003 .no_color(false)
1004 .build()
1005 }
1006
1007 #[test]
1010 fn empty_text_and_empty_renderables_have_distinct_endings() {
1011 let console = Console::builder().force_terminal(false).build();
1012 assert_eq!(console.render_export(&Text::new("")), "\n");
1013 assert_eq!(
1014 console.render_export(&crate::markdown::Markdown::new("")),
1015 ""
1016 );
1017 assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
1018 }
1019
1020 #[test]
1021 fn try_build_text_reports_bad_markup() {
1022 let console = test_console();
1023
1024 let err = console
1025 .try_build_text("[/nope]")
1026 .expect_err("an unmatched closing tag must be an error");
1027 assert!(
1028 matches!(err, crate::errors::RichError::Markup(_)),
1029 "{err:?}"
1030 );
1031 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
1033
1034 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
1035 assert_eq!(strict.plain(), "hi");
1036 assert_eq!(
1037 strict.spans().len(),
1038 console.build_text("[bold]hi[/]").spans().len()
1039 );
1040 }
1041
1042 #[test]
1050 fn unknown_tag_names_render_as_no_ops() {
1051 let console = test_console();
1052 let text = console
1053 .try_build_text("[nope]x[/]")
1054 .expect("an unknown tag name is not a syntax error");
1055 assert_eq!(console.render_to_string(&text), "x");
1056 assert_eq!(
1057 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
1058 "x"
1059 );
1060
1061 assert!(console.try_build_text("[bold]a[/italic]").is_err());
1063 assert!(console.try_build_text("[/nope]").is_err());
1064 }
1065
1066 #[test]
1069 fn markup_styles_bind_at_render_not_at_parse() {
1070 let themed = |definition: &str| {
1071 let mut theme = Theme::default_theme();
1072 theme.insert("accent", Style::parse(definition).unwrap());
1073 Console::builder()
1074 .force_terminal(true)
1075 .color_system(Some(ColorSystem::Truecolor))
1076 .width(80)
1077 .no_color(false)
1078 .theme(theme)
1079 .build()
1080 };
1081 let red = themed("bold red");
1082 let green = themed("underline green");
1083
1084 let text = red.build_text("[accent]hi[/]");
1086 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
1087 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
1089 }
1090
1091 #[test]
1094 fn try_build_text_expands_emoji_like_build_text() {
1095 let console = test_console();
1096 assert_eq!(
1097 console
1098 .try_build_text(":rocket: go")
1099 .expect("valid")
1100 .plain(),
1101 console.build_text(":rocket: go").plain()
1102 );
1103 }
1104
1105 #[test]
1106 fn renders_markup_string() {
1107 let console = test_console();
1108 assert_eq!(
1109 console.render_str_to_string("[bold red]hi[/]"),
1110 "\x1b[1;31mhi\x1b[0m"
1111 );
1112 }
1113
1114 #[test]
1115 fn print_justify_pads_to_width() {
1116 let console = Console::builder()
1117 .force_terminal(true)
1118 .color_system(Some(ColorSystem::Truecolor))
1119 .width(10)
1120 .build();
1121 assert_eq!(
1123 console.render_justified_to_string("hi", Justify::Left),
1124 "hi "
1125 );
1126 assert_eq!(
1127 console.render_justified_to_string("hi", Justify::Center),
1128 " hi "
1129 );
1130 assert_eq!(
1131 console.render_justified_to_string("hi", Justify::Right),
1132 " hi"
1133 );
1134 }
1135
1136 #[test]
1137 fn capture_records_ansi_instead_of_stdout() {
1138 let console = Console::builder()
1139 .force_terminal(true)
1140 .color_system(Some(ColorSystem::Truecolor))
1141 .width(20)
1142 .build();
1143 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
1145 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
1146 }
1147
1148 #[test]
1149 fn themed_exports_use_the_given_palette() {
1150 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
1151
1152 let console = Console::builder()
1153 .force_terminal(true)
1154 .color_system(Some(ColorSystem::Truecolor))
1155 .width(20)
1156 .no_color(false)
1157 .build();
1158 let render = |c: &Console| c.print_str("hi");
1159
1160 let monokai = console.export_html_themed(&MONOKAI, render);
1163 assert!(
1164 monokai.contains("#0c0c0c"),
1165 "monokai bg missing:\n{monokai}"
1166 );
1167
1168 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
1169 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
1170 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
1171
1172 let classes = console.export_html_classes_themed(&MONOKAI, render);
1174 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
1175 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
1176 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
1177
1178 assert!(console.export_html(render).contains("#ffffff"));
1180 }
1181
1182 #[test]
1183 fn page_with_honors_the_styles_flag() {
1184 use std::sync::Mutex;
1185
1186 #[derive(Default)]
1187 struct Recorder(Mutex<String>);
1188 impl crate::pager::Pager for Recorder {
1189 fn show(&self, content: &str) -> std::io::Result<()> {
1190 *self.0.lock().unwrap() = content.to_string();
1191 Ok(())
1192 }
1193 }
1194
1195 let console = Console::builder()
1196 .force_terminal(true)
1197 .color_system(Some(ColorSystem::Truecolor))
1198 .width(20)
1199 .no_color(false)
1200 .build();
1201
1202 let plain = Recorder::default();
1204 console
1205 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
1206 .unwrap();
1207 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
1208
1209 let styled = Recorder::default();
1211 console
1212 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
1213 .unwrap();
1214 assert_eq!(
1215 styled.0.lock().unwrap().as_str(),
1216 "\x1b[1;31mhi\x1b[0m there\n"
1217 );
1218 }
1219
1220 #[test]
1221 fn export_text_strips_styles() {
1222 let console = Console::builder()
1223 .force_terminal(true)
1224 .color_system(Some(ColorSystem::Truecolor))
1225 .width(20)
1226 .build();
1227 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
1229 assert_eq!(out, "hi there\n");
1230 }
1231
1232 #[test]
1233 fn export_html_matches_upstream() {
1234 let console = Console::builder()
1235 .force_terminal(true)
1236 .color_system(Some(ColorSystem::Truecolor))
1237 .width(20)
1238 .no_color(false)
1239 .build();
1240 let html = console.export_html(|c| {
1241 c.print_str("[bold red]hi[/] there");
1242 c.print_str("plain line");
1243 });
1244 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
1248 assert_eq!(html, expected);
1249 }
1250
1251 #[test]
1252 fn export_html_classes_matches_upstream() {
1253 let console = Console::builder()
1254 .force_terminal(true)
1255 .color_system(Some(ColorSystem::Truecolor))
1256 .width(20)
1257 .no_color(false)
1258 .build();
1259 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
1260 let expected =
1263 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
1264 assert_eq!(html, expected);
1265 }
1266
1267 #[test]
1268 fn capture_matches_direct_render() {
1269 let console = test_console();
1270 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
1271 assert_eq!(
1272 console.capture(|c| c.print(&panel)),
1273 console.render_export(&panel)
1274 );
1275 }
1276
1277 #[test]
1278 fn no_color_strips_styles() {
1279 let console = Console::builder()
1280 .force_terminal(true)
1281 .color_system(None)
1282 .build();
1283 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
1284 }
1285
1286 #[test]
1287 fn used_theme_applies_through_the_guard_and_pops_on_drop() {
1288 let mut console = Console::builder()
1289 .force_terminal(true)
1290 .color_system(Some(ColorSystem::Truecolor))
1291 .width(20)
1292 .highlight(false)
1293 .build();
1294 let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
1295 {
1296 let themed = console.use_theme(theme);
1297 let out = themed.capture(|c| c.print_str("[accent]x[/]"));
1298 assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
1299 }
1300 assert!(console.theme().get("accent").is_none());
1301 assert!(console.pop_theme().is_err(), "the base theme must remain");
1302 }
1303
1304 #[test]
1305 fn used_theme_is_popped_during_a_panic_unwind() {
1306 let mut console = Console::builder().width(20).build();
1307 let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
1308 let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1309 let _themed = console.use_theme(theme);
1310 panic!("render failed");
1311 }));
1312 assert!(unwound.is_err());
1313 assert!(console.theme().get("accent").is_none());
1314 }
1315}