1use std::io::{IsTerminal, Write};
10
11use crate::color::ColorSystem;
12use crate::protocol::{Highlighter, Renderable};
13use crate::segment::Segment;
14use crate::style::Style;
15use crate::text::Text;
16use crate::theme::Theme;
17
18const DEFAULT_WIDTH: usize = 80;
19const DEFAULT_HEIGHT: usize = 25;
20
21pub type GetTime = std::sync::Arc<dyn Fn() -> f64 + Send + Sync>;
24
25pub(crate) fn monotonic() -> f64 {
29 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
30 START
31 .get_or_init(std::time::Instant::now)
32 .elapsed()
33 .as_secs_f64()
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Justify {
40 #[default]
42 Default,
43 Left,
44 Center,
45 Right,
46 Full,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum Overflow {
53 #[default]
55 Fold,
56 Crop,
58 Ellipsis,
60 Ignore,
62}
63
64#[derive(Debug, Clone)]
69pub struct ConsoleOptions {
70 pub min_width: usize,
71 pub max_width: usize,
72 pub height: Option<usize>,
73 pub justify: Justify,
74 pub overflow: Option<Overflow>,
77 pub no_wrap: Option<bool>,
80 pub highlight: Option<bool>,
85 pub markup: Option<bool>,
88 pub max_height: usize,
91 pub encoding: String,
94 pub is_terminal: bool,
96 pub legacy_windows: bool,
99 pub size: ConsoleDimensions,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub struct ConsoleDimensions {
106 pub width: usize,
108 pub height: usize,
110}
111
112impl Default for ConsoleOptions {
113 fn default() -> Self {
116 ConsoleOptions {
117 min_width: 1,
118 max_width: DEFAULT_WIDTH,
119 height: None,
120 justify: Justify::Default,
121 overflow: None,
122 no_wrap: None,
123 highlight: None,
124 markup: None,
125 max_height: DEFAULT_HEIGHT,
126 encoding: "utf-8".to_string(),
127 is_terminal: false,
128 legacy_windows: false,
129 size: ConsoleDimensions {
130 width: DEFAULT_WIDTH,
131 height: DEFAULT_HEIGHT,
132 },
133 }
134 }
135}
136
137impl ConsoleOptions {
138 pub fn update_width(&self, width: usize) -> ConsoleOptions {
141 let mut options = self.clone();
144 options.min_width = width;
145 options.max_width = width;
146 options
147 }
148
149 pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
152 let mut options = self.update_width(width);
153 options.height = Some(height);
154 options.max_height = height;
155 options
156 }
157
158 pub fn update_height(&self, height: usize) -> ConsoleOptions {
161 let mut options = self.clone();
162 options.height = Some(height);
163 options.max_height = height;
164 options
165 }
166
167 pub fn reset_height(&self) -> ConsoleOptions {
170 let mut options = self.clone();
171 options.height = None;
172 options
173 }
174
175 pub fn ascii_only(&self) -> bool {
178 !self.encoding.starts_with("utf")
179 }
180}
181
182#[derive(Clone, Default)]
185pub struct RenderStrOptions<'a> {
186 pub style: crate::style::StyleType,
188 pub justify: Option<Justify>,
190 pub overflow: Option<Overflow>,
192 pub emoji: Option<bool>,
194 pub markup: Option<bool>,
196 pub highlight: Option<bool>,
198 pub highlighter: Option<&'a dyn Highlighter>,
201}
202
203pub type Environ = std::collections::HashMap<String, String>;
206
207type BrokenPipeHandler = std::sync::Arc<dyn Fn(&Console) + Send + Sync>;
209
210type GetDateTime = std::sync::Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>;
212
213type FormatTime = std::sync::Arc<dyn Fn(std::time::SystemTime) -> Text + Send + Sync>;
216
217#[derive(Clone)]
219enum LogTimeFormat {
220 Strftime(String),
221 Callable(FormatTime),
222}
223
224fn environ_get(environ: Option<&Environ>, key: &str) -> Option<String> {
226 match environ {
227 Some(environ) => environ.get(key).cloned(),
228 None => std::env::var_os(key).map(|value| value.to_string_lossy().into_owned()),
229 }
230}
231
232fn env_int(environ: Option<&Environ>, key: &str) -> Option<usize> {
235 let value = environ_get(environ, key)?;
236 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
237 return None;
238 }
239 value.parse().ok()
240}
241
242pub trait RenderHook: Send + Sync {
256 fn process_renderables(
259 &self,
260 renderables: Vec<Box<dyn Renderable>>,
261 ) -> Vec<Box<dyn Renderable>>;
262}
263
264struct Rendered(Vec<Segment>);
267
268impl Renderable for Rendered {
269 fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
270 self.0.clone()
271 }
272
273 fn measure(
274 &self,
275 _console: &Console,
276 _options: &ConsoleOptions,
277 ) -> crate::measure::Measurement {
278 let widest = Segment::split_lines(&self.0)
279 .iter()
280 .map(|line| Segment::get_line_length(line))
281 .max()
282 .unwrap_or(0);
283 crate::measure::Measurement::new(widest, widest)
284 }
285
286 fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
288 false
289 }
290}
291
292type CaptureStacks = std::collections::HashMap<std::thread::ThreadId, Vec<Vec<Segment>>>;
294
295fn current_capture(captures: &mut CaptureStacks) -> Option<&mut Vec<Segment>> {
297 captures
298 .get_mut(&std::thread::current().id())
299 .and_then(|stack| stack.last_mut())
300}
301
302pub struct Console {
305 render_environment: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
306 region_sink: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>,
308 code_highlighting: Option<crate::protocol::CodeHighlighting>,
310 color_system: Option<ColorSystem>,
311 width: Option<usize>,
314 height: Option<usize>,
316 terminal_size: ConsoleDimensions,
320 environ: Option<std::sync::Arc<Environ>>,
323 is_terminal: bool,
324 no_color: bool,
325 quiet: std::sync::atomic::AtomicBool,
328 soft_wrap: bool,
330 style: Option<crate::style::StyleType>,
332 on_broken_pipe: Option<BrokenPipeHandler>,
335 get_time: GetTime,
337 emoji: bool,
338 highlight: bool,
339 legacy_windows: bool,
340 safe_box: bool,
341 ascii_only: bool,
342 theme_stack: Vec<Theme>,
345 base_style: Style,
346 highlighters: Vec<std::sync::Arc<dyn Highlighter + Send + Sync>>,
349 markup: bool,
352 emoji_variant: Option<crate::emoji::EmojiVariant>,
355 tab_size: usize,
357 captures: std::sync::Mutex<CaptureStacks>,
364 is_alt_screen: std::sync::atomic::AtomicBool,
367 record: std::sync::atomic::AtomicBool,
370 record_buffer: std::sync::Mutex<Vec<Segment>>,
373 log_render: std::sync::Mutex<crate::log_render::LogRender>,
376 log_time: bool,
377 log_path: bool,
378 log_time_format: LogTimeFormat,
379 get_datetime: GetDateTime,
381 render_hooks: std::sync::Mutex<Vec<std::sync::Arc<dyn RenderHook>>>,
383 live_stack: LiveStack,
387}
388
389type LiveStack =
391 std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>>>;
392
393struct LockedHighlighter(std::sync::Mutex<Box<dyn Highlighter + Send>>);
396
397impl Highlighter for LockedHighlighter {
398 fn highlight(&self, text: &mut Text) {
399 let highlighter = self
400 .0
401 .lock()
402 .unwrap_or_else(std::sync::PoisonError::into_inner);
403 highlighter.highlight(text);
404 }
405}
406
407fn no_alt_screen() -> crate::errors::RichError {
409 crate::errors::RichError::NoAltScreen(
410 "Alt screen must be enabled to call update_screen".to_string(),
411 )
412}
413
414pub struct ScreenUpdate {
417 lines: Vec<Vec<Segment>>,
418 x: usize,
419 y: usize,
420}
421
422impl ScreenUpdate {
423 pub fn new(lines: Vec<Vec<Segment>>, x: usize, y: usize) -> Self {
425 ScreenUpdate { lines, x, y }
426 }
427}
428
429impl Renderable for ScreenUpdate {
430 fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
431 let mut segments = Vec::new();
432 for (offset, line) in self.lines.iter().enumerate() {
433 let to = crate::control::move_to_code(self.x as u128, self.y as u128 + offset as u128);
436 segments.push(Segment::control(to));
437 segments.extend(line.iter().cloned());
438 }
439 segments
440 }
441}
442
443impl Clone for Console {
444 fn clone(&self) -> Self {
454 Console {
455 render_environment: self.render_environment.clone(),
456 region_sink: self.region_sink.clone(),
457 code_highlighting: self.code_highlighting.clone(),
458 color_system: self.color_system,
459 width: self.width,
460 height: self.height,
461 terminal_size: self.terminal_size,
462 environ: self.environ.clone(),
463 is_terminal: self.is_terminal,
464 no_color: self.no_color,
465 quiet: std::sync::atomic::AtomicBool::new(self.quiet()),
466 soft_wrap: self.soft_wrap,
467 style: self.style.clone(),
468 on_broken_pipe: self.on_broken_pipe.clone(),
469 get_time: self.get_time.clone(),
470 emoji: self.emoji,
471 highlight: self.highlight,
472 legacy_windows: self.legacy_windows,
473 safe_box: self.safe_box,
474 ascii_only: self.ascii_only,
475 theme_stack: self.theme_stack.clone(),
476 base_style: self.base_style.clone(),
477 highlighters: self.highlighters.clone(),
478 markup: self.markup,
479 emoji_variant: self.emoji_variant,
480 tab_size: self.tab_size,
481 captures: std::sync::Mutex::new(CaptureStacks::new()),
482 is_alt_screen: std::sync::atomic::AtomicBool::new(false),
483 record: std::sync::atomic::AtomicBool::new(self.record()),
484 record_buffer: std::sync::Mutex::new(Vec::new()),
485 render_hooks: std::sync::Mutex::new(Vec::new()),
486 live_stack: self.live_stack.clone(),
487 log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
488 log_time: self.log_time,
489 log_path: self.log_path,
490 log_time_format: self.log_time_format.clone(),
491 get_datetime: self.get_datetime.clone(),
492 }
493 }
494}
495
496pub struct ThemeContext<'a> {
499 console: &'a mut Console,
500}
501
502impl std::ops::Deref for ThemeContext<'_> {
503 type Target = Console;
504
505 fn deref(&self) -> &Console {
506 self.console
507 }
508}
509
510impl std::ops::DerefMut for ThemeContext<'_> {
511 fn deref_mut(&mut self) -> &mut Console {
512 self.console
513 }
514}
515
516impl Drop for ThemeContext<'_> {
517 fn drop(&mut self) {
518 let _ = self.console.pop_theme();
521 }
522}
523
524pub struct ScreenContext<'a> {
527 console: &'a Console,
528 hide_cursor: bool,
529 changed: bool,
530 screen: Option<crate::screen::Screen>,
532 style: crate::style::StyleType,
533}
534
535impl ScreenContext<'_> {
536 pub fn update(
540 &mut self,
541 renderables: Vec<Box<dyn Renderable>>,
542 style: Option<crate::style::StyleType>,
543 ) {
544 if let Some(style) = style {
545 self.style = style;
546 }
547 let screen = if renderables.is_empty() {
548 self.screen
549 .take()
550 .unwrap_or_else(crate::screen::Screen::empty)
551 } else {
552 crate::screen::Screen::from_renderables(renderables)
553 };
554 let screen = screen.style(self.style.clone());
555 self.console.print(&screen);
557 self.screen = Some(screen);
558 }
559
560 pub fn console(&self) -> &Console {
562 self.console
563 }
564}
565
566impl Drop for ScreenContext<'_> {
567 fn drop(&mut self) {
568 if self.changed {
570 self.console.set_alt_screen(false);
571 if self.hide_cursor {
572 self.console.show_cursor(true);
573 }
574 }
575 }
576}
577
578fn lock_or_recover<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
580 mutex
581 .lock()
582 .unwrap_or_else(std::sync::PoisonError::into_inner)
583}
584
585impl Default for Console {
586 fn default() -> Self {
587 Console::new()
588 }
589}
590
591impl Console {
592 pub fn new() -> Self {
594 ConsoleBuilder::new().build()
595 }
596
597 pub fn builder() -> ConsoleBuilder {
599 ConsoleBuilder::new()
600 }
601
602 pub fn color_system(&self) -> Option<ColorSystem> {
610 self.color_system
611 }
612
613 pub fn no_color(&self) -> bool {
616 self.no_color
617 }
618
619 pub fn get_time(&self) -> f64 {
623 (self.get_time)()
624 }
625
626 pub fn width(&self) -> usize {
629 self.size().width
630 }
631
632 pub fn height(&self) -> usize {
636 self.size().height
637 }
638
639 pub fn set_width(&mut self, width: usize) {
641 self.width = Some(width);
642 }
643
644 pub fn set_height(&mut self, height: usize) {
646 self.height = Some(height);
647 }
648
649 pub fn set_size(&mut self, width: usize, height: usize) {
651 self.width = Some(width);
652 self.height = Some(height);
653 }
654
655 pub fn quiet(&self) -> bool {
657 self.quiet.load(std::sync::atomic::Ordering::SeqCst)
658 }
659
660 pub fn set_quiet(&self, quiet: bool) {
663 self.quiet.store(quiet, std::sync::atomic::Ordering::SeqCst);
664 }
665
666 pub fn soft_wrap(&self) -> bool {
668 self.soft_wrap
669 }
670
671 pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
674 self.soft_wrap = soft_wrap;
675 }
676
677 pub fn style(&self) -> Option<&crate::style::StyleType> {
679 self.style.as_ref()
680 }
681
682 pub fn set_style(&mut self, style: Option<crate::style::StyleType>) {
685 self.style = style;
686 }
687
688 pub fn is_dumb_terminal(&self) -> bool {
691 self.is_terminal && term_is_dumb(self.environ.as_deref())
692 }
693
694 pub fn on_broken_pipe(&self) {
704 if let Some(handler) = &self.on_broken_pipe {
705 handler(self);
706 return;
707 }
708 self.set_quiet(true);
709 std::process::exit(1);
710 }
711
712 pub fn is_terminal(&self) -> bool {
714 self.is_terminal
715 }
716
717 pub fn legacy_windows(&self) -> bool {
719 self.legacy_windows
720 }
721
722 pub fn safe_box(&self) -> bool {
724 self.safe_box
725 }
726
727 pub fn ascii_only(&self) -> bool {
729 self.ascii_only
730 }
731
732 pub fn theme(&self) -> &Theme {
734 self.theme_stack
735 .last()
736 .expect("the theme stack always holds its base theme")
737 }
738
739 pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
742 self.theme().get_style(style)
743 }
744
745 pub fn push_theme(&mut self, theme: Theme, inherit: bool) {
751 let top = if inherit {
752 let mut merged = self.theme().clone();
753 merged.extend_from(&theme);
754 merged
755 } else {
756 theme
757 };
758 self.theme_stack.push(top);
759 }
760
761 pub fn pop_theme(&mut self) -> crate::errors::Result<()> {
765 if self.theme_stack.len() == 1 {
766 return Err(crate::errors::RichError::ThemeStack(
767 "Unable to pop base theme".to_string(),
768 ));
769 }
770 self.theme_stack.pop();
771 Ok(())
772 }
773
774 pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
799 self.push_theme(theme, true);
800 ThemeContext { console: self }
801 }
802
803 pub fn base_style(&self) -> &Style {
805 &self.base_style
806 }
807
808 pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
812 self.highlighters
813 .push(std::sync::Arc::new(LockedHighlighter(
814 std::sync::Mutex::new(highlighter),
815 )));
816 }
817
818 pub fn options(&self) -> ConsoleOptions {
820 let size = self.size();
821 ConsoleOptions {
822 min_width: 1,
823 max_width: size.width,
824 height: None,
825 justify: Justify::Default,
826 overflow: None,
827 no_wrap: None,
828 highlight: None,
829 markup: None,
830 max_height: size.height,
831 encoding: self.encoding().to_string(),
832 is_terminal: self.is_terminal,
833 legacy_windows: self.legacy_windows,
834 size,
835 }
836 }
837
838 pub fn size(&self) -> ConsoleDimensions {
844 if let (Some(width), Some(height)) = (self.width, self.height) {
845 return ConsoleDimensions {
846 width: width.saturating_sub(usize::from(self.legacy_windows)),
847 height,
848 };
849 }
850 if self.is_dumb_terminal() {
851 return ConsoleDimensions {
852 width: DEFAULT_WIDTH,
853 height: DEFAULT_HEIGHT,
854 };
855 }
856 ConsoleDimensions {
857 width: self.width.unwrap_or(self.terminal_size.width),
858 height: self.height.unwrap_or(self.terminal_size.height),
859 }
860 }
861
862 pub fn encoding(&self) -> &'static str {
866 if self.ascii_only { "ascii" } else { "utf-8" }
867 }
868
869 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
876 let segments = self.render_segments(renderable);
877 self.segments_to_string(&segments)
878 }
879
880 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
885 self.render_segments_with(renderable, &self.options())
886 }
887
888 pub fn render_segments_with(
896 &self,
897 renderable: &dyn Renderable,
898 options: &ConsoleOptions,
899 ) -> Vec<Segment> {
900 self.print_segments(renderable, options, false)
901 }
902
903 fn print_segments(
912 &self,
913 renderable: &dyn Renderable,
914 options: &ConsoleOptions,
915 end_line: bool,
916 ) -> Vec<Segment> {
917 let mut options = options.clone();
918 let crop = !self.soft_wrap;
919 if self.soft_wrap {
920 options.no_wrap = options.no_wrap.or(Some(true));
921 options.overflow = options.overflow.or(Some(Overflow::Ignore));
922 }
923 let ends_line = end_line && Self::print_ends_line(renderable, &options);
924 let joined;
925 let joined_text = renderable.printed_text();
926 let renderable = match joined_text.clone() {
927 Some(text) => {
928 if matches!(
932 options.justify,
933 Justify::Left | Justify::Center | Justify::Right
934 ) && text.measurement().1 == 0
935 {
936 return Vec::new();
937 }
938 joined = text;
939 &joined as &dyn Renderable
940 }
941 None => renderable,
942 };
943 let align = match options.justify {
946 Justify::Left if joined_text.is_none() => Some(crate::align::HorizontalAlign::Left),
947 Justify::Center if joined_text.is_none() => Some(crate::align::HorizontalAlign::Center),
948 Justify::Right if joined_text.is_none() => Some(crate::align::HorizontalAlign::Right),
949 _ => None,
950 };
951 if options.justify == Justify::Default && renderable.fit_to_measurement() {
952 let measurement = renderable.measure(self, &options);
953 options.max_width = measurement.maximum.min(options.max_width).max(1);
954 }
955 let mut segments = match align {
956 _ if options.max_width < 1 => Vec::new(),
958 Some(align) => crate::align::Align::render_child(renderable, align, self, &options),
959 None => renderable.rich_render(self, &options),
960 };
961 if !segments.is_empty() {
962 if ends_line {
963 segments.push(Segment::line());
964 }
965 if let Some(style) = self.console_style() {
966 segments = Segment::apply_style_with(&segments, Some(&style), None);
967 }
968 }
969 if end_line {
971 segments = self.process_render_hooks(vec![segments], &options).concat();
972 }
973 if segments.is_empty() || !crop {
974 return segments;
975 }
976 Segment::crop_lines(&segments, self.width())
982 }
983
984 fn console_style(&self) -> Option<Style> {
987 let style = self.style.as_ref()?;
988 self.get_style(style).ok().filter(|style| !style.is_null())
989 }
990
991 pub fn render(
1002 &self,
1003 renderable: &dyn Renderable,
1004 options: Option<&ConsoleOptions>,
1005 ) -> Vec<Segment> {
1006 let default_options;
1007 let options = match options {
1008 Some(options) => options,
1009 None => {
1010 default_options = self.options();
1011 &default_options
1012 }
1013 };
1014 if options.max_width < 1 {
1015 return Vec::new();
1016 }
1017 renderable.rich_render(self, options)
1018 }
1019
1020 fn emit(&self, segments: Vec<Segment>) {
1023 self.emit_end(segments, true);
1024 }
1025
1026 fn emit_end(&self, segments: Vec<Segment>, newline: bool) {
1030 if segments.is_empty() || self.quiet() {
1032 return;
1033 }
1034 let segments = {
1035 let mut captures = self.lock_captures();
1036 if let Some(buffer) = current_capture(&mut captures) {
1037 buffer.extend(segments);
1038 if newline {
1039 buffer.push(Segment::line());
1040 }
1041 return;
1042 }
1043 segments
1044 };
1045 if self.record() {
1047 let mut recorded = self.lock_record_buffer();
1048 recorded.extend(segments.iter().cloned());
1049 if newline {
1050 recorded.push(Segment::line());
1051 }
1052 }
1053 let mut output = self.segments_to_string(&segments);
1054 if newline {
1055 output.push('\n');
1056 }
1057 let stdout = std::io::stdout();
1058 let mut lock = stdout.lock();
1059 let mut written = lock.write_all(output.as_bytes());
1060 if written.is_ok() && !newline {
1061 written = lock.flush();
1062 }
1063 drop(lock);
1064 if written.is_err_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe) {
1067 self.on_broken_pipe();
1068 }
1069 }
1070
1071 pub fn input(&self, prompt: &str) -> std::io::Result<Option<String>> {
1076 let prompt = self.build_text(prompt);
1077 self.input_from(&prompt, &mut crate::prompt::StdinInput)
1078 }
1079
1080 pub fn input_from(
1083 &self,
1084 prompt: &dyn Renderable,
1085 stream: &mut dyn crate::prompt::InputSource,
1086 ) -> std::io::Result<Option<String>> {
1087 let segments = self.render_segments(prompt);
1089 if segments.iter().any(|segment| !segment.text.is_empty()) {
1090 self.emit_end(segments, false);
1091 }
1092 stream.read_line()
1093 }
1094
1095 pub fn render_lines(
1101 &self,
1102 renderable: &dyn Renderable,
1103 options: &ConsoleOptions,
1104 pad: bool,
1105 ) -> Vec<Vec<Segment>> {
1106 self.render_lines_styled(renderable, options, None, pad)
1107 }
1108
1109 pub fn render_lines_styled(
1113 &self,
1114 renderable: &dyn Renderable,
1115 options: &ConsoleOptions,
1116 style: Option<&Style>,
1117 pad: bool,
1118 ) -> Vec<Vec<Segment>> {
1119 let pad_style = style.cloned();
1122 let style = style.filter(|style| !style.is_null());
1123 let mut segments = if options.max_width < 1 {
1126 Vec::new()
1127 } else {
1128 renderable.rich_render(self, options)
1129 };
1130 if let Some(style) = style {
1131 segments = Segment::apply_style(&segments, style);
1132 }
1133 let only_empty = !segments.is_empty()
1137 && segments
1138 .iter()
1139 .all(|segment| !segment.control && segment.text.is_empty());
1140 let mut lines = Segment::split_lines_owned(segments);
1141 if lines.is_empty() && only_empty {
1142 lines.push(Vec::new());
1143 }
1144 if pad {
1145 for line in &mut lines {
1146 *line = Segment::adjust_line_length(line, options.max_width, pad_style.clone());
1147 }
1148 }
1149 if let Some(height) = options.height {
1153 lines.truncate(height);
1154 while lines.len() < height {
1155 lines.push(if pad {
1156 vec![Segment::new(
1157 " ".repeat(options.max_width),
1158 pad_style.clone(),
1159 )]
1160 } else {
1161 Vec::new()
1162 });
1163 }
1164 }
1165 lines
1166 }
1167
1168 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
1172 self.render_export_with(renderable, &self.options())
1173 }
1174
1175 pub fn print(&self, renderable: &dyn Renderable) {
1179 self.print_with(renderable, &self.options());
1180 }
1181
1182 fn print_ends_line(renderable: &dyn Renderable, options: &ConsoleOptions) -> bool {
1185 renderable.print_end_line(options)
1186 || (renderable.printed_text().is_none()
1187 && matches!(
1188 options.justify,
1189 Justify::Left | Justify::Center | Justify::Right
1190 ))
1191 }
1192
1193 pub fn print_with(&self, renderable: &dyn Renderable, options: &ConsoleOptions) {
1199 let segments = self.print_segments(renderable, options, true);
1200 self.emit_end(segments, false);
1201 }
1202
1203 pub fn render_export_with(
1206 &self,
1207 renderable: &dyn Renderable,
1208 options: &ConsoleOptions,
1209 ) -> String {
1210 let segments = self.print_segments(renderable, options, true);
1211 self.segments_to_string(&segments)
1212 }
1213
1214 pub fn control(&self, control: &crate::control::Control) {
1219 let text = control.as_str();
1223 if !text.is_empty() {
1224 self.emit_end(vec![Segment::control(text)], false);
1225 }
1226 }
1227
1228 pub fn push_render_hook(&self, hook: std::sync::Arc<dyn RenderHook>) {
1230 lock_or_recover(&self.render_hooks).push(hook);
1231 }
1232
1233 pub fn pop_render_hook(&self) {
1237 lock_or_recover(&self.render_hooks).pop();
1238 }
1239
1240 fn process_render_hooks(
1244 &self,
1245 items: Vec<Vec<Segment>>,
1246 options: &ConsoleOptions,
1247 ) -> Vec<Vec<Segment>> {
1248 let hooks = lock_or_recover(&self.render_hooks).clone();
1249 if hooks.is_empty() {
1250 return items;
1251 }
1252 let mut renderables: Vec<Box<dyn Renderable>> = items
1253 .into_iter()
1254 .map(|segments| Box::new(Rendered(segments)) as Box<dyn Renderable>)
1255 .collect();
1256 for hook in &hooks {
1257 renderables = hook.process_renderables(renderables);
1258 }
1259 renderables
1260 .iter()
1261 .map(|renderable| {
1262 let mut segments = self.render(renderable.as_ref(), Some(options));
1263 if !segments.is_empty() && renderable.print_end_line(options) {
1264 segments.push(Segment::line());
1265 }
1266 segments
1267 })
1268 .collect()
1269 }
1270
1271 pub fn set_live(&self, live: std::sync::Arc<dyn std::any::Any + Send + Sync>) -> bool {
1282 let mut stack = lock_or_recover(&self.live_stack);
1283 stack.push(live);
1284 stack.len() == 1
1285 }
1286
1287 pub fn clear_live(&self) {
1291 lock_or_recover(&self.live_stack).pop();
1292 }
1293
1294 pub(crate) fn live_stack(&self) -> Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
1297 lock_or_recover(&self.live_stack).clone()
1298 }
1299
1300 pub fn screen(
1307 &self,
1308 hide_cursor: bool,
1309 style: Option<crate::style::StyleType>,
1310 ) -> ScreenContext<'_> {
1311 let changed = self.set_alt_screen(true);
1312 if changed && hide_cursor {
1313 self.show_cursor(false);
1314 }
1315 let style = style.unwrap_or_else(|| "".into());
1317 ScreenContext {
1318 console: self,
1319 hide_cursor,
1320 changed,
1321 screen: Some(crate::screen::Screen::empty().style(style.clone())),
1322 style,
1323 }
1324 }
1325
1326 pub fn status(
1334 &self,
1335 status: crate::status::Status,
1336 ) -> crate::status::LiveStatus<std::io::Stdout> {
1337 self.status_to(status, std::io::stdout())
1338 }
1339
1340 pub fn status_to<W: Write + Send + 'static>(
1343 &self,
1344 status: crate::status::Status,
1345 writer: W,
1346 ) -> crate::status::LiveStatus<W> {
1347 status.start(self.clone(), writer)
1348 }
1349
1350 pub fn is_alt_screen(&self) -> bool {
1353 self.is_alt_screen.load(std::sync::atomic::Ordering::SeqCst)
1354 }
1355
1356 pub fn set_alt_screen(&self, enable: bool) -> bool {
1360 if self.is_terminal && !self.legacy_windows {
1361 self.control(&crate::control::Control::alt_screen(enable));
1362 self.is_alt_screen
1363 .store(enable, std::sync::atomic::Ordering::SeqCst);
1364 return true;
1365 }
1366 false
1367 }
1368
1369 pub fn update_screen(
1372 &self,
1373 renderable: &dyn Renderable,
1374 region: Option<crate::region::Region>,
1375 options: Option<&ConsoleOptions>,
1376 ) -> crate::errors::Result<()> {
1377 if !self.is_alt_screen() {
1378 return Err(no_alt_screen());
1379 }
1380 let render_options = options.cloned().unwrap_or_else(|| self.options());
1381 let (x, y, render_options) = match region {
1382 None => {
1383 let height = render_options
1384 .height
1385 .filter(|&height| height > 0)
1386 .unwrap_or(self.height());
1387 let width = render_options.max_width;
1388 (0, 0, render_options.update_dimensions(width, height))
1389 }
1390 Some(region) => (
1391 region.x,
1392 region.y,
1393 render_options.update_dimensions(region.width, region.height),
1394 ),
1395 };
1396 let lines = self.render_lines(renderable, &render_options, true);
1397 self.update_screen_lines(&lines, x, y)
1398 }
1399
1400 pub fn update_screen_lines(
1403 &self,
1404 lines: &[Vec<Segment>],
1405 x: usize,
1406 y: usize,
1407 ) -> crate::errors::Result<()> {
1408 if !self.is_alt_screen() {
1409 return Err(no_alt_screen());
1410 }
1411 let update = ScreenUpdate::new(lines.to_vec(), x, y);
1412 let segments = self.render(&update, None);
1413 self.emit_end(segments, false);
1414 Ok(())
1415 }
1416
1417 pub fn show_cursor(&self, show: bool) {
1419 self.control(&crate::control::Control::show_cursor(show));
1420 }
1421
1422 pub fn clear(&self) {
1424 self.control(&crate::control::Control::clear());
1425 }
1426
1427 pub fn bell(&self) {
1429 self.control(&crate::control::Control::bell());
1430 }
1431
1432 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
1445 let segments = self.capture_segments(f);
1446 self.segments_to_string(&segments)
1447 }
1448
1449 pub fn begin_capture(&self) {
1453 self.lock_captures()
1454 .entry(std::thread::current().id())
1455 .or_default()
1456 .push(Vec::new());
1457 }
1458
1459 pub fn end_capture(&self) -> String {
1468 let id = std::thread::current().id();
1469 let segments: Vec<Segment> = {
1470 let mut captures = self.lock_captures();
1471 let Some(stack) = captures.get_mut(&id) else {
1472 return String::new();
1473 };
1474 let segments = stack.iter_mut().flat_map(std::mem::take).collect();
1475 stack.pop();
1476 if stack.is_empty() {
1477 captures.remove(&id);
1478 }
1479 segments
1480 };
1481 self.segments_to_string(&segments)
1482 }
1483
1484 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
1487 let segments = self.capture_segments(f);
1488 segments_to_plain(&segments)
1489 }
1490
1491 pub fn export_text_styled(&self, f: impl FnOnce(&Console)) -> String {
1496 let segments = self.capture_segments(f);
1497 self.segments_to_styled_text(&segments)
1498 }
1499
1500 pub fn save_text(
1503 &self,
1504 path: impl AsRef<std::path::Path>,
1505 styles: bool,
1506 f: impl FnOnce(&Console),
1507 ) -> std::io::Result<()> {
1508 let text = if styles {
1509 self.export_text_styled(f)
1510 } else {
1511 self.export_text(f)
1512 };
1513 std::fs::write(path, text)
1514 }
1515
1516 pub fn save_html(
1519 &self,
1520 path: impl AsRef<std::path::Path>,
1521 f: impl FnOnce(&Console),
1522 ) -> std::io::Result<()> {
1523 std::fs::write(path, self.export_html_classes(f))
1524 }
1525
1526 pub fn save_svg(
1529 &self,
1530 path: impl AsRef<std::path::Path>,
1531 title: &str,
1532 unique_id: &str,
1533 f: impl FnOnce(&Console),
1534 ) -> std::io::Result<()> {
1535 std::fs::write(path, self.export_svg(title, unique_id, f))
1536 }
1537
1538 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
1546 self.page_with(&crate::pager::SystemPager, styles, f)
1547 }
1548
1549 pub fn page_with(
1553 &self,
1554 pager: &dyn crate::pager::Pager,
1555 styles: bool,
1556 f: impl FnOnce(&Console),
1557 ) -> std::io::Result<()> {
1558 self.page_with_links(pager, styles, false, f)
1559 }
1560
1561 pub fn page_with_links(
1566 &self,
1567 pager: &dyn crate::pager::Pager,
1568 styles: bool,
1569 links: bool,
1570 f: impl FnOnce(&Console),
1571 ) -> std::io::Result<()> {
1572 let mut segments = self.capture_segments(f);
1573 let content = if !styles {
1574 segments_to_plain(&segments)
1575 } else {
1576 if !links {
1577 for segment in &mut segments {
1578 if let Some(style) = segment.style.as_mut()
1579 && style.link().is_some()
1580 {
1581 *style = style.update_link(None);
1582 }
1583 }
1584 }
1585 self.segments_to_string(&segments)
1586 };
1587 pager.show(&content)
1588 }
1589
1590 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
1594 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1595 }
1596
1597 pub fn export_html_themed(
1603 &self,
1604 theme: &crate::terminal_theme::TerminalTheme,
1605 f: impl FnOnce(&Console),
1606 ) -> String {
1607 let segments = self.capture_segments(f);
1608 crate::export::export_html_inline(&segments, theme)
1609 }
1610
1611 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
1615 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1616 }
1617
1618 pub fn export_html_classes_themed(
1621 &self,
1622 theme: &crate::terminal_theme::TerminalTheme,
1623 f: impl FnOnce(&Console),
1624 ) -> String {
1625 let segments = self.capture_segments(f);
1626 crate::export::export_html_classes(&segments, theme)
1627 }
1628
1629 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
1640 self.export_svg_themed(
1641 &crate::terminal_theme::SVG_EXPORT_THEME,
1642 title,
1643 unique_id,
1644 f,
1645 )
1646 }
1647
1648 pub fn export_svg_themed(
1651 &self,
1652 theme: &crate::terminal_theme::TerminalTheme,
1653 title: &str,
1654 unique_id: &str,
1655 f: impl FnOnce(&Console),
1656 ) -> String {
1657 let segments = self.capture_segments(f);
1658 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
1659 }
1660
1661 pub fn export_html_with(
1666 &self,
1667 theme: &crate::terminal_theme::TerminalTheme,
1668 code_format: Option<&str>,
1669 inline_styles: bool,
1670 f: impl FnOnce(&Console),
1671 ) -> Result<String, crate::export::ExportFormatError> {
1672 let segments = self.capture_segments(f);
1673 crate::export::export_html_with(&segments, theme, code_format, inline_styles)
1674 }
1675
1676 #[allow(clippy::too_many_arguments)]
1682 pub fn export_svg_with(
1683 &self,
1684 theme: &crate::terminal_theme::TerminalTheme,
1685 title: &str,
1686 unique_id: &str,
1687 code_format: Option<&str>,
1688 font_aspect_ratio: f64,
1689 f: impl FnOnce(&Console),
1690 ) -> Result<String, crate::export::ExportFormatError> {
1691 let segments = self.capture_segments(f);
1692 crate::svg::export_svg_with(
1693 &segments,
1694 theme,
1695 title,
1696 unique_id,
1697 self.width(),
1698 code_format.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1699 font_aspect_ratio,
1700 )
1701 }
1702
1703 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1720 self.capture_segments(f)
1721 }
1722
1723 fn capture_segments(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1728 struct CaptureGuard<'a>(&'a Console);
1731 impl CaptureGuard<'_> {
1732 fn pop(&self) -> Vec<Segment> {
1733 let id = std::thread::current().id();
1734 let mut captures = self.0.lock_captures();
1735 let Some(stack) = captures.get_mut(&id) else {
1736 return Vec::new();
1737 };
1738 let captured = stack.pop().unwrap_or_default();
1739 if stack.is_empty() {
1740 captures.remove(&id);
1741 }
1742 captured
1743 }
1744 }
1745 impl Drop for CaptureGuard<'_> {
1746 fn drop(&mut self) {
1747 self.pop();
1748 }
1749 }
1750 self.lock_captures()
1751 .entry(std::thread::current().id())
1752 .or_default()
1753 .push(Vec::new());
1754 let guard = CaptureGuard(self);
1755 f(self);
1756 let captured = guard.pop();
1757 std::mem::forget(guard);
1758 captured
1759 }
1760
1761 pub fn record(&self) -> bool {
1763 self.record.load(std::sync::atomic::Ordering::SeqCst)
1764 }
1765
1766 pub fn set_record(&self, record: bool) {
1769 self.record
1770 .store(record, std::sync::atomic::Ordering::SeqCst);
1771 }
1772
1773 fn take_recorded(&self, clear: bool) -> crate::errors::Result<Vec<Segment>> {
1776 if !self.record() {
1777 return Err(crate::errors::RichError::Console(
1778 "To export console contents set record=True in the constructor or instance"
1779 .to_string(),
1780 ));
1781 }
1782 let mut recorded = self.lock_record_buffer();
1783 Ok(if clear {
1784 std::mem::take(&mut *recorded)
1785 } else {
1786 recorded.clone()
1787 })
1788 }
1789
1790 pub fn export_text_recorded(&self, clear: bool, styles: bool) -> crate::errors::Result<String> {
1805 let segments = self.take_recorded(clear)?;
1806 Ok(if styles {
1807 self.segments_to_styled_text(&segments)
1808 } else {
1809 segments_to_plain(&segments)
1810 })
1811 }
1812
1813 pub fn save_text_recorded(
1816 &self,
1817 path: impl AsRef<std::path::Path>,
1818 clear: bool,
1819 styles: bool,
1820 ) -> std::io::Result<()> {
1821 let text = self
1822 .export_text_recorded(clear, styles)
1823 .map_err(std::io::Error::other)?;
1824 std::fs::write(path, text)
1825 }
1826
1827 pub fn export_html_recorded(
1838 &self,
1839 options: &ExportHtmlOptions<'_>,
1840 ) -> crate::errors::Result<String> {
1841 let segments = self.take_recorded(options.clear)?;
1842 let theme = options
1843 .theme
1844 .unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
1845 crate::export::export_html_with(
1846 &segments,
1847 theme,
1848 options.code_format,
1849 options.inline_styles,
1850 )
1851 .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1852 }
1853
1854 pub fn save_html_recorded(
1857 &self,
1858 path: impl AsRef<std::path::Path>,
1859 options: &ExportHtmlOptions<'_>,
1860 ) -> std::io::Result<()> {
1861 let html = self
1862 .export_html_recorded(options)
1863 .map_err(std::io::Error::other)?;
1864 std::fs::write(path, html)
1865 }
1866
1867 pub fn export_svg_recorded(
1877 &self,
1878 options: &ExportSvgOptions<'_>,
1879 ) -> crate::errors::Result<String> {
1880 let segments: Vec<Segment> = self
1881 .take_recorded(options.clear)?
1882 .into_iter()
1883 .filter(|segment| !segment.control)
1884 .collect();
1885 let theme = options
1886 .theme
1887 .unwrap_or(&crate::terminal_theme::SVG_EXPORT_THEME);
1888 crate::svg::export_svg_with(
1889 &segments,
1890 theme,
1891 options.title,
1892 options.unique_id,
1893 self.width(),
1894 options
1895 .code_format
1896 .unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1897 options.font_aspect_ratio,
1898 )
1899 .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1900 }
1901
1902 pub fn save_svg_recorded(
1905 &self,
1906 path: impl AsRef<std::path::Path>,
1907 options: &ExportSvgOptions<'_>,
1908 ) -> std::io::Result<()> {
1909 let svg = self
1910 .export_svg_recorded(options)
1911 .map_err(std::io::Error::other)?;
1912 std::fs::write(path, svg)
1913 }
1914
1915 fn lock_record_buffer(&self) -> std::sync::MutexGuard<'_, Vec<Segment>> {
1918 self.record_buffer
1919 .lock()
1920 .unwrap_or_else(std::sync::PoisonError::into_inner)
1921 }
1922
1923 fn segments_to_styled_text(&self, segments: &[Segment]) -> String {
1926 let system = self.color_system().unwrap_or(ColorSystem::Truecolor);
1927 segments
1928 .iter()
1929 .map(|segment| match &segment.style {
1930 Some(style) => style.render(&segment.text, Some(system)),
1931 None => segment.text.to_string(),
1932 })
1933 .collect()
1934 }
1935
1936 fn lock_captures(&self) -> std::sync::MutexGuard<'_, CaptureStacks> {
1939 self.captures
1940 .lock()
1941 .unwrap_or_else(std::sync::PoisonError::into_inner)
1942 }
1943
1944 pub fn print_str(&self, content: &str) {
1947 let text = self.build_text(content);
1948 self.print(&text);
1949 }
1950
1951 pub fn render_str_to_string(&self, content: &str) -> String {
1953 let text = self.build_text(content);
1954 self.render_to_string(&text)
1955 }
1956
1957 pub fn build_text(&self, content: &str) -> Text {
1961 self.try_build_text(content)
1966 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
1967 }
1968
1969 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
1973 if !self.markup {
1974 return Ok(self.decorate(Text::new(self.expand_emoji_plain(content))));
1976 }
1977 let markup = self.parse_markup(content, self.emoji)?;
1978
1979 let mut text = self.decorate(Text::new(markup.plain()));
1990 for span in markup.spans() {
1991 text.push_span(span.clone());
1992 }
1993 Ok(text)
1994 }
1995
1996 pub fn render_str(&self, content: &str, highlight: Option<bool>) -> Text {
2005 let highlight = highlight.unwrap_or(self.highlight);
2006 let markup = if !self.markup {
2009 Text::new(self.expand_emoji_plain(content))
2010 } else if content.contains('[') {
2011 self.parse_markup(content, self.emoji)
2012 .unwrap_or_else(|_| Text::new(self.expand_emoji(content)))
2013 } else if content.contains(':') {
2014 Text::new(self.expand_emoji(content))
2015 } else {
2016 Text::new(content)
2017 };
2018 if !highlight {
2019 return markup;
2020 }
2021 let mut text = self.decorate_with_repr(Text::new(markup.plain()));
2024 for span in markup.spans() {
2025 text.push_span(span.clone());
2026 }
2027 text
2028 }
2029
2030 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
2032 self.print(&self.try_build_text(content)?);
2033 Ok(())
2034 }
2035
2036 pub fn try_print_justified(
2039 &self,
2040 content: &str,
2041 justify: Justify,
2042 ) -> crate::errors::Result<()> {
2043 let text = self.try_build_text(content)?;
2044 let mut options = self.options();
2045 options.justify = justify;
2046 self.emit(text.rich_render(self, &options));
2047 Ok(())
2048 }
2049
2050 pub(crate) fn expand_emoji(&self, content: &str) -> String {
2057 if !self.emoji {
2058 return content.to_string();
2059 }
2060 let variant = if content.contains('[') {
2061 None
2062 } else {
2063 self.emoji_variant
2064 };
2065 crate::emoji::replace_with_variant(content, variant)
2066 }
2067
2068 fn parse_markup(&self, content: &str, emoji: bool) -> crate::errors::Result<Text> {
2073 if !content.contains('[') {
2074 return Ok(Text::new(if emoji {
2075 crate::emoji::replace_with_variant(content, self.emoji_variant)
2076 } else {
2077 content.to_string()
2078 }));
2079 }
2080 if emoji {
2081 crate::markup::render_emoji(content)
2082 } else {
2083 crate::markup::render(content)
2084 }
2085 }
2086
2087 fn expand_emoji_plain(&self, content: &str) -> String {
2091 if self.emoji {
2092 crate::emoji::replace_with_variant(content, self.emoji_variant)
2093 } else {
2094 content.to_string()
2095 }
2096 }
2097
2098 pub fn render_str_with(
2107 &self,
2108 content: &str,
2109 options: &RenderStrOptions<'_>,
2110 ) -> crate::errors::Result<Text> {
2111 let emoji = options.emoji.unwrap_or(self.emoji);
2112 let markup = options.markup.unwrap_or(self.markup);
2113 let highlight = options.highlight.unwrap_or(self.highlight);
2114
2115 let mut rich_text = if markup {
2116 self.parse_markup(content, emoji)?
2117 } else if emoji {
2118 Text::new(crate::emoji::replace_with_variant(
2119 content,
2120 self.emoji_variant,
2121 ))
2122 } else {
2123 Text::new(content)
2124 };
2125 rich_text.set_base_style(options.style.clone());
2126 rich_text.set_justify(options.justify.unwrap_or_default());
2127 rich_text.set_overflow(options.overflow);
2128
2129 if !highlight {
2130 return Ok(rich_text);
2131 }
2132 let mut text = Text::new(rich_text.plain());
2133 match options.highlighter {
2134 Some(highlighter) => highlighter.highlight(&mut text),
2135 None => text = self.decorate_with_repr(text),
2136 }
2137 for span in rich_text.spans() {
2138 text.push_span(span.clone());
2139 }
2140 Ok(text)
2141 }
2142
2143 pub fn emoji(&self) -> bool {
2146 self.emoji
2147 }
2148
2149 pub fn highlight(&self) -> bool {
2152 self.highlight
2153 }
2154
2155 pub fn markup(&self) -> bool {
2158 self.markup
2159 }
2160
2161 pub fn emoji_variant(&self) -> Option<crate::emoji::EmojiVariant> {
2163 self.emoji_variant
2164 }
2165
2166 pub fn tab_size(&self) -> usize {
2168 self.tab_size
2169 }
2170
2171 pub fn set_tab_size(&mut self, tab_size: usize) {
2174 self.tab_size = tab_size;
2175 }
2176
2177 pub fn set_window_title(&self, title: &str) -> bool {
2181 if self.is_terminal {
2182 self.control(&crate::control::Control::title(title));
2183 true
2184 } else {
2185 false
2186 }
2187 }
2188
2189 fn decorate(&self, mut text: Text) -> Text {
2192 for highlighter in &self.highlighters {
2193 highlighter.highlight(&mut text);
2194 }
2195 if self.highlight {
2196 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2197 }
2198 text
2199 }
2200
2201 fn decorate_with_repr(&self, mut text: Text) -> Text {
2204 for highlighter in &self.highlighters {
2205 highlighter.highlight(&mut text);
2206 }
2207 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2208 text
2209 }
2210
2211 pub fn print_justified(&self, content: &str, justify: Justify) {
2214 let text = self.build_text(content);
2215 let mut options = self.options();
2216 options.justify = justify;
2217 let segments = text.rich_render(self, &options);
2218 self.emit(segments);
2219 }
2220
2221 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
2226 let text = self.build_text(content);
2227 let mut options = self.options();
2228 options.justify = justify;
2229 let segments = text.rich_render(self, &options);
2230 self.segments_to_string(&segments)
2231 }
2232
2233 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
2236 let system = self.color_system;
2237 let colorless;
2240 let segments = if self.no_color && system.is_some() {
2241 colorless = Segment::remove_color(segments);
2242 &colorless[..]
2243 } else {
2244 segments
2245 };
2246 let mut out = String::new();
2247 for segment in segments {
2248 if segment.control && !self.is_terminal {
2251 continue;
2252 }
2253 match (&segment.style, system) {
2254 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
2255 _ => out.push_str(&segment.text),
2256 }
2257 }
2258 out
2259 }
2260}
2261
2262fn segments_to_plain(segments: &[Segment]) -> String {
2265 segments
2266 .iter()
2267 .filter(|s| !s.control)
2268 .map(|s| s.text.as_str())
2269 .collect()
2270}
2271
2272impl Renderable for String {
2277 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2278 self.options_text(console, options, options.highlight)
2279 .rich_render(console, options)
2280 }
2281
2282 fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2285 let text = self.options_text(console, options, Some(false));
2286 crate::measure::Measurement::get(console, options, &text)
2287 }
2288}
2289
2290trait OptionsText {
2292 fn options_text(
2293 &self,
2294 console: &Console,
2295 options: &ConsoleOptions,
2296 highlight: Option<bool>,
2297 ) -> Text;
2298}
2299
2300impl OptionsText for String {
2301 fn options_text(
2302 &self,
2303 console: &Console,
2304 options: &ConsoleOptions,
2305 highlight: Option<bool>,
2306 ) -> Text {
2307 let render_options = RenderStrOptions {
2308 highlight,
2309 markup: options.markup,
2310 ..Default::default()
2311 };
2312 console
2313 .render_str_with(self, &render_options)
2314 .unwrap_or_else(|_| {
2315 console
2316 .render_str_with(
2317 self,
2318 &RenderStrOptions {
2319 markup: Some(false),
2320 ..render_options
2321 },
2322 )
2323 .unwrap_or_else(|_| Text::new(self.as_str()))
2324 })
2325 }
2326}
2327
2328impl Renderable for Text {
2329 fn printed_text(&self) -> Option<Text> {
2330 let mut text = self.clone();
2336 text.clear_layout_options();
2337 text.base_style_to_span();
2338 Some(text)
2339 }
2340
2341 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2342 let justify = self.get_justify_option().unwrap_or(options.justify);
2347 if self.is_empty() && matches!(justify, Justify::Default | Justify::Full) {
2350 return vec![Segment::new("", None)];
2351 }
2352 let overflow = self
2356 .get_overflow()
2357 .or(options.overflow)
2358 .unwrap_or(Overflow::Fold);
2359 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
2360 self.render_joined_wrapped_tabs(
2362 console.theme(),
2363 console.base_style(),
2364 options.max_width,
2365 justify,
2366 overflow,
2367 no_wrap,
2368 self.console_tab_size(console),
2369 )
2370 }
2371
2372 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2373 let (minimum, maximum) = self.measurement();
2374 crate::measure::Measurement::new(
2375 minimum.min(options.max_width),
2376 maximum.min(options.max_width),
2377 )
2378 }
2379}
2380
2381#[derive(Clone, Copy)]
2384pub struct ExportHtmlOptions<'a> {
2385 pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2388 pub clear: bool,
2390 pub code_format: Option<&'a str>,
2393 pub inline_styles: bool,
2396}
2397
2398impl Default for ExportHtmlOptions<'_> {
2399 fn default() -> Self {
2400 ExportHtmlOptions {
2401 theme: None,
2402 clear: true,
2403 code_format: None,
2404 inline_styles: false,
2405 }
2406 }
2407}
2408
2409#[derive(Clone, Copy)]
2412pub struct ExportSvgOptions<'a> {
2413 pub title: &'a str,
2415 pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2418 pub clear: bool,
2420 pub code_format: Option<&'a str>,
2423 pub font_aspect_ratio: f64,
2425 pub unique_id: &'a str,
2431}
2432
2433impl Default for ExportSvgOptions<'_> {
2434 fn default() -> Self {
2435 ExportSvgOptions {
2436 title: "Rich",
2437 theme: None,
2438 clear: true,
2439 code_format: None,
2440 font_aspect_ratio: 0.61,
2441 unique_id: "terminal",
2442 }
2443 }
2444}
2445
2446pub struct ConsoleBuilder {
2448 force_terminal: Option<bool>,
2449 color_system: Option<ColorSystem>,
2450 color_system_set: bool,
2451 width: Option<usize>,
2452 height: Option<usize>,
2453 no_color: Option<bool>,
2454 get_time: Option<GetTime>,
2455 emoji: Option<bool>,
2456 highlight: Option<bool>,
2457 legacy_windows: Option<bool>,
2458 safe_box: Option<bool>,
2459 ascii_only: Option<bool>,
2460 theme: Option<Theme>,
2461 markup: Option<bool>,
2462 emoji_variant: Option<crate::emoji::EmojiVariant>,
2463 tab_size: Option<usize>,
2464 environ: Option<Environ>,
2465 quiet: bool,
2466 soft_wrap: bool,
2467 style: Option<crate::style::StyleType>,
2468 on_broken_pipe: Option<BrokenPipeHandler>,
2469 record: bool,
2470 log_time: bool,
2471 log_path: bool,
2472 log_time_format: Option<LogTimeFormat>,
2473 get_datetime: Option<GetDateTime>,
2474}
2475
2476impl ConsoleBuilder {
2477 fn new() -> Self {
2478 ConsoleBuilder {
2479 force_terminal: None,
2480 color_system: None,
2481 color_system_set: false,
2482 width: None,
2483 height: None,
2484 no_color: None,
2485 get_time: None,
2486 emoji: None,
2487 highlight: None,
2488 legacy_windows: None,
2489 safe_box: None,
2490 ascii_only: None,
2491 theme: None,
2492 markup: None,
2493 emoji_variant: None,
2494 tab_size: None,
2495 environ: None,
2496 quiet: false,
2497 soft_wrap: false,
2498 style: None,
2499 on_broken_pipe: None,
2500 record: false,
2501 log_time: true,
2502 log_path: true,
2503 log_time_format: None,
2504 get_datetime: None,
2505 }
2506 }
2507
2508 pub fn log_time(mut self, value: bool) -> Self {
2511 self.log_time = value;
2512 self
2513 }
2514
2515 pub fn log_path(mut self, value: bool) -> Self {
2518 self.log_path = value;
2519 self
2520 }
2521
2522 pub fn log_time_format(mut self, format: impl Into<String>) -> Self {
2526 self.log_time_format = Some(LogTimeFormat::Strftime(format.into()));
2527 self
2528 }
2529
2530 pub fn log_time_formatter(
2533 mut self,
2534 format: impl Fn(std::time::SystemTime) -> Text + Send + Sync + 'static,
2535 ) -> Self {
2536 self.log_time_format = Some(LogTimeFormat::Callable(std::sync::Arc::new(format)));
2537 self
2538 }
2539
2540 pub fn get_datetime(
2546 mut self,
2547 clock: impl Fn() -> std::time::SystemTime + Send + Sync + 'static,
2548 ) -> Self {
2549 self.get_datetime = Some(std::sync::Arc::new(clock));
2550 self
2551 }
2552
2553 pub fn record(mut self, value: bool) -> Self {
2557 self.record = value;
2558 self
2559 }
2560
2561 pub fn environ(mut self, environ: Environ) -> Self {
2566 self.environ = Some(environ);
2567 self
2568 }
2569
2570 pub fn quiet(mut self, value: bool) -> Self {
2572 self.quiet = value;
2573 self
2574 }
2575
2576 pub fn soft_wrap(mut self, value: bool) -> Self {
2579 self.soft_wrap = value;
2580 self
2581 }
2582
2583 pub fn style(mut self, style: impl Into<crate::style::StyleType>) -> Self {
2586 self.style = Some(style.into());
2587 self
2588 }
2589
2590 pub fn on_broken_pipe(mut self, handler: impl Fn(&Console) + Send + Sync + 'static) -> Self {
2594 self.on_broken_pipe = Some(std::sync::Arc::new(handler));
2595 self
2596 }
2597
2598 pub fn markup(mut self, value: bool) -> Self {
2601 self.markup = Some(value);
2602 self
2603 }
2604
2605 pub fn emoji_variant(mut self, variant: Option<crate::emoji::EmojiVariant>) -> Self {
2608 self.emoji_variant = variant;
2609 self
2610 }
2611
2612 pub fn tab_size(mut self, tab_size: usize) -> Self {
2615 self.tab_size = Some(tab_size);
2616 self
2617 }
2618
2619 pub fn force_terminal(mut self, value: bool) -> Self {
2620 self.force_terminal = Some(value);
2621 self
2622 }
2623
2624 pub fn legacy_windows(mut self, value: bool) -> Self {
2626 self.legacy_windows = Some(value);
2627 self
2628 }
2629
2630 pub fn safe_box(mut self, value: bool) -> Self {
2632 self.safe_box = Some(value);
2633 self
2634 }
2635
2636 pub fn ascii_only(mut self, value: bool) -> Self {
2638 self.ascii_only = Some(value);
2639 self
2640 }
2641
2642 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
2644 self.color_system = system;
2645 self.color_system_set = true;
2646 self
2647 }
2648
2649 pub fn width(mut self, width: usize) -> Self {
2650 self.width = Some(width);
2651 self
2652 }
2653
2654 pub fn height(mut self, height: usize) -> Self {
2656 self.height = Some(height);
2657 self
2658 }
2659
2660 pub fn no_color(mut self, value: bool) -> Self {
2664 self.no_color = Some(value);
2665 self
2666 }
2667
2668 pub fn get_time(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
2672 self.get_time = Some(std::sync::Arc::new(clock));
2673 self
2674 }
2675
2676 pub fn emoji(mut self, value: bool) -> Self {
2678 self.emoji = Some(value);
2679 self
2680 }
2681
2682 pub fn highlight(mut self, value: bool) -> Self {
2685 self.highlight = Some(value);
2686 self
2687 }
2688
2689 pub fn theme(mut self, theme: Theme) -> Self {
2690 self.theme = Some(theme);
2691 self
2692 }
2693
2694 pub fn build(self) -> Console {
2695 let environ = self.environ.map(std::sync::Arc::new);
2696 let env = environ.as_deref();
2697 let legacy_windows = self.legacy_windows.unwrap_or(false);
2698 let is_terminal = self
2699 .force_terminal
2700 .unwrap_or_else(|| detect_is_terminal(env));
2701 let no_color = self
2706 .no_color
2707 .unwrap_or_else(|| environ_get(env, "NO_COLOR").is_some_and(|value| !value.is_empty()));
2708 let color_system = if self.color_system_set {
2711 self.color_system
2712 } else if is_terminal && !term_is_dumb(env) {
2713 Some(detect_color_system(env))
2714 } else {
2715 None
2716 };
2717 let width = self.width.or_else(|| {
2720 env_int(env, "COLUMNS")
2721 .map(|columns| columns.saturating_sub(usize::from(legacy_windows)))
2722 });
2723 let height = self.height.or_else(|| env_int(env, "LINES"));
2724 let terminal_size = detect_terminal_size(legacy_windows);
2725 Console {
2726 render_environment: None,
2727 region_sink: None,
2728 code_highlighting: None,
2729 color_system,
2730 width,
2731 height,
2732 terminal_size,
2733 environ,
2734 is_terminal,
2735 no_color,
2736 quiet: std::sync::atomic::AtomicBool::new(self.quiet),
2737 soft_wrap: self.soft_wrap,
2738 style: self.style,
2739 on_broken_pipe: self.on_broken_pipe,
2740 get_time: self
2741 .get_time
2742 .unwrap_or_else(|| std::sync::Arc::new(monotonic)),
2743 emoji: self.emoji.unwrap_or(true),
2744 highlight: self.highlight.unwrap_or(true),
2749 legacy_windows,
2750 safe_box: self.safe_box.unwrap_or(true),
2751 ascii_only: self.ascii_only.unwrap_or(false),
2752 theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
2753 base_style: Style::new(),
2754 highlighters: Vec::new(),
2755 markup: self.markup.unwrap_or(true),
2756 emoji_variant: self.emoji_variant,
2757 tab_size: self.tab_size.unwrap_or(crate::text::DEFAULT_TAB_SIZE),
2759 captures: std::sync::Mutex::new(CaptureStacks::new()),
2760 is_alt_screen: std::sync::atomic::AtomicBool::new(false),
2761 record: std::sync::atomic::AtomicBool::new(self.record),
2762 record_buffer: std::sync::Mutex::new(Vec::new()),
2763 render_hooks: std::sync::Mutex::new(Vec::new()),
2764 live_stack: LiveStack::default(),
2765 log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
2766 log_time: self.log_time,
2767 log_path: self.log_path,
2768 log_time_format: self
2769 .log_time_format
2770 .unwrap_or_else(|| LogTimeFormat::Strftime("[%X]".to_string())),
2771 get_datetime: self
2772 .get_datetime
2773 .unwrap_or_else(|| std::sync::Arc::new(std::time::SystemTime::now)),
2774 }
2775 }
2776}
2777
2778fn absolute_path(path: &str) -> String {
2781 let path = std::path::Path::new(path);
2782 if path.is_absolute() {
2783 return path.to_string_lossy().into_owned();
2784 }
2785 std::env::current_dir()
2786 .map(|directory| directory.join(path).to_string_lossy().into_owned())
2787 .unwrap_or_else(|_| path.to_string_lossy().into_owned())
2788}
2789
2790fn new_log_render(log_time: bool, log_path: bool) -> crate::log_render::LogRender {
2792 crate::log_render::LogRender::new()
2793 .show_time(log_time)
2794 .show_path(log_path)
2795}
2796
2797fn term_is_dumb(environ: Option<&Environ>) -> bool {
2800 environ_get(environ, "TERM")
2801 .is_some_and(|term| matches!(term.to_lowercase().as_str(), "dumb" | "unknown"))
2802}
2803
2804fn detect_is_terminal(environ: Option<&Environ>) -> bool {
2808 match environ_get(environ, "TTY_COMPATIBLE").as_deref() {
2809 Some("0") => return false,
2810 Some("1") => return true,
2811 _ => {}
2812 }
2813 if let Some(force_color) = environ_get(environ, "FORCE_COLOR") {
2814 return !force_color.is_empty();
2815 }
2816 std::io::stdout().is_terminal()
2817}
2818
2819#[cfg(not(windows))]
2822fn term_colors(colors: &str) -> Option<ColorSystem> {
2823 match colors {
2824 "kitty" | "256color" => Some(ColorSystem::EightBit),
2825 "16color" => Some(ColorSystem::Standard),
2826 _ => None,
2827 }
2828}
2829
2830#[cfg_attr(windows, allow(unused_variables))]
2843fn detect_color_system(environ: Option<&Environ>) -> ColorSystem {
2844 #[cfg(windows)]
2856 {
2857 let _ = anstyle_query::windows::enable_ansi_colors();
2858 ColorSystem::Truecolor
2859 }
2860
2861 #[cfg(not(windows))]
2864 {
2865 let color_term = environ_get(environ, "COLORTERM")
2866 .unwrap_or_default()
2867 .trim()
2868 .to_lowercase();
2869 if matches!(color_term.as_str(), "truecolor" | "24bit") {
2870 return ColorSystem::Truecolor;
2871 }
2872 let term = environ_get(environ, "TERM")
2873 .unwrap_or_default()
2874 .trim()
2875 .to_lowercase();
2876 let colors = term.rsplit('-').next().unwrap_or("");
2878 term_colors(colors).unwrap_or(ColorSystem::Standard)
2879 }
2880}
2881
2882fn detect_terminal_size(legacy_windows: bool) -> ConsoleDimensions {
2886 let (width, height) = match terminal_size::terminal_size() {
2887 Some((terminal_size::Width(w), terminal_size::Height(h))) => (w as usize, h as usize),
2888 None => (0, 0),
2889 };
2890 let width = if width == 0 { DEFAULT_WIDTH } else { width };
2891 let height = if height == 0 { DEFAULT_HEIGHT } else { height };
2892 ConsoleDimensions {
2893 width: width - usize::from(legacy_windows),
2894 height,
2895 }
2896}
2897
2898impl crate::protocol::ConsoleCodeHighlighting for Console {
2899 fn set_code_highlighting(&mut self, value: Option<crate::protocol::CodeHighlighting>) {
2900 self.code_highlighting = value;
2901 }
2902 fn code_highlighting(&self) -> Option<&crate::protocol::CodeHighlighting> {
2903 self.code_highlighting.as_ref()
2904 }
2905}
2906
2907impl crate::protocol::ConsoleRegions for Console {
2908 fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>) {
2909 self.region_sink = value;
2910 }
2911 fn region_sink(&self) -> Option<&dyn crate::protocol::RegionSink> {
2912 self.region_sink.as_deref()
2913 }
2914}
2915
2916impl crate::protocol::ConsoleEnvironment for Console {
2917 fn set_render_environment(
2918 &mut self,
2919 value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
2920 ) {
2921 self.render_environment = value;
2922 }
2923 fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
2924 self.render_environment.as_deref()
2925 }
2926}
2927
2928pub enum PrintObject<'a> {
2931 Str(&'a str),
2934 Text(&'a Text),
2936 Renderable(&'a dyn Renderable),
2938}
2939
2940#[derive(Clone)]
2942pub struct PrintOptions<'a> {
2943 pub sep: &'a str,
2944 pub end: &'a str,
2945 pub style: Option<crate::style::StyleType>,
2946 pub justify: Option<Justify>,
2947 pub overflow: Option<Overflow>,
2948 pub no_wrap: Option<bool>,
2949 pub emoji: Option<bool>,
2950 pub markup: Option<bool>,
2951 pub highlight: Option<bool>,
2952 pub width: Option<usize>,
2953 pub height: Option<usize>,
2954 pub crop: bool,
2955 pub soft_wrap: Option<bool>,
2956 pub new_line_start: bool,
2957}
2958
2959impl Default for PrintOptions<'_> {
2960 fn default() -> Self {
2961 PrintOptions {
2962 sep: " ",
2963 end: "\n",
2964 style: None,
2965 justify: None,
2966 overflow: None,
2967 no_wrap: None,
2968 emoji: None,
2969 markup: None,
2970 highlight: None,
2971 width: None,
2972 height: None,
2973 crop: true,
2974 soft_wrap: None,
2975 new_line_start: false,
2976 }
2977 }
2978}
2979
2980#[derive(Clone)]
2985pub enum LogObject<'a> {
2986 Str(&'a str),
2989 Text(&'a Text),
2991 Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
2993}
2994
2995#[derive(Clone)]
3000pub struct LogOptions<'a> {
3001 pub sep: &'a str,
3002 pub end: &'a str,
3003 pub style: Option<crate::style::StyleType>,
3004 pub justify: Option<Justify>,
3005 pub emoji: Option<bool>,
3006 pub markup: Option<bool>,
3007 pub highlight: Option<bool>,
3008}
3009
3010impl Default for LogOptions<'_> {
3011 fn default() -> Self {
3012 LogOptions {
3013 sep: " ",
3014 end: "\n",
3015 style: None,
3016 justify: None,
3017 emoji: None,
3018 markup: None,
3019 highlight: None,
3020 }
3021 }
3022}
3023
3024pub(crate) struct OwnedLog {
3027 objects: Vec<OwnedLogObject>,
3028 sep: String,
3029 end: String,
3030 style: Option<crate::style::StyleType>,
3031 justify: Option<Justify>,
3032 emoji: Option<bool>,
3033 markup: Option<bool>,
3034 highlight: Option<bool>,
3035 file: String,
3036 line: u32,
3037}
3038
3039enum OwnedLogObject {
3040 Str(String),
3041 Text(Text),
3042 Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
3043}
3044
3045impl OwnedLog {
3046 pub(crate) fn new(
3048 objects: &[LogObject<'_>],
3049 options: &LogOptions<'_>,
3050 file: &str,
3051 line: u32,
3052 ) -> Self {
3053 OwnedLog {
3054 objects: objects
3055 .iter()
3056 .map(|object| match object {
3057 LogObject::Str(content) => OwnedLogObject::Str((*content).to_string()),
3058 LogObject::Text(text) => OwnedLogObject::Text((*text).clone()),
3059 LogObject::Renderable(renderable) => {
3060 OwnedLogObject::Renderable(renderable.clone())
3061 }
3062 })
3063 .collect(),
3064 sep: options.sep.to_string(),
3065 end: options.end.to_string(),
3066 style: options.style.clone(),
3067 justify: options.justify,
3068 emoji: options.emoji,
3069 markup: options.markup,
3070 highlight: options.highlight,
3071 file: file.to_string(),
3072 line,
3073 }
3074 }
3075
3076 pub(crate) fn log(&self, console: &Console) -> crate::errors::Result<()> {
3078 let objects: Vec<LogObject<'_>> = self
3079 .objects
3080 .iter()
3081 .map(|object| match object {
3082 OwnedLogObject::Str(content) => LogObject::Str(content),
3083 OwnedLogObject::Text(text) => LogObject::Text(text),
3084 OwnedLogObject::Renderable(renderable) => LogObject::Renderable(renderable.clone()),
3085 })
3086 .collect();
3087 let options = LogOptions {
3088 sep: &self.sep,
3089 end: &self.end,
3090 style: self.style.clone(),
3091 justify: self.justify,
3092 emoji: self.emoji,
3093 markup: self.markup,
3094 highlight: self.highlight,
3095 };
3096 console.log_at(&objects, &options, &self.file, self.line)
3097 }
3098}
3099
3100struct LogItem {
3104 renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3105 align: Option<crate::align::HorizontalAlign>,
3106 end: Option<String>,
3110 styles: Vec<Style>,
3111}
3112
3113impl Renderable for LogItem {
3114 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
3115 let mut segments = match self.align {
3116 Some(align) => {
3117 crate::align::Align::render_child(&*self.renderable, align, console, options)
3118 }
3119 None => self.renderable.rich_render(console, options),
3120 };
3121 if let Some(end) = self.end.as_deref().filter(|end| !end.is_empty()) {
3122 segments.push(Segment::new(end, None));
3123 if end.ends_with('\n') {
3124 segments.push(Segment::new("", None));
3126 }
3127 }
3128 for style in &self.styles {
3129 segments = Segment::apply_style_with(&segments, Some(style), None);
3130 }
3131 segments
3132 }
3133
3134 fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
3135 crate::measure::Measurement::get(console, options, &*self.renderable)
3136 }
3137}
3138
3139impl Console {
3141 #[track_caller]
3150 pub fn log(
3151 &self,
3152 objects: &[LogObject<'_>],
3153 options: &LogOptions<'_>,
3154 ) -> crate::errors::Result<()> {
3155 let caller = std::panic::Location::caller();
3156 self.log_at(objects, options, caller.file(), caller.line())
3157 }
3158
3159 pub fn log_at(
3163 &self,
3164 objects: &[LogObject<'_>],
3165 options: &LogOptions<'_>,
3166 file: &str,
3167 line: u32,
3168 ) -> crate::errors::Result<()> {
3169 let mut styles = Vec::new();
3170 if let Some(style) = &self.style {
3171 styles.push(self.get_style(style)?);
3172 }
3173 if let Some(style) = &options.style {
3174 styles.push(self.get_style(style)?);
3175 }
3176 styles.retain(|style| !style.is_null());
3177 let align = match options.justify {
3178 Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3179 Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3180 Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3181 _ => None,
3182 };
3183 let item = |renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3184 end: Option<String>|
3185 -> std::sync::Arc<dyn Renderable + Send + Sync> {
3186 std::sync::Arc::new(LogItem {
3187 renderable,
3188 align,
3189 end,
3190 styles: styles.clone(),
3191 })
3192 };
3193 let end = options
3196 .end
3197 .strip_suffix('\n')
3198 .unwrap_or(options.end)
3199 .to_string();
3200 let mut renderables: Vec<std::sync::Arc<dyn Renderable + Send + Sync>> = Vec::new();
3201 if objects.is_empty() {
3202 renderables.push(item(std::sync::Arc::new(Text::new("")), None));
3204 }
3205 let mut texts: Vec<Text> = Vec::new();
3206 let flush =
3207 |texts: &mut Vec<Text>,
3208 renderables: &mut Vec<std::sync::Arc<dyn Renderable + Send + Sync>>| {
3209 if !texts.is_empty() {
3210 let mut separator = Text::new(options.sep);
3211 separator.set_justify_option(options.justify);
3212 let joined = separator.join(texts);
3213 renderables.push(item(std::sync::Arc::new(joined), Some(end.clone())));
3214 texts.clear();
3215 }
3216 };
3217 for object in objects {
3218 match object {
3219 LogObject::Str(content) => {
3220 let render_str = RenderStrOptions {
3221 emoji: options.emoji,
3222 markup: options.markup,
3223 highlight: options.highlight,
3224 ..Default::default()
3225 };
3226 texts.push(self.render_str_with(content, &render_str)?);
3227 }
3228 LogObject::Text(text) => texts.push((*text).clone()),
3229 LogObject::Renderable(renderable) => {
3230 flush(&mut texts, &mut renderables);
3231 renderables.push(item(renderable.clone(), None));
3232 }
3233 }
3234 }
3235 flush(&mut texts, &mut renderables);
3236
3237 let time = self.log_time.then(|| {
3238 let now = (self.get_datetime)();
3239 match &self.log_time_format {
3240 LogTimeFormat::Strftime(format) => {
3241 Text::new(crate::log_render::strftime(now, format))
3242 }
3243 LogTimeFormat::Callable(format) => format(now),
3244 }
3245 });
3246 let path = file
3249 .rsplit(std::path::MAIN_SEPARATOR)
3250 .next()
3251 .unwrap_or(file);
3252 let link_path = (!file.starts_with('<')).then(|| absolute_path(file));
3253 let table = self
3254 .log_render
3255 .lock()
3256 .unwrap_or_else(std::sync::PoisonError::into_inner)
3257 .render_renderables(
3258 self,
3259 renderables,
3260 time,
3261 Text::new(""),
3262 Some(path),
3263 Some(line),
3264 link_path.as_deref(),
3265 );
3266 let mut segments = self.render(&table, None);
3267 if !segments.is_empty() {
3268 segments.push(Segment::line());
3269 }
3270 let segments = self
3271 .process_render_hooks(vec![segments], &self.options())
3272 .concat();
3273 let segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3274 .into_iter()
3275 .flatten()
3276 .collect();
3277 self.emit_end(segments, false);
3278 Ok(())
3279 }
3280
3281 pub fn print_objects(
3288 &self,
3289 objects: &[PrintObject<'_>],
3290 options: &PrintOptions<'_>,
3291 ) -> crate::errors::Result<()> {
3292 let segments = self.render_objects(objects, options)?;
3293 self.emit_end(segments, false);
3294 Ok(())
3295 }
3296
3297 pub fn render_objects(
3300 &self,
3301 objects: &[PrintObject<'_>],
3302 options: &PrintOptions<'_>,
3303 ) -> crate::errors::Result<Vec<Segment>> {
3304 let (mut no_wrap, mut overflow, mut crop) =
3305 (options.no_wrap, options.overflow, options.crop);
3306 if options.soft_wrap.unwrap_or(self.soft_wrap) {
3309 no_wrap = no_wrap.or(Some(true));
3310 overflow = overflow.or(Some(Overflow::Ignore));
3311 crop = false;
3312 }
3313 let align = match options.justify {
3314 Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3315 Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3316 Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3317 _ => None,
3318 };
3319 let mut render_options = self.options();
3320 if let Some(justify) = options.justify {
3321 render_options.justify = justify;
3322 }
3323 render_options.overflow = overflow;
3324 render_options.no_wrap = no_wrap;
3325 render_options.markup = options.markup;
3326 render_options.highlight = options.highlight;
3327 if let Some(width) = options.width {
3328 render_options = render_options.update_width(width.min(self.width()));
3329 }
3330 if options.height.is_some() {
3331 render_options.height = options.height;
3332 }
3333 let style = match &options.style {
3334 Some(style) => Some(self.get_style(style)?),
3335 None => None,
3336 };
3337 let console_style = match &self.style {
3340 Some(style) => Some(self.get_style(style)?).filter(|style| !style.is_null()),
3341 None => None,
3342 };
3343
3344 let mut items: Vec<Vec<Segment>> = Vec::new();
3346 let mut emit = |renderable: &dyn Renderable, end: Option<&str>| {
3347 let mut rendered = if render_options.max_width < 1 {
3348 Vec::new()
3349 } else {
3350 match align {
3351 Some(align) => {
3352 crate::align::Align::render_child(renderable, align, self, &render_options)
3353 }
3354 None => renderable.rich_render(self, &render_options),
3355 }
3356 };
3357 match end {
3360 Some(end) if !end.is_empty() => rendered.push(Segment::new(end, None)),
3361 Some(_) => {}
3362 None if !rendered.is_empty()
3366 && Self::print_ends_line(renderable, &render_options) =>
3367 {
3368 rendered.push(Segment::line())
3369 }
3370 None => {}
3371 }
3372 if let Some(console_style) = &console_style {
3373 rendered = Segment::apply_style_with(&rendered, Some(console_style), None);
3374 }
3375 items.push(rendered);
3376 };
3377
3378 let blank = [PrintObject::Str("")];
3381 let objects = if objects.is_empty() && options.end != "\n" {
3382 &blank[..]
3383 } else {
3384 objects
3385 };
3386 if objects.is_empty() {
3387 emit(&Text::new(""), Some("\n"));
3388 }
3389 let mut texts: Vec<Text> = Vec::new();
3390 let flush = |texts: &mut Vec<Text>, emit: &mut dyn FnMut(&dyn Renderable, Option<&str>)| {
3391 if !texts.is_empty() {
3392 let mut separator = Text::new(options.sep);
3393 separator.set_justify_option(options.justify);
3394 emit(&separator.join(texts), Some(options.end));
3395 texts.clear();
3396 }
3397 };
3398 for object in objects {
3399 match object {
3400 PrintObject::Str(content) => {
3401 let render_str = RenderStrOptions {
3402 emoji: options.emoji,
3403 markup: options.markup,
3404 highlight: options.highlight,
3405 ..Default::default()
3406 };
3407 texts.push(self.render_str_with(content, &render_str)?);
3408 }
3409 PrintObject::Text(text) => texts.push((*text).clone()),
3410 PrintObject::Renderable(renderable) => {
3411 flush(&mut texts, &mut emit);
3412 emit(*renderable, None);
3413 }
3414 }
3415 }
3416 flush(&mut texts, &mut emit);
3417
3418 let mut segments: Vec<Segment> = Vec::new();
3419 for mut rendered in self.process_render_hooks(items, &render_options) {
3420 if let Some(style) = &style {
3422 let mut styled = Vec::with_capacity(rendered.len());
3423 for (line, terminated) in Segment::split_lines_terminator(&rendered) {
3424 styled.extend(Segment::apply_style_with(&line, Some(style), None));
3425 if terminated {
3426 styled.push(Segment::line());
3427 }
3428 }
3429 rendered = styled;
3430 }
3431 segments.extend(rendered);
3432 }
3433
3434 if options.new_line_start {
3435 let plain: String = segments
3436 .iter()
3437 .map(|segment| segment.text.as_str())
3438 .collect();
3439 if plain.lines().count() > 1 {
3440 segments.insert(0, Segment::line());
3441 }
3442 }
3443 if crop {
3444 segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3445 .into_iter()
3446 .flatten()
3447 .collect();
3448 }
3449 Ok(segments)
3450 }
3451
3452 pub fn line(&self, count: usize) {
3454 if count > 0 {
3455 self.emit_end(vec![Segment::new("\n".repeat(count), None)], false);
3456 }
3457 }
3458
3459 pub fn rule(
3462 &self,
3463 title: &str,
3464 characters: &str,
3465 style: impl Into<crate::style::StyleType>,
3466 align: crate::align::HorizontalAlign,
3467 ) {
3468 let rule = crate::rule::Rule::new(title)
3469 .characters(characters)
3470 .style(style)
3471 .align(align);
3472 self.print(&rule);
3473 }
3474
3475 pub fn out(
3478 &self,
3479 objects: &[&str],
3480 sep: &str,
3481 end: &str,
3482 style: Option<crate::style::StyleType>,
3483 highlight: Option<bool>,
3484 ) -> crate::errors::Result<()> {
3485 let raw = objects.join(sep);
3486 self.print_objects(
3487 &[PrintObject::Str(&raw)],
3488 &PrintOptions {
3489 end,
3490 style,
3491 highlight,
3492 emoji: Some(false),
3493 markup: Some(false),
3494 no_wrap: Some(true),
3495 overflow: Some(Overflow::Ignore),
3496 crop: false,
3497 ..Default::default()
3498 },
3499 )
3500 }
3501
3502 pub fn print_json(
3505 &self,
3506 json: &str,
3507 options: &crate::json::JsonOptions,
3508 ) -> crate::errors::Result<()> {
3509 let renderable = crate::json::Json::with_options(json, options)?;
3510 self.print_objects(
3511 &[PrintObject::Renderable(&renderable)],
3512 &PrintOptions {
3513 soft_wrap: Some(true),
3514 ..Default::default()
3515 },
3516 )
3517 }
3518
3519 pub fn measure(
3522 &self,
3523 renderable: &dyn Renderable,
3524 options: Option<&ConsoleOptions>,
3525 ) -> crate::measure::Measurement {
3526 let default_options;
3527 let options = match options {
3528 Some(options) => options,
3529 None => {
3530 default_options = self.options();
3531 &default_options
3532 }
3533 };
3534 crate::measure::Measurement::get(self, options, renderable)
3535 }
3536}
3537
3538#[cfg(test)]
3539mod tests {
3540 use super::*;
3541
3542 fn test_console() -> Console {
3543 Console::builder()
3544 .force_terminal(true)
3545 .color_system(Some(ColorSystem::Truecolor))
3546 .width(80)
3547 .no_color(false)
3548 .build()
3549 }
3550
3551 #[test]
3554 fn empty_text_and_empty_renderables_have_distinct_endings() {
3555 let console = Console::builder().force_terminal(false).build();
3556 assert_eq!(console.render_export(&Text::new("")), "\n");
3557 #[cfg(feature = "markdown")]
3558 assert_eq!(
3559 console.render_export(&crate::markdown::Markdown::new("")),
3560 ""
3561 );
3562 assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
3563 }
3564
3565 #[test]
3566 fn try_build_text_reports_bad_markup() {
3567 let console = test_console();
3568
3569 let err = console
3570 .try_build_text("[/nope]")
3571 .expect_err("an unmatched closing tag must be an error");
3572 assert!(
3573 matches!(err, crate::errors::RichError::Markup(_)),
3574 "{err:?}"
3575 );
3576 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
3578
3579 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
3580 assert_eq!(strict.plain(), "hi");
3581 assert_eq!(
3582 strict.spans().len(),
3583 console.build_text("[bold]hi[/]").spans().len()
3584 );
3585 }
3586
3587 #[test]
3595 fn unknown_tag_names_render_as_no_ops() {
3596 let console = test_console();
3597 let text = console
3598 .try_build_text("[nope]x[/]")
3599 .expect("an unknown tag name is not a syntax error");
3600 assert_eq!(console.render_to_string(&text), "x");
3601 assert_eq!(
3602 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
3603 "x"
3604 );
3605
3606 assert!(console.try_build_text("[bold]a[/italic]").is_err());
3608 assert!(console.try_build_text("[/nope]").is_err());
3609 }
3610
3611 #[test]
3614 fn markup_styles_bind_at_render_not_at_parse() {
3615 let themed = |definition: &str| {
3616 let mut theme = Theme::default_theme();
3617 theme.insert("accent", Style::parse(definition).unwrap());
3618 Console::builder()
3619 .force_terminal(true)
3620 .color_system(Some(ColorSystem::Truecolor))
3621 .width(80)
3622 .no_color(false)
3623 .theme(theme)
3624 .build()
3625 };
3626 let red = themed("bold red");
3627 let green = themed("underline green");
3628
3629 let text = red.build_text("[accent]hi[/]");
3631 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
3632 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
3634 }
3635
3636 #[test]
3639 fn try_build_text_expands_emoji_like_build_text() {
3640 let console = test_console();
3641 assert_eq!(
3642 console
3643 .try_build_text(":rocket: go")
3644 .expect("valid")
3645 .plain(),
3646 console.build_text(":rocket: go").plain()
3647 );
3648 }
3649
3650 #[test]
3651 fn renders_markup_string() {
3652 let console = test_console();
3653 assert_eq!(
3654 console.render_str_to_string("[bold red]hi[/]"),
3655 "\x1b[1;31mhi\x1b[0m"
3656 );
3657 }
3658
3659 #[test]
3660 fn print_justify_pads_to_width() {
3661 let console = Console::builder()
3662 .force_terminal(true)
3663 .color_system(Some(ColorSystem::Truecolor))
3664 .width(10)
3665 .build();
3666 assert_eq!(
3668 console.render_justified_to_string("hi", Justify::Left),
3669 "hi "
3670 );
3671 assert_eq!(
3672 console.render_justified_to_string("hi", Justify::Center),
3673 " hi "
3674 );
3675 assert_eq!(
3676 console.render_justified_to_string("hi", Justify::Right),
3677 " hi"
3678 );
3679 }
3680
3681 #[test]
3682 fn capture_records_ansi_instead_of_stdout() {
3683 let console = Console::builder()
3684 .force_terminal(true)
3685 .color_system(Some(ColorSystem::Truecolor))
3686 .width(20)
3687 .build();
3688 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
3690 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
3691 }
3692
3693 #[test]
3694 fn themed_exports_use_the_given_palette() {
3695 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
3696
3697 let console = Console::builder()
3698 .force_terminal(true)
3699 .color_system(Some(ColorSystem::Truecolor))
3700 .width(20)
3701 .no_color(false)
3702 .build();
3703 let render = |c: &Console| c.print_str("hi");
3704
3705 let monokai = console.export_html_themed(&MONOKAI, render);
3708 assert!(
3709 monokai.contains("#0c0c0c"),
3710 "monokai bg missing:\n{monokai}"
3711 );
3712
3713 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
3714 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
3715 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
3716
3717 let classes = console.export_html_classes_themed(&MONOKAI, render);
3719 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
3720 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
3721 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
3722
3723 assert!(console.export_html(render).contains("#ffffff"));
3725 }
3726
3727 #[test]
3728 fn page_with_honors_the_styles_flag() {
3729 use std::sync::Mutex;
3730
3731 #[derive(Default)]
3732 struct Recorder(Mutex<String>);
3733 impl crate::pager::Pager for Recorder {
3734 fn show(&self, content: &str) -> std::io::Result<()> {
3735 *self.0.lock().unwrap() = content.to_string();
3736 Ok(())
3737 }
3738 }
3739
3740 let console = Console::builder()
3741 .force_terminal(true)
3742 .color_system(Some(ColorSystem::Truecolor))
3743 .width(20)
3744 .no_color(false)
3745 .build();
3746
3747 let plain = Recorder::default();
3749 console
3750 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
3751 .unwrap();
3752 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
3753
3754 let styled = Recorder::default();
3756 console
3757 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
3758 .unwrap();
3759 assert_eq!(
3760 styled.0.lock().unwrap().as_str(),
3761 "\x1b[1;31mhi\x1b[0m there\n"
3762 );
3763
3764 let linked = |links: bool| {
3767 let recorder = Recorder::default();
3768 console
3769 .page_with_links(&recorder, true, links, |c| {
3770 c.print_str("[bold link=https://x.io]hi[/] there")
3771 })
3772 .unwrap();
3773
3774 recorder.0.lock().unwrap().clone()
3775 };
3776 assert_eq!(linked(false), "\x1b[1mhi\x1b[0m there\n");
3777 assert_eq!(
3778 linked(true),
3779 "\x1b]8;;https://x.io\x1b\\\x1b[1mhi\x1b[0m\x1b]8;;\x1b\\ there\n"
3780 );
3781 }
3782
3783 #[test]
3784 fn export_text_styled_renders_truecolor() {
3785 let console = |system| {
3788 Console::builder()
3789 .force_terminal(true)
3790 .color_system(system)
3791 .width(20)
3792 .no_color(false)
3793 .build()
3794 };
3795 let export =
3796 |console: Console| console.export_text_styled(|c| c.print_str("[#ff8800]x[/] y"));
3797 assert_eq!(
3798 export(console(Some(ColorSystem::Standard))),
3799 "\x1b[91mx\x1b[0m y\n"
3800 );
3801 assert_eq!(export(console(None)), "\x1b[38;2;255;136;0mx\x1b[0m y\n");
3802 }
3803
3804 #[test]
3805 fn export_text_strips_styles() {
3806 let console = Console::builder()
3807 .force_terminal(true)
3808 .color_system(Some(ColorSystem::Truecolor))
3809 .width(20)
3810 .build();
3811 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
3813 assert_eq!(out, "hi there\n");
3814 }
3815
3816 #[test]
3817 fn export_html_matches_upstream() {
3818 let console = Console::builder()
3819 .force_terminal(true)
3820 .color_system(Some(ColorSystem::Truecolor))
3821 .width(20)
3822 .no_color(false)
3823 .build();
3824 let html = console.export_html(|c| {
3825 c.print_str("[bold red]hi[/] there");
3826 c.print_str("plain line");
3827 });
3828 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
3832 assert_eq!(html, expected);
3833 }
3834
3835 #[test]
3836 fn export_html_classes_matches_upstream() {
3837 let console = Console::builder()
3838 .force_terminal(true)
3839 .color_system(Some(ColorSystem::Truecolor))
3840 .width(20)
3841 .no_color(false)
3842 .build();
3843 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
3844 let expected =
3847 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
3848 assert_eq!(html, expected);
3849 }
3850
3851 #[test]
3854 fn render_lines_pads_with_the_given_style() {
3855 let console = test_console();
3856 let options = console.options().update_width(5).update_height(2);
3857 let lines = console.render_lines(&Text::new("hi"), &options, true);
3858 assert_eq!(lines.len(), 2);
3859 let width_pad = lines[0].last().unwrap();
3860 assert_eq!((width_pad.text.as_str(), &width_pad.style), (" ", &None));
3861 assert_eq!(lines[1], vec![Segment::new(" ", None)]);
3862
3863 let red = Style::parse("red").unwrap();
3864 let lines = console.render_lines_styled(&Text::new("hi"), &options, Some(&red), true);
3865 assert_eq!(lines[0].last().unwrap().style, Some(red.clone()));
3866 assert_eq!(lines[1], vec![Segment::new(" ", Some(red))]);
3867 }
3868
3869 #[test]
3870 fn capture_matches_direct_render() {
3871 let console = test_console();
3872 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
3873 assert_eq!(
3874 console.capture(|c| c.print(&panel)),
3875 console.render_export(&panel)
3876 );
3877 }
3878
3879 #[test]
3880 fn no_color_strips_styles() {
3881 let console = Console::builder()
3882 .force_terminal(true)
3883 .color_system(None)
3884 .build();
3885 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
3886 }
3887
3888 #[test]
3889 fn used_theme_applies_through_the_guard_and_pops_on_drop() {
3890 let mut console = Console::builder()
3891 .force_terminal(true)
3892 .color_system(Some(ColorSystem::Truecolor))
3893 .width(20)
3894 .highlight(false)
3895 .build();
3896 let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
3897 {
3898 let themed = console.use_theme(theme);
3899 let out = themed.capture(|c| c.print_str("[accent]x[/]"));
3900 assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
3901 }
3902 assert!(console.theme().get("accent").is_none());
3903 assert!(console.pop_theme().is_err(), "the base theme must remain");
3904 }
3905
3906 #[test]
3907 fn used_theme_is_popped_during_a_panic_unwind() {
3908 let mut console = Console::builder().width(20).build();
3909 let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
3910 let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3911 let _themed = console.use_theme(theme);
3912 panic!("render failed");
3913 }));
3914 assert!(unwound.is_err());
3915 assert!(console.theme().get("accent").is_none());
3916 }
3917
3918 #[test]
3919 fn print_objects_keeps_a_renderable_s_open_last_line() {
3920 let console = Console::builder()
3923 .force_terminal(true)
3924 .color_system(Some(crate::color::ColorSystem::Truecolor))
3925 .width(4)
3926 .height(2)
3927 .build();
3928 let screen = crate::screen::Screen::new(Box::new(Text::new("hi")));
3929 let printed = console.capture(|c| c.print(&screen));
3930 let objects = console.capture(|c| {
3931 c.print_objects(
3932 &[PrintObject::Renderable(&screen)],
3933 &PrintOptions::default(),
3934 )
3935 .unwrap()
3936 });
3937 assert_eq!(objects, printed);
3938 assert!(!objects.ends_with('\n'), "{objects:?}");
3939 }
3940
3941 #[test]
3942 fn a_printed_text_defers_layout_to_the_print_options() {
3943 let console = Console::builder()
3946 .force_terminal(true)
3947 .color_system(Some(crate::color::ColorSystem::Truecolor))
3948 .width(6)
3949 .highlight(false)
3950 .build();
3951 let text = Text::new("abcdefghij").overflow(Overflow::Ellipsis);
3952 assert_eq!(console.render_export(&text), "abcdef\nghij\n");
3953 let mut options = console.options();
3954 options.overflow = Some(Overflow::Ellipsis);
3955 assert_eq!(
3956 console.render_export_with(&Text::new("abcdefghij"), &options),
3957 "abcde…\n"
3958 );
3959 let wide = Console::builder()
3960 .force_terminal(true)
3961 .color_system(Some(crate::color::ColorSystem::Truecolor))
3962 .width(20)
3963 .highlight(false)
3964 .build();
3965 let tabbed = Text::new("a\tb").justify(Justify::Right);
3966 assert_eq!(wide.render_export(&tabbed), "a b\n");
3967 }
3968}