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 {
867 "ascii"
868 } else {
869 "utf-8"
870 }
871 }
872
873 pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
880 let segments = self.render_segments(renderable);
881 self.segments_to_string(&segments)
882 }
883
884 fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
889 self.render_segments_with(renderable, &self.options())
890 }
891
892 pub fn render_segments_with(
900 &self,
901 renderable: &dyn Renderable,
902 options: &ConsoleOptions,
903 ) -> Vec<Segment> {
904 self.print_segments(renderable, options, false)
905 }
906
907 fn print_segments(
916 &self,
917 renderable: &dyn Renderable,
918 options: &ConsoleOptions,
919 end_line: bool,
920 ) -> Vec<Segment> {
921 let mut options = options.clone();
922 let crop = !self.soft_wrap;
923 if self.soft_wrap {
924 options.no_wrap = options.no_wrap.or(Some(true));
925 options.overflow = options.overflow.or(Some(Overflow::Ignore));
926 }
927 let ends_line = end_line && Self::print_ends_line(renderable, &options);
928 let joined;
929 let joined_text = renderable.printed_text();
930 let renderable = match joined_text.clone() {
931 Some(text) => {
932 if matches!(
936 options.justify,
937 Justify::Left | Justify::Center | Justify::Right
938 ) && text.measurement().1 == 0
939 {
940 return Vec::new();
941 }
942 joined = text;
943 &joined as &dyn Renderable
944 }
945 None => renderable,
946 };
947 let align = match options.justify {
950 Justify::Left if joined_text.is_none() => Some(crate::align::HorizontalAlign::Left),
951 Justify::Center if joined_text.is_none() => Some(crate::align::HorizontalAlign::Center),
952 Justify::Right if joined_text.is_none() => Some(crate::align::HorizontalAlign::Right),
953 _ => None,
954 };
955 if options.justify == Justify::Default && renderable.fit_to_measurement() {
956 let measurement = renderable.measure(self, &options);
957 options.max_width = measurement.maximum.min(options.max_width).max(1);
958 }
959 let mut segments = match align {
960 _ if options.max_width < 1 => Vec::new(),
962 Some(align) => crate::align::Align::render_child(renderable, align, self, &options),
963 None => renderable.rich_render(self, &options),
964 };
965 if !segments.is_empty() {
966 if ends_line {
967 segments.push(Segment::line());
968 }
969 if let Some(style) = self.console_style() {
970 segments = Segment::apply_style_with(&segments, Some(&style), None);
971 }
972 }
973 if end_line {
975 segments = self.process_render_hooks(vec![segments], &options).concat();
976 }
977 if segments.is_empty() || !crop {
978 return segments;
979 }
980 Segment::crop_lines(&segments, self.width())
986 }
987
988 fn console_style(&self) -> Option<Style> {
991 let style = self.style.as_ref()?;
992 self.get_style(style).ok().filter(|style| !style.is_null())
993 }
994
995 pub fn render(
1006 &self,
1007 renderable: &dyn Renderable,
1008 options: Option<&ConsoleOptions>,
1009 ) -> Vec<Segment> {
1010 let default_options;
1011 let options = match options {
1012 Some(options) => options,
1013 None => {
1014 default_options = self.options();
1015 &default_options
1016 }
1017 };
1018 if options.max_width < 1 {
1019 return Vec::new();
1020 }
1021 renderable.rich_render(self, options)
1022 }
1023
1024 fn emit(&self, segments: Vec<Segment>) {
1027 self.emit_end(segments, true);
1028 }
1029
1030 fn emit_end(&self, segments: Vec<Segment>, newline: bool) {
1034 if segments.is_empty() || self.quiet() {
1036 return;
1037 }
1038 let segments = {
1039 let mut captures = self.lock_captures();
1040 if let Some(buffer) = current_capture(&mut captures) {
1041 buffer.extend(segments);
1042 if newline {
1043 buffer.push(Segment::line());
1044 }
1045 return;
1046 }
1047 segments
1048 };
1049 if self.record() {
1051 let mut recorded = self.lock_record_buffer();
1052 recorded.extend(segments.iter().cloned());
1053 if newline {
1054 recorded.push(Segment::line());
1055 }
1056 }
1057 let mut output = self.segments_to_string(&segments);
1058 if newline {
1059 output.push('\n');
1060 }
1061 let stdout = std::io::stdout();
1062 let mut lock = stdout.lock();
1063 let mut written = lock.write_all(output.as_bytes());
1064 if written.is_ok() && !newline {
1065 written = lock.flush();
1066 }
1067 drop(lock);
1068 if written.is_err_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe) {
1071 self.on_broken_pipe();
1072 }
1073 }
1074
1075 pub fn input(&self, prompt: &str) -> std::io::Result<Option<String>> {
1080 let prompt = self.build_text(prompt);
1081 self.input_from(&prompt, &mut crate::prompt::StdinInput)
1082 }
1083
1084 pub fn input_from(
1087 &self,
1088 prompt: &dyn Renderable,
1089 stream: &mut dyn crate::prompt::InputSource,
1090 ) -> std::io::Result<Option<String>> {
1091 let segments = self.render_segments(prompt);
1093 if segments.iter().any(|segment| !segment.text.is_empty()) {
1094 self.emit_end(segments, false);
1095 }
1096 stream.read_line()
1097 }
1098
1099 pub fn render_lines(
1105 &self,
1106 renderable: &dyn Renderable,
1107 options: &ConsoleOptions,
1108 pad: bool,
1109 ) -> Vec<Vec<Segment>> {
1110 self.render_lines_styled(renderable, options, None, pad)
1111 }
1112
1113 pub fn render_lines_styled(
1117 &self,
1118 renderable: &dyn Renderable,
1119 options: &ConsoleOptions,
1120 style: Option<&Style>,
1121 pad: bool,
1122 ) -> Vec<Vec<Segment>> {
1123 let pad_style = style.cloned();
1126 let style = style.filter(|style| !style.is_null());
1127 let mut segments = if options.max_width < 1 {
1130 Vec::new()
1131 } else {
1132 renderable.rich_render(self, options)
1133 };
1134 if let Some(style) = style {
1135 segments = Segment::apply_style(&segments, style);
1136 }
1137 let only_empty = !segments.is_empty()
1141 && segments
1142 .iter()
1143 .all(|segment| !segment.control && segment.text.is_empty());
1144 let mut lines = Segment::split_lines_owned(segments);
1145 if lines.is_empty() && only_empty {
1146 lines.push(Vec::new());
1147 }
1148 if pad {
1149 for line in &mut lines {
1150 *line = Segment::adjust_line_length(line, options.max_width, pad_style.clone());
1151 }
1152 }
1153 if let Some(height) = options.height {
1157 lines.truncate(height);
1158 while lines.len() < height {
1159 lines.push(if pad {
1160 vec![Segment::new(
1161 " ".repeat(options.max_width),
1162 pad_style.clone(),
1163 )]
1164 } else {
1165 Vec::new()
1166 });
1167 }
1168 }
1169 lines
1170 }
1171
1172 pub fn render_export(&self, renderable: &dyn Renderable) -> String {
1176 self.render_export_with(renderable, &self.options())
1177 }
1178
1179 pub fn print(&self, renderable: &dyn Renderable) {
1183 self.print_with(renderable, &self.options());
1184 }
1185
1186 fn print_ends_line(renderable: &dyn Renderable, options: &ConsoleOptions) -> bool {
1189 renderable.print_end_line(options)
1190 || (renderable.printed_text().is_none()
1191 && matches!(
1192 options.justify,
1193 Justify::Left | Justify::Center | Justify::Right
1194 ))
1195 }
1196
1197 pub fn print_with(&self, renderable: &dyn Renderable, options: &ConsoleOptions) {
1203 let segments = self.print_segments(renderable, options, true);
1204 self.emit_end(segments, false);
1205 }
1206
1207 pub fn render_export_with(
1210 &self,
1211 renderable: &dyn Renderable,
1212 options: &ConsoleOptions,
1213 ) -> String {
1214 let segments = self.print_segments(renderable, options, true);
1215 self.segments_to_string(&segments)
1216 }
1217
1218 pub fn control(&self, control: &crate::control::Control) {
1223 let text = control.as_str();
1227 if !text.is_empty() {
1228 self.emit_end(vec![Segment::control(text)], false);
1229 }
1230 }
1231
1232 pub fn push_render_hook(&self, hook: std::sync::Arc<dyn RenderHook>) {
1234 lock_or_recover(&self.render_hooks).push(hook);
1235 }
1236
1237 pub fn pop_render_hook(&self) {
1241 lock_or_recover(&self.render_hooks).pop();
1242 }
1243
1244 fn process_render_hooks(
1248 &self,
1249 items: Vec<Vec<Segment>>,
1250 options: &ConsoleOptions,
1251 ) -> Vec<Vec<Segment>> {
1252 let hooks = lock_or_recover(&self.render_hooks).clone();
1253 if hooks.is_empty() {
1254 return items;
1255 }
1256 let mut renderables: Vec<Box<dyn Renderable>> = items
1257 .into_iter()
1258 .map(|segments| Box::new(Rendered(segments)) as Box<dyn Renderable>)
1259 .collect();
1260 for hook in &hooks {
1261 renderables = hook.process_renderables(renderables);
1262 }
1263 renderables
1264 .iter()
1265 .map(|renderable| {
1266 let mut segments = self.render(renderable.as_ref(), Some(options));
1267 if !segments.is_empty() && renderable.print_end_line(options) {
1268 segments.push(Segment::line());
1269 }
1270 segments
1271 })
1272 .collect()
1273 }
1274
1275 pub fn set_live(&self, live: std::sync::Arc<dyn std::any::Any + Send + Sync>) -> bool {
1286 let mut stack = lock_or_recover(&self.live_stack);
1287 stack.push(live);
1288 stack.len() == 1
1289 }
1290
1291 pub fn clear_live(&self) {
1295 lock_or_recover(&self.live_stack).pop();
1296 }
1297
1298 pub(crate) fn live_stack(&self) -> Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
1301 lock_or_recover(&self.live_stack).clone()
1302 }
1303
1304 pub fn screen(
1311 &self,
1312 hide_cursor: bool,
1313 style: Option<crate::style::StyleType>,
1314 ) -> ScreenContext<'_> {
1315 let changed = self.set_alt_screen(true);
1316 if changed && hide_cursor {
1317 self.show_cursor(false);
1318 }
1319 let style = style.unwrap_or_else(|| "".into());
1321 ScreenContext {
1322 console: self,
1323 hide_cursor,
1324 changed,
1325 screen: Some(crate::screen::Screen::empty().style(style.clone())),
1326 style,
1327 }
1328 }
1329
1330 pub fn status(
1338 &self,
1339 status: crate::status::Status,
1340 ) -> crate::status::LiveStatus<std::io::Stdout> {
1341 self.status_to(status, std::io::stdout())
1342 }
1343
1344 pub fn status_to<W: Write + Send + 'static>(
1347 &self,
1348 status: crate::status::Status,
1349 writer: W,
1350 ) -> crate::status::LiveStatus<W> {
1351 status.start(self.clone(), writer)
1352 }
1353
1354 pub fn is_alt_screen(&self) -> bool {
1357 self.is_alt_screen.load(std::sync::atomic::Ordering::SeqCst)
1358 }
1359
1360 pub fn set_alt_screen(&self, enable: bool) -> bool {
1364 if self.is_terminal && !self.legacy_windows {
1365 self.control(&crate::control::Control::alt_screen(enable));
1366 self.is_alt_screen
1367 .store(enable, std::sync::atomic::Ordering::SeqCst);
1368 return true;
1369 }
1370 false
1371 }
1372
1373 pub fn update_screen(
1376 &self,
1377 renderable: &dyn Renderable,
1378 region: Option<crate::region::Region>,
1379 options: Option<&ConsoleOptions>,
1380 ) -> crate::errors::Result<()> {
1381 if !self.is_alt_screen() {
1382 return Err(no_alt_screen());
1383 }
1384 let render_options = options.cloned().unwrap_or_else(|| self.options());
1385 let (x, y, render_options) = match region {
1386 None => {
1387 let height = render_options
1388 .height
1389 .filter(|&height| height > 0)
1390 .unwrap_or(self.height());
1391 let width = render_options.max_width;
1392 (0, 0, render_options.update_dimensions(width, height))
1393 }
1394 Some(region) => (
1395 region.x,
1396 region.y,
1397 render_options.update_dimensions(region.width, region.height),
1398 ),
1399 };
1400 let lines = self.render_lines(renderable, &render_options, true);
1401 self.update_screen_lines(&lines, x, y)
1402 }
1403
1404 pub fn update_screen_lines(
1407 &self,
1408 lines: &[Vec<Segment>],
1409 x: usize,
1410 y: usize,
1411 ) -> crate::errors::Result<()> {
1412 if !self.is_alt_screen() {
1413 return Err(no_alt_screen());
1414 }
1415 let update = ScreenUpdate::new(lines.to_vec(), x, y);
1416 let segments = self.render(&update, None);
1417 self.emit_end(segments, false);
1418 Ok(())
1419 }
1420
1421 pub fn show_cursor(&self, show: bool) {
1423 self.control(&crate::control::Control::show_cursor(show));
1424 }
1425
1426 pub fn clear(&self) {
1428 self.control(&crate::control::Control::clear());
1429 }
1430
1431 pub fn bell(&self) {
1433 self.control(&crate::control::Control::bell());
1434 }
1435
1436 pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
1449 let segments = self.capture_segments(f);
1450 self.segments_to_string(&segments)
1451 }
1452
1453 pub fn begin_capture(&self) {
1457 self.lock_captures()
1458 .entry(std::thread::current().id())
1459 .or_default()
1460 .push(Vec::new());
1461 }
1462
1463 pub fn end_capture(&self) -> String {
1472 let id = std::thread::current().id();
1473 let segments: Vec<Segment> = {
1474 let mut captures = self.lock_captures();
1475 let Some(stack) = captures.get_mut(&id) else {
1476 return String::new();
1477 };
1478 let segments = stack.iter_mut().flat_map(std::mem::take).collect();
1479 stack.pop();
1480 if stack.is_empty() {
1481 captures.remove(&id);
1482 }
1483 segments
1484 };
1485 self.segments_to_string(&segments)
1486 }
1487
1488 pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
1491 let segments = self.capture_segments(f);
1492 segments_to_plain(&segments)
1493 }
1494
1495 pub fn export_text_styled(&self, f: impl FnOnce(&Console)) -> String {
1500 let segments = self.capture_segments(f);
1501 self.segments_to_styled_text(&segments)
1502 }
1503
1504 pub fn save_text(
1507 &self,
1508 path: impl AsRef<std::path::Path>,
1509 styles: bool,
1510 f: impl FnOnce(&Console),
1511 ) -> std::io::Result<()> {
1512 let text = if styles {
1513 self.export_text_styled(f)
1514 } else {
1515 self.export_text(f)
1516 };
1517 std::fs::write(path, text)
1518 }
1519
1520 pub fn save_html(
1523 &self,
1524 path: impl AsRef<std::path::Path>,
1525 f: impl FnOnce(&Console),
1526 ) -> std::io::Result<()> {
1527 std::fs::write(path, self.export_html_classes(f))
1528 }
1529
1530 pub fn save_svg(
1533 &self,
1534 path: impl AsRef<std::path::Path>,
1535 title: &str,
1536 unique_id: &str,
1537 f: impl FnOnce(&Console),
1538 ) -> std::io::Result<()> {
1539 std::fs::write(path, self.export_svg(title, unique_id, f))
1540 }
1541
1542 pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
1550 self.page_with(&crate::pager::SystemPager, styles, f)
1551 }
1552
1553 pub fn page_with(
1557 &self,
1558 pager: &dyn crate::pager::Pager,
1559 styles: bool,
1560 f: impl FnOnce(&Console),
1561 ) -> std::io::Result<()> {
1562 self.page_with_links(pager, styles, false, f)
1563 }
1564
1565 pub fn page_with_links(
1570 &self,
1571 pager: &dyn crate::pager::Pager,
1572 styles: bool,
1573 links: bool,
1574 f: impl FnOnce(&Console),
1575 ) -> std::io::Result<()> {
1576 let mut segments = self.capture_segments(f);
1577 let content = if !styles {
1578 segments_to_plain(&segments)
1579 } else {
1580 if !links {
1581 for segment in &mut segments {
1582 if let Some(style) = segment.style.as_mut() {
1583 if style.link().is_some() {
1584 *style = style.update_link(None);
1585 }
1586 }
1587 }
1588 }
1589 self.segments_to_string(&segments)
1590 };
1591 pager.show(&content)
1592 }
1593
1594 pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
1598 self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1599 }
1600
1601 pub fn export_html_themed(
1607 &self,
1608 theme: &crate::terminal_theme::TerminalTheme,
1609 f: impl FnOnce(&Console),
1610 ) -> String {
1611 let segments = self.capture_segments(f);
1612 crate::export::export_html_inline(&segments, theme)
1613 }
1614
1615 pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
1619 self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1620 }
1621
1622 pub fn export_html_classes_themed(
1625 &self,
1626 theme: &crate::terminal_theme::TerminalTheme,
1627 f: impl FnOnce(&Console),
1628 ) -> String {
1629 let segments = self.capture_segments(f);
1630 crate::export::export_html_classes(&segments, theme)
1631 }
1632
1633 pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
1644 self.export_svg_themed(
1645 &crate::terminal_theme::SVG_EXPORT_THEME,
1646 title,
1647 unique_id,
1648 f,
1649 )
1650 }
1651
1652 pub fn export_svg_themed(
1655 &self,
1656 theme: &crate::terminal_theme::TerminalTheme,
1657 title: &str,
1658 unique_id: &str,
1659 f: impl FnOnce(&Console),
1660 ) -> String {
1661 let segments = self.capture_segments(f);
1662 crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
1663 }
1664
1665 pub fn export_html_with(
1670 &self,
1671 theme: &crate::terminal_theme::TerminalTheme,
1672 code_format: Option<&str>,
1673 inline_styles: bool,
1674 f: impl FnOnce(&Console),
1675 ) -> Result<String, crate::export::ExportFormatError> {
1676 let segments = self.capture_segments(f);
1677 crate::export::export_html_with(&segments, theme, code_format, inline_styles)
1678 }
1679
1680 #[allow(clippy::too_many_arguments)]
1686 pub fn export_svg_with(
1687 &self,
1688 theme: &crate::terminal_theme::TerminalTheme,
1689 title: &str,
1690 unique_id: &str,
1691 code_format: Option<&str>,
1692 font_aspect_ratio: f64,
1693 f: impl FnOnce(&Console),
1694 ) -> Result<String, crate::export::ExportFormatError> {
1695 let segments = self.capture_segments(f);
1696 crate::svg::export_svg_with(
1697 &segments,
1698 theme,
1699 title,
1700 unique_id,
1701 self.width(),
1702 code_format.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1703 font_aspect_ratio,
1704 )
1705 }
1706
1707 pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1724 self.capture_segments(f)
1725 }
1726
1727 fn capture_segments(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1732 struct CaptureGuard<'a>(&'a Console);
1735 impl CaptureGuard<'_> {
1736 fn pop(&self) -> Vec<Segment> {
1737 let id = std::thread::current().id();
1738 let mut captures = self.0.lock_captures();
1739 let Some(stack) = captures.get_mut(&id) else {
1740 return Vec::new();
1741 };
1742 let captured = stack.pop().unwrap_or_default();
1743 if stack.is_empty() {
1744 captures.remove(&id);
1745 }
1746 captured
1747 }
1748 }
1749 impl Drop for CaptureGuard<'_> {
1750 fn drop(&mut self) {
1751 self.pop();
1752 }
1753 }
1754 self.lock_captures()
1755 .entry(std::thread::current().id())
1756 .or_default()
1757 .push(Vec::new());
1758 let guard = CaptureGuard(self);
1759 f(self);
1760 let captured = guard.pop();
1761 std::mem::forget(guard);
1762 captured
1763 }
1764
1765 pub fn record(&self) -> bool {
1767 self.record.load(std::sync::atomic::Ordering::SeqCst)
1768 }
1769
1770 pub fn set_record(&self, record: bool) {
1773 self.record
1774 .store(record, std::sync::atomic::Ordering::SeqCst);
1775 }
1776
1777 fn take_recorded(&self, clear: bool) -> crate::errors::Result<Vec<Segment>> {
1780 if !self.record() {
1781 return Err(crate::errors::RichError::Console(
1782 "To export console contents set record=True in the constructor or instance"
1783 .to_string(),
1784 ));
1785 }
1786 let mut recorded = self.lock_record_buffer();
1787 Ok(if clear {
1788 std::mem::take(&mut *recorded)
1789 } else {
1790 recorded.clone()
1791 })
1792 }
1793
1794 pub fn export_text_recorded(&self, clear: bool, styles: bool) -> crate::errors::Result<String> {
1809 let segments = self.take_recorded(clear)?;
1810 Ok(if styles {
1811 self.segments_to_styled_text(&segments)
1812 } else {
1813 segments_to_plain(&segments)
1814 })
1815 }
1816
1817 pub fn save_text_recorded(
1820 &self,
1821 path: impl AsRef<std::path::Path>,
1822 clear: bool,
1823 styles: bool,
1824 ) -> std::io::Result<()> {
1825 let text = self
1826 .export_text_recorded(clear, styles)
1827 .map_err(std::io::Error::other)?;
1828 std::fs::write(path, text)
1829 }
1830
1831 pub fn export_html_recorded(
1842 &self,
1843 options: &ExportHtmlOptions<'_>,
1844 ) -> crate::errors::Result<String> {
1845 let segments = self.take_recorded(options.clear)?;
1846 let theme = options
1847 .theme
1848 .unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
1849 crate::export::export_html_with(
1850 &segments,
1851 theme,
1852 options.code_format,
1853 options.inline_styles,
1854 )
1855 .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1856 }
1857
1858 pub fn save_html_recorded(
1861 &self,
1862 path: impl AsRef<std::path::Path>,
1863 options: &ExportHtmlOptions<'_>,
1864 ) -> std::io::Result<()> {
1865 let html = self
1866 .export_html_recorded(options)
1867 .map_err(std::io::Error::other)?;
1868 std::fs::write(path, html)
1869 }
1870
1871 pub fn export_svg_recorded(
1881 &self,
1882 options: &ExportSvgOptions<'_>,
1883 ) -> crate::errors::Result<String> {
1884 let segments: Vec<Segment> = self
1885 .take_recorded(options.clear)?
1886 .into_iter()
1887 .filter(|segment| !segment.control)
1888 .collect();
1889 let theme = options
1890 .theme
1891 .unwrap_or(&crate::terminal_theme::SVG_EXPORT_THEME);
1892 crate::svg::export_svg_with(
1893 &segments,
1894 theme,
1895 options.title,
1896 options.unique_id,
1897 self.width(),
1898 options
1899 .code_format
1900 .unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1901 options.font_aspect_ratio,
1902 )
1903 .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1904 }
1905
1906 pub fn save_svg_recorded(
1909 &self,
1910 path: impl AsRef<std::path::Path>,
1911 options: &ExportSvgOptions<'_>,
1912 ) -> std::io::Result<()> {
1913 let svg = self
1914 .export_svg_recorded(options)
1915 .map_err(std::io::Error::other)?;
1916 std::fs::write(path, svg)
1917 }
1918
1919 fn lock_record_buffer(&self) -> std::sync::MutexGuard<'_, Vec<Segment>> {
1922 self.record_buffer
1923 .lock()
1924 .unwrap_or_else(std::sync::PoisonError::into_inner)
1925 }
1926
1927 fn segments_to_styled_text(&self, segments: &[Segment]) -> String {
1930 let system = self.color_system().unwrap_or(ColorSystem::Truecolor);
1931 segments
1932 .iter()
1933 .map(|segment| match &segment.style {
1934 Some(style) => style.render(&segment.text, Some(system)),
1935 None => segment.text.to_string(),
1936 })
1937 .collect()
1938 }
1939
1940 fn lock_captures(&self) -> std::sync::MutexGuard<'_, CaptureStacks> {
1943 self.captures
1944 .lock()
1945 .unwrap_or_else(std::sync::PoisonError::into_inner)
1946 }
1947
1948 pub fn print_str(&self, content: &str) {
1951 let text = self.build_text(content);
1952 self.print(&text);
1953 }
1954
1955 pub fn render_str_to_string(&self, content: &str) -> String {
1957 let text = self.build_text(content);
1958 self.render_to_string(&text)
1959 }
1960
1961 pub fn build_text(&self, content: &str) -> Text {
1965 self.try_build_text(content)
1970 .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
1971 }
1972
1973 pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
1977 if !self.markup {
1978 return Ok(self.decorate(Text::new(self.expand_emoji_plain(content))));
1980 }
1981 let markup = self.parse_markup(content, self.emoji)?;
1982
1983 let mut text = self.decorate(Text::new(markup.plain()));
1994 for span in markup.spans() {
1995 text.push_span(span.clone());
1996 }
1997 Ok(text)
1998 }
1999
2000 pub fn render_str(&self, content: &str, highlight: Option<bool>) -> Text {
2009 let highlight = highlight.unwrap_or(self.highlight);
2010 let markup = if !self.markup {
2013 Text::new(self.expand_emoji_plain(content))
2014 } else if content.contains('[') {
2015 self.parse_markup(content, self.emoji)
2016 .unwrap_or_else(|_| Text::new(self.expand_emoji(content)))
2017 } else if content.contains(':') {
2018 Text::new(self.expand_emoji(content))
2019 } else {
2020 Text::new(content)
2021 };
2022 if !highlight {
2023 return markup;
2024 }
2025 let mut text = self.decorate_with_repr(Text::new(markup.plain()));
2028 for span in markup.spans() {
2029 text.push_span(span.clone());
2030 }
2031 text
2032 }
2033
2034 pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
2036 self.print(&self.try_build_text(content)?);
2037 Ok(())
2038 }
2039
2040 pub fn try_print_justified(
2043 &self,
2044 content: &str,
2045 justify: Justify,
2046 ) -> crate::errors::Result<()> {
2047 let text = self.try_build_text(content)?;
2048 let mut options = self.options();
2049 options.justify = justify;
2050 self.emit(text.rich_render(self, &options));
2051 Ok(())
2052 }
2053
2054 pub(crate) fn expand_emoji(&self, content: &str) -> String {
2061 if !self.emoji {
2062 return content.to_string();
2063 }
2064 let variant = if content.contains('[') {
2065 None
2066 } else {
2067 self.emoji_variant
2068 };
2069 crate::emoji::replace_with_variant(content, variant)
2070 }
2071
2072 fn parse_markup(&self, content: &str, emoji: bool) -> crate::errors::Result<Text> {
2077 if !content.contains('[') {
2078 return Ok(Text::new(if emoji {
2079 crate::emoji::replace_with_variant(content, self.emoji_variant)
2080 } else {
2081 content.to_string()
2082 }));
2083 }
2084 if emoji {
2085 crate::markup::render_emoji(content)
2086 } else {
2087 crate::markup::render(content)
2088 }
2089 }
2090
2091 fn expand_emoji_plain(&self, content: &str) -> String {
2095 if self.emoji {
2096 crate::emoji::replace_with_variant(content, self.emoji_variant)
2097 } else {
2098 content.to_string()
2099 }
2100 }
2101
2102 pub fn render_str_with(
2111 &self,
2112 content: &str,
2113 options: &RenderStrOptions<'_>,
2114 ) -> crate::errors::Result<Text> {
2115 let emoji = options.emoji.unwrap_or(self.emoji);
2116 let markup = options.markup.unwrap_or(self.markup);
2117 let highlight = options.highlight.unwrap_or(self.highlight);
2118
2119 let mut rich_text = if markup {
2120 self.parse_markup(content, emoji)?
2121 } else if emoji {
2122 Text::new(crate::emoji::replace_with_variant(
2123 content,
2124 self.emoji_variant,
2125 ))
2126 } else {
2127 Text::new(content)
2128 };
2129 rich_text.set_base_style(options.style.clone());
2130 rich_text.set_justify(options.justify.unwrap_or_default());
2131 rich_text.set_overflow(options.overflow);
2132
2133 if !highlight {
2134 return Ok(rich_text);
2135 }
2136 let mut text = Text::new(rich_text.plain());
2137 match options.highlighter {
2138 Some(highlighter) => highlighter.highlight(&mut text),
2139 None => text = self.decorate_with_repr(text),
2140 }
2141 for span in rich_text.spans() {
2142 text.push_span(span.clone());
2143 }
2144 Ok(text)
2145 }
2146
2147 pub fn emoji(&self) -> bool {
2150 self.emoji
2151 }
2152
2153 pub fn highlight(&self) -> bool {
2156 self.highlight
2157 }
2158
2159 pub fn markup(&self) -> bool {
2162 self.markup
2163 }
2164
2165 pub fn emoji_variant(&self) -> Option<crate::emoji::EmojiVariant> {
2167 self.emoji_variant
2168 }
2169
2170 pub fn tab_size(&self) -> usize {
2172 self.tab_size
2173 }
2174
2175 pub fn set_tab_size(&mut self, tab_size: usize) {
2178 self.tab_size = tab_size;
2179 }
2180
2181 pub fn set_window_title(&self, title: &str) -> bool {
2185 if self.is_terminal {
2186 self.control(&crate::control::Control::title(title));
2187 true
2188 } else {
2189 false
2190 }
2191 }
2192
2193 fn decorate(&self, mut text: Text) -> Text {
2196 for highlighter in &self.highlighters {
2197 highlighter.highlight(&mut text);
2198 }
2199 if self.highlight {
2200 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2201 }
2202 text
2203 }
2204
2205 fn decorate_with_repr(&self, mut text: Text) -> Text {
2208 for highlighter in &self.highlighters {
2209 highlighter.highlight(&mut text);
2210 }
2211 crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2212 text
2213 }
2214
2215 pub fn print_justified(&self, content: &str, justify: Justify) {
2218 let text = self.build_text(content);
2219 let mut options = self.options();
2220 options.justify = justify;
2221 let segments = text.rich_render(self, &options);
2222 self.emit(segments);
2223 }
2224
2225 pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
2230 let text = self.build_text(content);
2231 let mut options = self.options();
2232 options.justify = justify;
2233 let segments = text.rich_render(self, &options);
2234 self.segments_to_string(&segments)
2235 }
2236
2237 pub fn segments_to_string(&self, segments: &[Segment]) -> String {
2240 let system = self.color_system;
2241 let colorless;
2244 let segments = if self.no_color && system.is_some() {
2245 colorless = Segment::remove_color(segments);
2246 &colorless[..]
2247 } else {
2248 segments
2249 };
2250 let mut out = String::new();
2251 for segment in segments {
2252 if segment.control && !self.is_terminal {
2255 continue;
2256 }
2257 match (&segment.style, system) {
2258 (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
2259 _ => out.push_str(&segment.text),
2260 }
2261 }
2262 out
2263 }
2264}
2265
2266fn segments_to_plain(segments: &[Segment]) -> String {
2269 segments
2270 .iter()
2271 .filter(|s| !s.control)
2272 .map(|s| s.text.as_str())
2273 .collect()
2274}
2275
2276impl Renderable for String {
2281 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2282 self.options_text(console, options, options.highlight)
2283 .rich_render(console, options)
2284 }
2285
2286 fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2289 let text = self.options_text(console, options, Some(false));
2290 crate::measure::Measurement::get(console, options, &text)
2291 }
2292}
2293
2294trait OptionsText {
2296 fn options_text(
2297 &self,
2298 console: &Console,
2299 options: &ConsoleOptions,
2300 highlight: Option<bool>,
2301 ) -> Text;
2302}
2303
2304impl OptionsText for String {
2305 fn options_text(
2306 &self,
2307 console: &Console,
2308 options: &ConsoleOptions,
2309 highlight: Option<bool>,
2310 ) -> Text {
2311 let render_options = RenderStrOptions {
2312 highlight,
2313 markup: options.markup,
2314 ..Default::default()
2315 };
2316 console
2317 .render_str_with(self, &render_options)
2318 .unwrap_or_else(|_| {
2319 console
2320 .render_str_with(
2321 self,
2322 &RenderStrOptions {
2323 markup: Some(false),
2324 ..render_options
2325 },
2326 )
2327 .unwrap_or_else(|_| Text::new(self.as_str()))
2328 })
2329 }
2330}
2331
2332impl Renderable for Text {
2333 fn printed_text(&self) -> Option<Text> {
2334 let mut text = self.clone();
2340 text.clear_layout_options();
2341 text.base_style_to_span();
2342 Some(text)
2343 }
2344
2345 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2346 let justify = self.get_justify_option().unwrap_or(options.justify);
2351 if self.is_empty() && matches!(justify, Justify::Default | Justify::Full) {
2354 return vec![Segment::new("", None)];
2355 }
2356 let overflow = self
2360 .get_overflow()
2361 .or(options.overflow)
2362 .unwrap_or(Overflow::Fold);
2363 let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
2364 self.render_joined_wrapped_tabs(
2366 console.theme(),
2367 console.base_style(),
2368 options.max_width,
2369 justify,
2370 overflow,
2371 no_wrap,
2372 self.console_tab_size(console),
2373 )
2374 }
2375
2376 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2377 let (minimum, maximum) = self.measurement();
2378 crate::measure::Measurement::new(
2379 minimum.min(options.max_width),
2380 maximum.min(options.max_width),
2381 )
2382 }
2383}
2384
2385#[derive(Clone, Copy)]
2388pub struct ExportHtmlOptions<'a> {
2389 pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2392 pub clear: bool,
2394 pub code_format: Option<&'a str>,
2397 pub inline_styles: bool,
2400}
2401
2402impl Default for ExportHtmlOptions<'_> {
2403 fn default() -> Self {
2404 ExportHtmlOptions {
2405 theme: None,
2406 clear: true,
2407 code_format: None,
2408 inline_styles: false,
2409 }
2410 }
2411}
2412
2413#[derive(Clone, Copy)]
2416pub struct ExportSvgOptions<'a> {
2417 pub title: &'a str,
2419 pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2422 pub clear: bool,
2424 pub code_format: Option<&'a str>,
2427 pub font_aspect_ratio: f64,
2429 pub unique_id: &'a str,
2435}
2436
2437impl Default for ExportSvgOptions<'_> {
2438 fn default() -> Self {
2439 ExportSvgOptions {
2440 title: "Rich",
2441 theme: None,
2442 clear: true,
2443 code_format: None,
2444 font_aspect_ratio: 0.61,
2445 unique_id: "terminal",
2446 }
2447 }
2448}
2449
2450pub struct ConsoleBuilder {
2452 force_terminal: Option<bool>,
2453 color_system: Option<ColorSystem>,
2454 color_system_set: bool,
2455 width: Option<usize>,
2456 height: Option<usize>,
2457 no_color: Option<bool>,
2458 get_time: Option<GetTime>,
2459 emoji: Option<bool>,
2460 highlight: Option<bool>,
2461 legacy_windows: Option<bool>,
2462 safe_box: Option<bool>,
2463 ascii_only: Option<bool>,
2464 theme: Option<Theme>,
2465 markup: Option<bool>,
2466 emoji_variant: Option<crate::emoji::EmojiVariant>,
2467 tab_size: Option<usize>,
2468 environ: Option<Environ>,
2469 quiet: bool,
2470 soft_wrap: bool,
2471 style: Option<crate::style::StyleType>,
2472 on_broken_pipe: Option<BrokenPipeHandler>,
2473 record: bool,
2474 log_time: bool,
2475 log_path: bool,
2476 log_time_format: Option<LogTimeFormat>,
2477 get_datetime: Option<GetDateTime>,
2478}
2479
2480impl ConsoleBuilder {
2481 fn new() -> Self {
2482 ConsoleBuilder {
2483 force_terminal: None,
2484 color_system: None,
2485 color_system_set: false,
2486 width: None,
2487 height: None,
2488 no_color: None,
2489 get_time: None,
2490 emoji: None,
2491 highlight: None,
2492 legacy_windows: None,
2493 safe_box: None,
2494 ascii_only: None,
2495 theme: None,
2496 markup: None,
2497 emoji_variant: None,
2498 tab_size: None,
2499 environ: None,
2500 quiet: false,
2501 soft_wrap: false,
2502 style: None,
2503 on_broken_pipe: None,
2504 record: false,
2505 log_time: true,
2506 log_path: true,
2507 log_time_format: None,
2508 get_datetime: None,
2509 }
2510 }
2511
2512 pub fn log_time(mut self, value: bool) -> Self {
2515 self.log_time = value;
2516 self
2517 }
2518
2519 pub fn log_path(mut self, value: bool) -> Self {
2522 self.log_path = value;
2523 self
2524 }
2525
2526 pub fn log_time_format(mut self, format: impl Into<String>) -> Self {
2530 self.log_time_format = Some(LogTimeFormat::Strftime(format.into()));
2531 self
2532 }
2533
2534 pub fn log_time_formatter(
2537 mut self,
2538 format: impl Fn(std::time::SystemTime) -> Text + Send + Sync + 'static,
2539 ) -> Self {
2540 self.log_time_format = Some(LogTimeFormat::Callable(std::sync::Arc::new(format)));
2541 self
2542 }
2543
2544 pub fn get_datetime(
2550 mut self,
2551 clock: impl Fn() -> std::time::SystemTime + Send + Sync + 'static,
2552 ) -> Self {
2553 self.get_datetime = Some(std::sync::Arc::new(clock));
2554 self
2555 }
2556
2557 pub fn record(mut self, value: bool) -> Self {
2561 self.record = value;
2562 self
2563 }
2564
2565 pub fn environ(mut self, environ: Environ) -> Self {
2570 self.environ = Some(environ);
2571 self
2572 }
2573
2574 pub fn quiet(mut self, value: bool) -> Self {
2576 self.quiet = value;
2577 self
2578 }
2579
2580 pub fn soft_wrap(mut self, value: bool) -> Self {
2583 self.soft_wrap = value;
2584 self
2585 }
2586
2587 pub fn style(mut self, style: impl Into<crate::style::StyleType>) -> Self {
2590 self.style = Some(style.into());
2591 self
2592 }
2593
2594 pub fn on_broken_pipe(mut self, handler: impl Fn(&Console) + Send + Sync + 'static) -> Self {
2598 self.on_broken_pipe = Some(std::sync::Arc::new(handler));
2599 self
2600 }
2601
2602 pub fn markup(mut self, value: bool) -> Self {
2605 self.markup = Some(value);
2606 self
2607 }
2608
2609 pub fn emoji_variant(mut self, variant: Option<crate::emoji::EmojiVariant>) -> Self {
2612 self.emoji_variant = variant;
2613 self
2614 }
2615
2616 pub fn tab_size(mut self, tab_size: usize) -> Self {
2619 self.tab_size = Some(tab_size);
2620 self
2621 }
2622
2623 pub fn force_terminal(mut self, value: bool) -> Self {
2624 self.force_terminal = Some(value);
2625 self
2626 }
2627
2628 pub fn legacy_windows(mut self, value: bool) -> Self {
2630 self.legacy_windows = Some(value);
2631 self
2632 }
2633
2634 pub fn safe_box(mut self, value: bool) -> Self {
2636 self.safe_box = Some(value);
2637 self
2638 }
2639
2640 pub fn ascii_only(mut self, value: bool) -> Self {
2642 self.ascii_only = Some(value);
2643 self
2644 }
2645
2646 pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
2648 self.color_system = system;
2649 self.color_system_set = true;
2650 self
2651 }
2652
2653 pub fn width(mut self, width: usize) -> Self {
2654 self.width = Some(width);
2655 self
2656 }
2657
2658 pub fn height(mut self, height: usize) -> Self {
2660 self.height = Some(height);
2661 self
2662 }
2663
2664 pub fn no_color(mut self, value: bool) -> Self {
2668 self.no_color = Some(value);
2669 self
2670 }
2671
2672 pub fn get_time(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
2676 self.get_time = Some(std::sync::Arc::new(clock));
2677 self
2678 }
2679
2680 pub fn emoji(mut self, value: bool) -> Self {
2682 self.emoji = Some(value);
2683 self
2684 }
2685
2686 pub fn highlight(mut self, value: bool) -> Self {
2689 self.highlight = Some(value);
2690 self
2691 }
2692
2693 pub fn theme(mut self, theme: Theme) -> Self {
2694 self.theme = Some(theme);
2695 self
2696 }
2697
2698 pub fn build(self) -> Console {
2699 let environ = self.environ.map(std::sync::Arc::new);
2700 let env = environ.as_deref();
2701 let legacy_windows = self.legacy_windows.unwrap_or(false);
2702 let is_terminal = self
2703 .force_terminal
2704 .unwrap_or_else(|| detect_is_terminal(env));
2705 let no_color = self
2710 .no_color
2711 .unwrap_or_else(|| environ_get(env, "NO_COLOR").is_some_and(|value| !value.is_empty()));
2712 let color_system = if self.color_system_set {
2715 self.color_system
2716 } else if is_terminal && !term_is_dumb(env) {
2717 Some(detect_color_system(env))
2718 } else {
2719 None
2720 };
2721 let width = self.width.or_else(|| {
2724 env_int(env, "COLUMNS")
2725 .map(|columns| columns.saturating_sub(usize::from(legacy_windows)))
2726 });
2727 let height = self.height.or_else(|| env_int(env, "LINES"));
2728 let terminal_size = detect_terminal_size(legacy_windows);
2729 Console {
2730 render_environment: None,
2731 region_sink: None,
2732 code_highlighting: None,
2733 color_system,
2734 width,
2735 height,
2736 terminal_size,
2737 environ,
2738 is_terminal,
2739 no_color,
2740 quiet: std::sync::atomic::AtomicBool::new(self.quiet),
2741 soft_wrap: self.soft_wrap,
2742 style: self.style,
2743 on_broken_pipe: self.on_broken_pipe,
2744 get_time: self
2745 .get_time
2746 .unwrap_or_else(|| std::sync::Arc::new(monotonic)),
2747 emoji: self.emoji.unwrap_or(true),
2748 highlight: self.highlight.unwrap_or(true),
2753 legacy_windows,
2754 safe_box: self.safe_box.unwrap_or(true),
2755 ascii_only: self.ascii_only.unwrap_or(false),
2756 theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
2757 base_style: Style::new(),
2758 highlighters: Vec::new(),
2759 markup: self.markup.unwrap_or(true),
2760 emoji_variant: self.emoji_variant,
2761 tab_size: self.tab_size.unwrap_or(crate::text::DEFAULT_TAB_SIZE),
2763 captures: std::sync::Mutex::new(CaptureStacks::new()),
2764 is_alt_screen: std::sync::atomic::AtomicBool::new(false),
2765 record: std::sync::atomic::AtomicBool::new(self.record),
2766 record_buffer: std::sync::Mutex::new(Vec::new()),
2767 render_hooks: std::sync::Mutex::new(Vec::new()),
2768 live_stack: LiveStack::default(),
2769 log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
2770 log_time: self.log_time,
2771 log_path: self.log_path,
2772 log_time_format: self
2773 .log_time_format
2774 .unwrap_or_else(|| LogTimeFormat::Strftime("[%X]".to_string())),
2775 get_datetime: self
2776 .get_datetime
2777 .unwrap_or_else(|| std::sync::Arc::new(std::time::SystemTime::now)),
2778 }
2779 }
2780}
2781
2782fn absolute_path(path: &str) -> String {
2785 let path = std::path::Path::new(path);
2786 if path.is_absolute() {
2787 return path.to_string_lossy().into_owned();
2788 }
2789 std::env::current_dir()
2790 .map(|directory| directory.join(path).to_string_lossy().into_owned())
2791 .unwrap_or_else(|_| path.to_string_lossy().into_owned())
2792}
2793
2794fn new_log_render(log_time: bool, log_path: bool) -> crate::log_render::LogRender {
2796 crate::log_render::LogRender::new()
2797 .show_time(log_time)
2798 .show_path(log_path)
2799}
2800
2801fn term_is_dumb(environ: Option<&Environ>) -> bool {
2804 environ_get(environ, "TERM")
2805 .is_some_and(|term| matches!(term.to_lowercase().as_str(), "dumb" | "unknown"))
2806}
2807
2808fn detect_is_terminal(environ: Option<&Environ>) -> bool {
2812 match environ_get(environ, "TTY_COMPATIBLE").as_deref() {
2813 Some("0") => return false,
2814 Some("1") => return true,
2815 _ => {}
2816 }
2817 if let Some(force_color) = environ_get(environ, "FORCE_COLOR") {
2818 return !force_color.is_empty();
2819 }
2820 std::io::stdout().is_terminal()
2821}
2822
2823#[cfg(not(windows))]
2826fn term_colors(colors: &str) -> Option<ColorSystem> {
2827 match colors {
2828 "kitty" | "256color" => Some(ColorSystem::EightBit),
2829 "16color" => Some(ColorSystem::Standard),
2830 _ => None,
2831 }
2832}
2833
2834#[cfg_attr(windows, allow(unused_variables))]
2847fn detect_color_system(environ: Option<&Environ>) -> ColorSystem {
2848 #[cfg(windows)]
2860 {
2861 let _ = anstyle_query::windows::enable_ansi_colors();
2862 ColorSystem::Truecolor
2863 }
2864
2865 #[cfg(not(windows))]
2868 {
2869 let color_term = environ_get(environ, "COLORTERM")
2870 .unwrap_or_default()
2871 .trim()
2872 .to_lowercase();
2873 if matches!(color_term.as_str(), "truecolor" | "24bit") {
2874 return ColorSystem::Truecolor;
2875 }
2876 let term = environ_get(environ, "TERM")
2877 .unwrap_or_default()
2878 .trim()
2879 .to_lowercase();
2880 let colors = term.rsplit('-').next().unwrap_or("");
2882 term_colors(colors).unwrap_or(ColorSystem::Standard)
2883 }
2884}
2885
2886fn detect_terminal_size(legacy_windows: bool) -> ConsoleDimensions {
2890 let (width, height) = match terminal_size::terminal_size() {
2891 Some((terminal_size::Width(w), terminal_size::Height(h))) => (w as usize, h as usize),
2892 None => (0, 0),
2893 };
2894 let width = if width == 0 { DEFAULT_WIDTH } else { width };
2895 let height = if height == 0 { DEFAULT_HEIGHT } else { height };
2896 ConsoleDimensions {
2897 width: width - usize::from(legacy_windows),
2898 height,
2899 }
2900}
2901
2902impl crate::protocol::ConsoleCodeHighlighting for Console {
2903 fn set_code_highlighting(&mut self, value: Option<crate::protocol::CodeHighlighting>) {
2904 self.code_highlighting = value;
2905 }
2906 fn code_highlighting(&self) -> Option<&crate::protocol::CodeHighlighting> {
2907 self.code_highlighting.as_ref()
2908 }
2909}
2910
2911impl crate::protocol::ConsoleRegions for Console {
2912 fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>) {
2913 self.region_sink = value;
2914 }
2915 fn region_sink(&self) -> Option<&dyn crate::protocol::RegionSink> {
2916 self.region_sink.as_deref()
2917 }
2918}
2919
2920impl crate::protocol::ConsoleEnvironment for Console {
2921 fn set_render_environment(
2922 &mut self,
2923 value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
2924 ) {
2925 self.render_environment = value;
2926 }
2927 fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
2928 self.render_environment.as_deref()
2929 }
2930}
2931
2932pub enum PrintObject<'a> {
2935 Str(&'a str),
2938 Text(&'a Text),
2940 Renderable(&'a dyn Renderable),
2942}
2943
2944#[derive(Clone)]
2946pub struct PrintOptions<'a> {
2947 pub sep: &'a str,
2948 pub end: &'a str,
2949 pub style: Option<crate::style::StyleType>,
2950 pub justify: Option<Justify>,
2951 pub overflow: Option<Overflow>,
2952 pub no_wrap: Option<bool>,
2953 pub emoji: Option<bool>,
2954 pub markup: Option<bool>,
2955 pub highlight: Option<bool>,
2956 pub width: Option<usize>,
2957 pub height: Option<usize>,
2958 pub crop: bool,
2959 pub soft_wrap: Option<bool>,
2960 pub new_line_start: bool,
2961}
2962
2963impl Default for PrintOptions<'_> {
2964 fn default() -> Self {
2965 PrintOptions {
2966 sep: " ",
2967 end: "\n",
2968 style: None,
2969 justify: None,
2970 overflow: None,
2971 no_wrap: None,
2972 emoji: None,
2973 markup: None,
2974 highlight: None,
2975 width: None,
2976 height: None,
2977 crop: true,
2978 soft_wrap: None,
2979 new_line_start: false,
2980 }
2981 }
2982}
2983
2984#[derive(Clone)]
2989pub enum LogObject<'a> {
2990 Str(&'a str),
2993 Text(&'a Text),
2995 Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
2997}
2998
2999#[derive(Clone)]
3004pub struct LogOptions<'a> {
3005 pub sep: &'a str,
3006 pub end: &'a str,
3007 pub style: Option<crate::style::StyleType>,
3008 pub justify: Option<Justify>,
3009 pub emoji: Option<bool>,
3010 pub markup: Option<bool>,
3011 pub highlight: Option<bool>,
3012}
3013
3014impl Default for LogOptions<'_> {
3015 fn default() -> Self {
3016 LogOptions {
3017 sep: " ",
3018 end: "\n",
3019 style: None,
3020 justify: None,
3021 emoji: None,
3022 markup: None,
3023 highlight: None,
3024 }
3025 }
3026}
3027
3028pub(crate) struct OwnedLog {
3031 objects: Vec<OwnedLogObject>,
3032 sep: String,
3033 end: String,
3034 style: Option<crate::style::StyleType>,
3035 justify: Option<Justify>,
3036 emoji: Option<bool>,
3037 markup: Option<bool>,
3038 highlight: Option<bool>,
3039 file: String,
3040 line: u32,
3041}
3042
3043enum OwnedLogObject {
3044 Str(String),
3045 Text(Text),
3046 Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
3047}
3048
3049impl OwnedLog {
3050 pub(crate) fn new(
3052 objects: &[LogObject<'_>],
3053 options: &LogOptions<'_>,
3054 file: &str,
3055 line: u32,
3056 ) -> Self {
3057 OwnedLog {
3058 objects: objects
3059 .iter()
3060 .map(|object| match object {
3061 LogObject::Str(content) => OwnedLogObject::Str((*content).to_string()),
3062 LogObject::Text(text) => OwnedLogObject::Text((*text).clone()),
3063 LogObject::Renderable(renderable) => {
3064 OwnedLogObject::Renderable(renderable.clone())
3065 }
3066 })
3067 .collect(),
3068 sep: options.sep.to_string(),
3069 end: options.end.to_string(),
3070 style: options.style.clone(),
3071 justify: options.justify,
3072 emoji: options.emoji,
3073 markup: options.markup,
3074 highlight: options.highlight,
3075 file: file.to_string(),
3076 line,
3077 }
3078 }
3079
3080 pub(crate) fn log(&self, console: &Console) -> crate::errors::Result<()> {
3082 let objects: Vec<LogObject<'_>> = self
3083 .objects
3084 .iter()
3085 .map(|object| match object {
3086 OwnedLogObject::Str(content) => LogObject::Str(content),
3087 OwnedLogObject::Text(text) => LogObject::Text(text),
3088 OwnedLogObject::Renderable(renderable) => LogObject::Renderable(renderable.clone()),
3089 })
3090 .collect();
3091 let options = LogOptions {
3092 sep: &self.sep,
3093 end: &self.end,
3094 style: self.style.clone(),
3095 justify: self.justify,
3096 emoji: self.emoji,
3097 markup: self.markup,
3098 highlight: self.highlight,
3099 };
3100 console.log_at(&objects, &options, &self.file, self.line)
3101 }
3102}
3103
3104struct LogItem {
3108 renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3109 align: Option<crate::align::HorizontalAlign>,
3110 end: Option<String>,
3114 styles: Vec<Style>,
3115}
3116
3117impl Renderable for LogItem {
3118 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
3119 let mut segments = match self.align {
3120 Some(align) => {
3121 crate::align::Align::render_child(&*self.renderable, align, console, options)
3122 }
3123 None => self.renderable.rich_render(console, options),
3124 };
3125 if let Some(end) = self.end.as_deref().filter(|end| !end.is_empty()) {
3126 segments.push(Segment::new(end, None));
3127 if end.ends_with('\n') {
3128 segments.push(Segment::new("", None));
3130 }
3131 }
3132 for style in &self.styles {
3133 segments = Segment::apply_style_with(&segments, Some(style), None);
3134 }
3135 segments
3136 }
3137
3138 fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
3139 crate::measure::Measurement::get(console, options, &*self.renderable)
3140 }
3141}
3142
3143impl Console {
3145 #[track_caller]
3154 pub fn log(
3155 &self,
3156 objects: &[LogObject<'_>],
3157 options: &LogOptions<'_>,
3158 ) -> crate::errors::Result<()> {
3159 let caller = std::panic::Location::caller();
3160 self.log_at(objects, options, caller.file(), caller.line())
3161 }
3162
3163 pub fn log_at(
3167 &self,
3168 objects: &[LogObject<'_>],
3169 options: &LogOptions<'_>,
3170 file: &str,
3171 line: u32,
3172 ) -> crate::errors::Result<()> {
3173 let mut styles = Vec::new();
3174 if let Some(style) = &self.style {
3175 styles.push(self.get_style(style)?);
3176 }
3177 if let Some(style) = &options.style {
3178 styles.push(self.get_style(style)?);
3179 }
3180 styles.retain(|style| !style.is_null());
3181 let align = match options.justify {
3182 Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3183 Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3184 Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3185 _ => None,
3186 };
3187 let item = |renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3188 end: Option<String>|
3189 -> std::sync::Arc<dyn Renderable + Send + Sync> {
3190 std::sync::Arc::new(LogItem {
3191 renderable,
3192 align,
3193 end,
3194 styles: styles.clone(),
3195 })
3196 };
3197 let end = options
3200 .end
3201 .strip_suffix('\n')
3202 .unwrap_or(options.end)
3203 .to_string();
3204 let mut renderables: Vec<std::sync::Arc<dyn Renderable + Send + Sync>> = Vec::new();
3205 if objects.is_empty() {
3206 renderables.push(item(std::sync::Arc::new(Text::new("")), None));
3208 }
3209 let mut texts: Vec<Text> = Vec::new();
3210 let flush =
3211 |texts: &mut Vec<Text>,
3212 renderables: &mut Vec<std::sync::Arc<dyn Renderable + Send + Sync>>| {
3213 if !texts.is_empty() {
3214 let mut separator = Text::new(options.sep);
3215 separator.set_justify_option(options.justify);
3216 let joined = separator.join(texts);
3217 renderables.push(item(std::sync::Arc::new(joined), Some(end.clone())));
3218 texts.clear();
3219 }
3220 };
3221 for object in objects {
3222 match object {
3223 LogObject::Str(content) => {
3224 let render_str = RenderStrOptions {
3225 emoji: options.emoji,
3226 markup: options.markup,
3227 highlight: options.highlight,
3228 ..Default::default()
3229 };
3230 texts.push(self.render_str_with(content, &render_str)?);
3231 }
3232 LogObject::Text(text) => texts.push((*text).clone()),
3233 LogObject::Renderable(renderable) => {
3234 flush(&mut texts, &mut renderables);
3235 renderables.push(item(renderable.clone(), None));
3236 }
3237 }
3238 }
3239 flush(&mut texts, &mut renderables);
3240
3241 let time = self.log_time.then(|| {
3242 let now = (self.get_datetime)();
3243 match &self.log_time_format {
3244 LogTimeFormat::Strftime(format) => {
3245 Text::new(crate::log_render::strftime(now, format))
3246 }
3247 LogTimeFormat::Callable(format) => format(now),
3248 }
3249 });
3250 let path = file
3253 .rsplit(std::path::MAIN_SEPARATOR)
3254 .next()
3255 .unwrap_or(file);
3256 let link_path = (!file.starts_with('<')).then(|| absolute_path(file));
3257 let table = self
3258 .log_render
3259 .lock()
3260 .unwrap_or_else(std::sync::PoisonError::into_inner)
3261 .render_renderables(
3262 self,
3263 renderables,
3264 time,
3265 Text::new(""),
3266 Some(path),
3267 Some(line),
3268 link_path.as_deref(),
3269 );
3270 let mut segments = self.render(&table, None);
3271 if !segments.is_empty() {
3272 segments.push(Segment::line());
3273 }
3274 let segments = self
3275 .process_render_hooks(vec![segments], &self.options())
3276 .concat();
3277 let segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3278 .into_iter()
3279 .flatten()
3280 .collect();
3281 self.emit_end(segments, false);
3282 Ok(())
3283 }
3284
3285 pub fn print_objects(
3292 &self,
3293 objects: &[PrintObject<'_>],
3294 options: &PrintOptions<'_>,
3295 ) -> crate::errors::Result<()> {
3296 let segments = self.render_objects(objects, options)?;
3297 self.emit_end(segments, false);
3298 Ok(())
3299 }
3300
3301 pub fn render_objects(
3304 &self,
3305 objects: &[PrintObject<'_>],
3306 options: &PrintOptions<'_>,
3307 ) -> crate::errors::Result<Vec<Segment>> {
3308 let (mut no_wrap, mut overflow, mut crop) =
3309 (options.no_wrap, options.overflow, options.crop);
3310 if options.soft_wrap.unwrap_or(self.soft_wrap) {
3313 no_wrap = no_wrap.or(Some(true));
3314 overflow = overflow.or(Some(Overflow::Ignore));
3315 crop = false;
3316 }
3317 let align = match options.justify {
3318 Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3319 Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3320 Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3321 _ => None,
3322 };
3323 let mut render_options = self.options();
3324 if let Some(justify) = options.justify {
3325 render_options.justify = justify;
3326 }
3327 render_options.overflow = overflow;
3328 render_options.no_wrap = no_wrap;
3329 render_options.markup = options.markup;
3330 render_options.highlight = options.highlight;
3331 if let Some(width) = options.width {
3332 render_options = render_options.update_width(width.min(self.width()));
3333 }
3334 if options.height.is_some() {
3335 render_options.height = options.height;
3336 }
3337 let style = match &options.style {
3338 Some(style) => Some(self.get_style(style)?),
3339 None => None,
3340 };
3341 let console_style = match &self.style {
3344 Some(style) => Some(self.get_style(style)?).filter(|style| !style.is_null()),
3345 None => None,
3346 };
3347
3348 let mut items: Vec<Vec<Segment>> = Vec::new();
3350 let mut emit = |renderable: &dyn Renderable, end: Option<&str>| {
3351 let mut rendered = if render_options.max_width < 1 {
3352 Vec::new()
3353 } else {
3354 match align {
3355 Some(align) => {
3356 crate::align::Align::render_child(renderable, align, self, &render_options)
3357 }
3358 None => renderable.rich_render(self, &render_options),
3359 }
3360 };
3361 match end {
3364 Some(end) if !end.is_empty() => rendered.push(Segment::new(end, None)),
3365 Some(_) => {}
3366 None if !rendered.is_empty()
3370 && Self::print_ends_line(renderable, &render_options) =>
3371 {
3372 rendered.push(Segment::line())
3373 }
3374 None => {}
3375 }
3376 if let Some(console_style) = &console_style {
3377 rendered = Segment::apply_style_with(&rendered, Some(console_style), None);
3378 }
3379 items.push(rendered);
3380 };
3381
3382 let blank = [PrintObject::Str("")];
3385 let objects = if objects.is_empty() && options.end != "\n" {
3386 &blank[..]
3387 } else {
3388 objects
3389 };
3390 if objects.is_empty() {
3391 emit(&Text::new(""), Some("\n"));
3392 }
3393 let mut texts: Vec<Text> = Vec::new();
3394 let flush = |texts: &mut Vec<Text>, emit: &mut dyn FnMut(&dyn Renderable, Option<&str>)| {
3395 if !texts.is_empty() {
3396 let mut separator = Text::new(options.sep);
3397 separator.set_justify_option(options.justify);
3398 emit(&separator.join(texts), Some(options.end));
3399 texts.clear();
3400 }
3401 };
3402 for object in objects {
3403 match object {
3404 PrintObject::Str(content) => {
3405 let render_str = RenderStrOptions {
3406 emoji: options.emoji,
3407 markup: options.markup,
3408 highlight: options.highlight,
3409 ..Default::default()
3410 };
3411 texts.push(self.render_str_with(content, &render_str)?);
3412 }
3413 PrintObject::Text(text) => texts.push((*text).clone()),
3414 PrintObject::Renderable(renderable) => {
3415 flush(&mut texts, &mut emit);
3416 emit(*renderable, None);
3417 }
3418 }
3419 }
3420 flush(&mut texts, &mut emit);
3421
3422 let mut segments: Vec<Segment> = Vec::new();
3423 for mut rendered in self.process_render_hooks(items, &render_options) {
3424 if let Some(style) = &style {
3426 let mut styled = Vec::with_capacity(rendered.len());
3427 for (line, terminated) in Segment::split_lines_terminator(&rendered) {
3428 styled.extend(Segment::apply_style_with(&line, Some(style), None));
3429 if terminated {
3430 styled.push(Segment::line());
3431 }
3432 }
3433 rendered = styled;
3434 }
3435 segments.extend(rendered);
3436 }
3437
3438 if options.new_line_start {
3439 let plain: String = segments
3440 .iter()
3441 .map(|segment| segment.text.as_str())
3442 .collect();
3443 if plain.lines().count() > 1 {
3444 segments.insert(0, Segment::line());
3445 }
3446 }
3447 if crop {
3448 segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3449 .into_iter()
3450 .flatten()
3451 .collect();
3452 }
3453 Ok(segments)
3454 }
3455
3456 pub fn line(&self, count: usize) {
3458 if count > 0 {
3459 self.emit_end(vec![Segment::new("\n".repeat(count), None)], false);
3460 }
3461 }
3462
3463 pub fn rule(
3466 &self,
3467 title: &str,
3468 characters: &str,
3469 style: impl Into<crate::style::StyleType>,
3470 align: crate::align::HorizontalAlign,
3471 ) {
3472 let rule = crate::rule::Rule::new(title)
3473 .characters(characters)
3474 .style(style)
3475 .align(align);
3476 self.print(&rule);
3477 }
3478
3479 pub fn out(
3482 &self,
3483 objects: &[&str],
3484 sep: &str,
3485 end: &str,
3486 style: Option<crate::style::StyleType>,
3487 highlight: Option<bool>,
3488 ) -> crate::errors::Result<()> {
3489 let raw = objects.join(sep);
3490 self.print_objects(
3491 &[PrintObject::Str(&raw)],
3492 &PrintOptions {
3493 end,
3494 style,
3495 highlight,
3496 emoji: Some(false),
3497 markup: Some(false),
3498 no_wrap: Some(true),
3499 overflow: Some(Overflow::Ignore),
3500 crop: false,
3501 ..Default::default()
3502 },
3503 )
3504 }
3505
3506 pub fn print_json(
3509 &self,
3510 json: &str,
3511 options: &crate::json::JsonOptions,
3512 ) -> crate::errors::Result<()> {
3513 let renderable = crate::json::Json::with_options(json, options)?;
3514 self.print_objects(
3515 &[PrintObject::Renderable(&renderable)],
3516 &PrintOptions {
3517 soft_wrap: Some(true),
3518 ..Default::default()
3519 },
3520 )
3521 }
3522
3523 pub fn measure(
3526 &self,
3527 renderable: &dyn Renderable,
3528 options: Option<&ConsoleOptions>,
3529 ) -> crate::measure::Measurement {
3530 let default_options;
3531 let options = match options {
3532 Some(options) => options,
3533 None => {
3534 default_options = self.options();
3535 &default_options
3536 }
3537 };
3538 crate::measure::Measurement::get(self, options, renderable)
3539 }
3540}
3541
3542#[cfg(test)]
3543mod tests {
3544 use super::*;
3545
3546 fn test_console() -> Console {
3547 Console::builder()
3548 .force_terminal(true)
3549 .color_system(Some(ColorSystem::Truecolor))
3550 .width(80)
3551 .no_color(false)
3552 .build()
3553 }
3554
3555 #[test]
3558 fn empty_text_and_empty_renderables_have_distinct_endings() {
3559 let console = Console::builder().force_terminal(false).build();
3560 assert_eq!(console.render_export(&Text::new("")), "\n");
3561 #[cfg(feature = "markdown")]
3562 assert_eq!(
3563 console.render_export(&crate::markdown::Markdown::new("")),
3564 ""
3565 );
3566 assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
3567 }
3568
3569 #[test]
3570 fn try_build_text_reports_bad_markup() {
3571 let console = test_console();
3572
3573 let err = console
3574 .try_build_text("[/nope]")
3575 .expect_err("an unmatched closing tag must be an error");
3576 assert!(
3577 matches!(err, crate::errors::RichError::Markup(_)),
3578 "{err:?}"
3579 );
3580 assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
3582
3583 let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
3584 assert_eq!(strict.plain(), "hi");
3585 assert_eq!(
3586 strict.spans().len(),
3587 console.build_text("[bold]hi[/]").spans().len()
3588 );
3589 }
3590
3591 #[test]
3599 fn unknown_tag_names_render_as_no_ops() {
3600 let console = test_console();
3601 let text = console
3602 .try_build_text("[nope]x[/]")
3603 .expect("an unknown tag name is not a syntax error");
3604 assert_eq!(console.render_to_string(&text), "x");
3605 assert_eq!(
3606 console.render_to_string(&console.build_text("[a.b.c]x[/]")),
3607 "x"
3608 );
3609
3610 assert!(console.try_build_text("[bold]a[/italic]").is_err());
3612 assert!(console.try_build_text("[/nope]").is_err());
3613 }
3614
3615 #[test]
3618 fn markup_styles_bind_at_render_not_at_parse() {
3619 let themed = |definition: &str| {
3620 let mut theme = Theme::default_theme();
3621 theme.insert("accent", Style::parse(definition).unwrap());
3622 Console::builder()
3623 .force_terminal(true)
3624 .color_system(Some(ColorSystem::Truecolor))
3625 .width(80)
3626 .no_color(false)
3627 .theme(theme)
3628 .build()
3629 };
3630 let red = themed("bold red");
3631 let green = themed("underline green");
3632
3633 let text = red.build_text("[accent]hi[/]");
3635 assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
3636 assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
3638 }
3639
3640 #[test]
3643 fn try_build_text_expands_emoji_like_build_text() {
3644 let console = test_console();
3645 assert_eq!(
3646 console
3647 .try_build_text(":rocket: go")
3648 .expect("valid")
3649 .plain(),
3650 console.build_text(":rocket: go").plain()
3651 );
3652 }
3653
3654 #[test]
3655 fn renders_markup_string() {
3656 let console = test_console();
3657 assert_eq!(
3658 console.render_str_to_string("[bold red]hi[/]"),
3659 "\x1b[1;31mhi\x1b[0m"
3660 );
3661 }
3662
3663 #[test]
3664 fn print_justify_pads_to_width() {
3665 let console = Console::builder()
3666 .force_terminal(true)
3667 .color_system(Some(ColorSystem::Truecolor))
3668 .width(10)
3669 .build();
3670 assert_eq!(
3672 console.render_justified_to_string("hi", Justify::Left),
3673 "hi "
3674 );
3675 assert_eq!(
3676 console.render_justified_to_string("hi", Justify::Center),
3677 " hi "
3678 );
3679 assert_eq!(
3680 console.render_justified_to_string("hi", Justify::Right),
3681 " hi"
3682 );
3683 }
3684
3685 #[test]
3686 fn capture_records_ansi_instead_of_stdout() {
3687 let console = Console::builder()
3688 .force_terminal(true)
3689 .color_system(Some(ColorSystem::Truecolor))
3690 .width(20)
3691 .build();
3692 let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
3694 assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
3695 }
3696
3697 #[test]
3698 fn themed_exports_use_the_given_palette() {
3699 use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
3700
3701 let console = Console::builder()
3702 .force_terminal(true)
3703 .color_system(Some(ColorSystem::Truecolor))
3704 .width(20)
3705 .no_color(false)
3706 .build();
3707 let render = |c: &Console| c.print_str("hi");
3708
3709 let monokai = console.export_html_themed(&MONOKAI, render);
3712 assert!(
3713 monokai.contains("#0c0c0c"),
3714 "monokai bg missing:\n{monokai}"
3715 );
3716
3717 let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
3718 assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
3719 assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
3720
3721 let classes = console.export_html_classes_themed(&MONOKAI, render);
3723 assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
3724 let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
3725 assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
3726
3727 assert!(console.export_html(render).contains("#ffffff"));
3729 }
3730
3731 #[test]
3732 fn page_with_honors_the_styles_flag() {
3733 use std::sync::Mutex;
3734
3735 #[derive(Default)]
3736 struct Recorder(Mutex<String>);
3737 impl crate::pager::Pager for Recorder {
3738 fn show(&self, content: &str) -> std::io::Result<()> {
3739 *self.0.lock().unwrap() = content.to_string();
3740 Ok(())
3741 }
3742 }
3743
3744 let console = Console::builder()
3745 .force_terminal(true)
3746 .color_system(Some(ColorSystem::Truecolor))
3747 .width(20)
3748 .no_color(false)
3749 .build();
3750
3751 let plain = Recorder::default();
3753 console
3754 .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
3755 .unwrap();
3756 assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
3757
3758 let styled = Recorder::default();
3760 console
3761 .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
3762 .unwrap();
3763 assert_eq!(
3764 styled.0.lock().unwrap().as_str(),
3765 "\x1b[1;31mhi\x1b[0m there\n"
3766 );
3767
3768 let linked = |links: bool| {
3771 let recorder = Recorder::default();
3772 console
3773 .page_with_links(&recorder, true, links, |c| {
3774 c.print_str("[bold link=https://x.io]hi[/] there")
3775 })
3776 .unwrap();
3777 let out = recorder.0.lock().unwrap().clone();
3778 out
3779 };
3780 assert_eq!(linked(false), "\x1b[1mhi\x1b[0m there\n");
3781 assert_eq!(
3782 linked(true),
3783 "\x1b]8;;https://x.io\x1b\\\x1b[1mhi\x1b[0m\x1b]8;;\x1b\\ there\n"
3784 );
3785 }
3786
3787 #[test]
3788 fn export_text_styled_renders_truecolor() {
3789 let console = |system| {
3792 Console::builder()
3793 .force_terminal(true)
3794 .color_system(system)
3795 .width(20)
3796 .no_color(false)
3797 .build()
3798 };
3799 let export =
3800 |console: Console| console.export_text_styled(|c| c.print_str("[#ff8800]x[/] y"));
3801 assert_eq!(
3802 export(console(Some(ColorSystem::Standard))),
3803 "\x1b[91mx\x1b[0m y\n"
3804 );
3805 assert_eq!(export(console(None)), "\x1b[38;2;255;136;0mx\x1b[0m y\n");
3806 }
3807
3808 #[test]
3809 fn export_text_strips_styles() {
3810 let console = Console::builder()
3811 .force_terminal(true)
3812 .color_system(Some(ColorSystem::Truecolor))
3813 .width(20)
3814 .build();
3815 let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
3817 assert_eq!(out, "hi there\n");
3818 }
3819
3820 #[test]
3821 fn export_html_matches_upstream() {
3822 let console = Console::builder()
3823 .force_terminal(true)
3824 .color_system(Some(ColorSystem::Truecolor))
3825 .width(20)
3826 .no_color(false)
3827 .build();
3828 let html = console.export_html(|c| {
3829 c.print_str("[bold red]hi[/] there");
3830 c.print_str("plain line");
3831 });
3832 let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
3836 assert_eq!(html, expected);
3837 }
3838
3839 #[test]
3840 fn export_html_classes_matches_upstream() {
3841 let console = Console::builder()
3842 .force_terminal(true)
3843 .color_system(Some(ColorSystem::Truecolor))
3844 .width(20)
3845 .no_color(false)
3846 .build();
3847 let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
3848 let expected =
3851 include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
3852 assert_eq!(html, expected);
3853 }
3854
3855 #[test]
3858 fn render_lines_pads_with_the_given_style() {
3859 let console = test_console();
3860 let options = console.options().update_width(5).update_height(2);
3861 let lines = console.render_lines(&Text::new("hi"), &options, true);
3862 assert_eq!(lines.len(), 2);
3863 let width_pad = lines[0].last().unwrap();
3864 assert_eq!((width_pad.text.as_str(), &width_pad.style), (" ", &None));
3865 assert_eq!(lines[1], vec![Segment::new(" ", None)]);
3866
3867 let red = Style::parse("red").unwrap();
3868 let lines = console.render_lines_styled(&Text::new("hi"), &options, Some(&red), true);
3869 assert_eq!(lines[0].last().unwrap().style, Some(red.clone()));
3870 assert_eq!(lines[1], vec![Segment::new(" ", Some(red))]);
3871 }
3872
3873 #[test]
3874 fn capture_matches_direct_render() {
3875 let console = test_console();
3876 let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
3877 assert_eq!(
3878 console.capture(|c| c.print(&panel)),
3879 console.render_export(&panel)
3880 );
3881 }
3882
3883 #[test]
3884 fn no_color_strips_styles() {
3885 let console = Console::builder()
3886 .force_terminal(true)
3887 .color_system(None)
3888 .build();
3889 assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
3890 }
3891
3892 #[test]
3893 fn used_theme_applies_through_the_guard_and_pops_on_drop() {
3894 let mut console = Console::builder()
3895 .force_terminal(true)
3896 .color_system(Some(ColorSystem::Truecolor))
3897 .width(20)
3898 .highlight(false)
3899 .build();
3900 let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
3901 {
3902 let themed = console.use_theme(theme);
3903 let out = themed.capture(|c| c.print_str("[accent]x[/]"));
3904 assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
3905 }
3906 assert!(console.theme().get("accent").is_none());
3907 assert!(console.pop_theme().is_err(), "the base theme must remain");
3908 }
3909
3910 #[test]
3911 fn used_theme_is_popped_during_a_panic_unwind() {
3912 let mut console = Console::builder().width(20).build();
3913 let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
3914 let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3915 let _themed = console.use_theme(theme);
3916 panic!("render failed");
3917 }));
3918 assert!(unwound.is_err());
3919 assert!(console.theme().get("accent").is_none());
3920 }
3921
3922 #[test]
3923 fn print_objects_keeps_a_renderable_s_open_last_line() {
3924 let console = Console::builder()
3927 .force_terminal(true)
3928 .color_system(Some(crate::color::ColorSystem::Truecolor))
3929 .width(4)
3930 .height(2)
3931 .build();
3932 let screen = crate::screen::Screen::new(Box::new(Text::new("hi")));
3933 let printed = console.capture(|c| c.print(&screen));
3934 let objects = console.capture(|c| {
3935 c.print_objects(
3936 &[PrintObject::Renderable(&screen)],
3937 &PrintOptions::default(),
3938 )
3939 .unwrap()
3940 });
3941 assert_eq!(objects, printed);
3942 assert!(!objects.ends_with('\n'), "{objects:?}");
3943 }
3944
3945 #[test]
3946 fn a_printed_text_defers_layout_to_the_print_options() {
3947 let console = Console::builder()
3950 .force_terminal(true)
3951 .color_system(Some(crate::color::ColorSystem::Truecolor))
3952 .width(6)
3953 .highlight(false)
3954 .build();
3955 let text = Text::new("abcdefghij").overflow(Overflow::Ellipsis);
3956 assert_eq!(console.render_export(&text), "abcdef\nghij\n");
3957 let mut options = console.options();
3958 options.overflow = Some(Overflow::Ellipsis);
3959 assert_eq!(
3960 console.render_export_with(&Text::new("abcdefghij"), &options),
3961 "abcde…\n"
3962 );
3963 let wide = Console::builder()
3964 .force_terminal(true)
3965 .color_system(Some(crate::color::ColorSystem::Truecolor))
3966 .width(20)
3967 .highlight(false)
3968 .build();
3969 let tabbed = Text::new("a\tb").justify(Justify::Right);
3970 assert_eq!(wide.render_export(&tabbed), "a b\n");
3971 }
3972}