1use std::collections::HashMap;
82use std::fmt::Write as FmtWrite;
83use std::io::{self, Write};
84use std::sync::{
85 Arc, Mutex, Weak,
86 atomic::{AtomicBool, Ordering},
87};
88use time::OffsetDateTime;
89
90use crate::color::{ColorSystem, DEFAULT_TERMINAL_THEME, SVG_EXPORT_THEME, TerminalTheme};
91use crate::emoji;
92use crate::highlighter::{Highlighter, ReprHighlighter};
93use crate::live::LiveInner;
94use crate::markup;
95use crate::measure::{Measurement, RichMeasure};
96use crate::protocol::{RichCast, RichCastOutput};
97use crate::renderables::Renderable;
98use crate::segment::{ControlCode, ControlType, Segment};
99use crate::style::{Attributes, Style, StyleParseError};
100use crate::sync::lock_recover;
101use crate::terminal;
102use crate::text::{JustifyMethod, OverflowMethod, Text};
103use crate::theme::{Theme, ThemeStack, ThemeStackError};
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct ConsoleDimensions {
108 pub width: usize,
110 pub height: usize,
112}
113
114impl Default for ConsoleDimensions {
115 fn default() -> Self {
116 Self {
117 width: 80,
118 height: 24,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct ConsoleOptions {
126 pub size: ConsoleDimensions,
128 pub legacy_windows: bool,
130 pub min_width: usize,
132 pub max_width: usize,
134 pub is_terminal: bool,
136 pub encoding: String,
138 pub max_height: usize,
140 pub justify: Option<JustifyMethod>,
142 pub overflow: Option<OverflowMethod>,
144 pub no_wrap: Option<bool>,
146 pub highlight: Option<bool>,
148 pub markup: Option<bool>,
150 pub height: Option<usize>,
152}
153
154impl Default for ConsoleOptions {
155 fn default() -> Self {
156 Self {
157 size: ConsoleDimensions::default(),
158 legacy_windows: false,
159 min_width: 1,
160 max_width: 80,
161 is_terminal: true,
162 encoding: String::from("utf-8"),
163 max_height: usize::MAX,
164 justify: None,
165 overflow: None,
166 no_wrap: None,
167 highlight: None,
168 markup: None,
169 height: None,
170 }
171 }
172}
173
174impl ConsoleOptions {
175 #[must_use]
177 pub fn update_width(&self, width: usize) -> Self {
178 Self {
179 max_width: width.min(self.max_width),
180 ..self.clone()
181 }
182 }
183
184 #[must_use]
186 pub fn update_height(&self, height: usize) -> Self {
187 Self {
188 height: Some(height),
189 ..self.clone()
190 }
191 }
192
193 #[must_use]
195 pub fn update_dimensions(&self, width: usize, height: usize) -> Self {
196 Self {
197 size: ConsoleDimensions { width, height },
198 max_width: width,
199 max_height: height,
200 height: Some(height),
201 ..self.clone()
202 }
203 }
204}
205
206#[derive(Clone, Default)]
208pub struct PrintOptions {
209 pub sep: String,
211 pub end: String,
213 pub style: Option<Style>,
215 pub justify: Option<JustifyMethod>,
217 pub overflow: Option<OverflowMethod>,
219 pub no_wrap: Option<bool>,
221 pub no_newline: bool,
223 pub markup: Option<bool>,
225 pub highlight: Option<bool>,
227 pub highlighter: Option<Arc<dyn Highlighter>>,
229 pub width: Option<usize>,
231 pub crop: bool,
233 pub soft_wrap: bool,
235}
236
237impl PrintOptions {
238 #[must_use]
240 pub fn new() -> Self {
241 Self {
242 sep: String::from(" "),
243 end: String::from("\n"),
244 ..Default::default()
245 }
246 }
247
248 #[must_use]
250 pub fn with_markup(mut self, markup: bool) -> Self {
251 self.markup = Some(markup);
252 self
253 }
254
255 #[must_use]
257 pub fn with_style(mut self, style: Style) -> Self {
258 self.style = Some(style);
259 self
260 }
261
262 #[must_use]
264 pub fn with_sep(mut self, sep: impl Into<String>) -> Self {
265 self.sep = sep.into();
266 self
267 }
268
269 #[must_use]
271 pub fn with_end(mut self, end: impl Into<String>) -> Self {
272 self.end = end.into();
273 self
274 }
275
276 #[must_use]
278 pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
279 self.justify = Some(justify);
280 self
281 }
282
283 #[must_use]
285 pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
286 self.overflow = Some(overflow);
287 self
288 }
289
290 #[must_use]
292 pub fn with_no_wrap(mut self, no_wrap: bool) -> Self {
293 self.no_wrap = Some(no_wrap);
294 self
295 }
296
297 #[must_use]
299 pub fn with_no_newline(mut self, no_newline: bool) -> Self {
300 self.no_newline = no_newline;
301 self
302 }
303
304 #[must_use]
306 pub fn with_highlight(mut self, highlight: bool) -> Self {
307 self.highlight = Some(highlight);
308 self
309 }
310
311 #[must_use]
313 pub fn with_highlighter<H: Highlighter + 'static>(mut self, highlighter: H) -> Self {
314 self.highlighter = Some(Arc::new(highlighter));
315 self
316 }
317
318 #[must_use]
320 pub fn with_width(mut self, width: usize) -> Self {
321 self.width = Some(width);
322 self
323 }
324
325 #[must_use]
327 pub fn with_crop(mut self, crop: bool) -> Self {
328 self.crop = crop;
329 self
330 }
331
332 #[must_use]
334 pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
335 self.soft_wrap = soft_wrap;
336 self
337 }
338}
339
340impl std::fmt::Debug for PrintOptions {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("PrintOptions")
343 .field("sep", &self.sep)
344 .field("end", &self.end)
345 .field("style", &self.style)
346 .field("justify", &self.justify)
347 .field("overflow", &self.overflow)
348 .field("no_wrap", &self.no_wrap)
349 .field("no_newline", &self.no_newline)
350 .field("markup", &self.markup)
351 .field("highlight", &self.highlight)
352 .field(
353 "highlighter",
354 &self.highlighter.as_ref().map(|_| "<Highlighter>"),
355 )
356 .field("width", &self.width)
357 .field("crop", &self.crop)
358 .field("soft_wrap", &self.soft_wrap)
359 .finish()
360 }
361}
362
363pub trait RenderHook: Send + Sync {
365 fn process(&self, console: &Console, segments: &[Segment<'static>]) -> Vec<Segment<'static>>;
366}
367
368pub struct Console {
405 color_system: Option<ColorSystem>,
407 force_terminal: Option<bool>,
409 tab_size: usize,
411 record: AtomicBool,
413 markup: bool,
415 emoji: bool,
417 highlight: bool,
419 highlighter: Arc<dyn Highlighter>,
421 theme_stack: Mutex<ThemeStack>,
423 width: Option<usize>,
425 height: Option<usize>,
427 safe_box: bool,
429 file: Mutex<Box<dyn Write + Send>>,
431 buffer: Mutex<Vec<Segment<'static>>>,
433 is_terminal: bool,
435 detected_color_system: Option<ColorSystem>,
437 render_hooks: Mutex<Vec<Arc<dyn RenderHook>>>,
439 live_stack: Mutex<Vec<Weak<LiveInner>>>,
441}
442
443impl std::fmt::Debug for Console {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 f.debug_struct("Console")
446 .field("color_system", &self.color_system)
447 .field("force_terminal", &self.force_terminal)
448 .field("tab_size", &self.tab_size)
449 .field("record", &self.record.load(Ordering::Relaxed))
450 .field("markup", &self.markup)
451 .field("emoji", &self.emoji)
452 .field("highlight", &self.highlight)
453 .field("width", &self.width)
454 .field("height", &self.height)
455 .field("safe_box", &self.safe_box)
456 .field("file", &"<dyn Write>")
457 .field("buffer_len", &lock_recover(&self.buffer).len())
458 .field("is_terminal", &self.is_terminal)
459 .field("detected_color_system", &self.detected_color_system)
460 .finish_non_exhaustive()
461 }
462}
463
464impl Default for Console {
465 fn default() -> Self {
466 Self::new()
467 }
468}
469
470impl Console {
471 #[must_use]
473 pub fn new() -> Self {
474 let is_terminal = terminal::is_terminal();
475 let detected_color_system = if is_terminal {
476 terminal::detect_color_system()
477 } else {
478 None
479 };
480
481 Self {
482 color_system: None,
483 force_terminal: None,
484 tab_size: 8,
485 record: AtomicBool::new(false),
486 markup: true,
487 emoji: true,
488 highlight: true,
489 highlighter: Arc::new(ReprHighlighter::default()),
490 theme_stack: Mutex::new(ThemeStack::new(Theme::default())),
491 width: None,
492 height: None,
493 safe_box: false,
494 file: Mutex::new(Box::new(io::stdout())),
495 buffer: Mutex::new(Vec::new()),
496 is_terminal,
497 detected_color_system,
498 render_hooks: Mutex::new(Vec::new()),
499 live_stack: Mutex::new(Vec::new()),
500 }
501 }
502
503 #[must_use]
505 pub fn builder() -> ConsoleBuilder {
506 ConsoleBuilder::default()
507 }
508
509 #[must_use]
511 pub fn shared(self) -> Arc<Self> {
512 Arc::new(self)
513 }
514
515 #[must_use]
517 pub fn width(&self) -> usize {
518 self.width.unwrap_or_else(terminal::get_terminal_width)
519 }
520
521 #[must_use]
523 pub fn height(&self) -> usize {
524 self.height.unwrap_or_else(terminal::get_terminal_height)
525 }
526
527 #[must_use]
529 pub fn size(&self) -> ConsoleDimensions {
530 ConsoleDimensions {
531 width: self.width(),
532 height: self.height(),
533 }
534 }
535
536 #[must_use]
538 pub fn is_terminal(&self) -> bool {
539 self.force_terminal.unwrap_or(self.is_terminal)
540 }
541
542 #[must_use]
547 pub(crate) const fn is_terminal_detected(&self) -> bool {
548 self.is_terminal
549 }
550
551 #[must_use]
553 pub fn color_system(&self) -> Option<ColorSystem> {
554 self.color_system.or(self.detected_color_system)
555 }
556
557 #[must_use]
559 pub const fn emoji(&self) -> bool {
560 self.emoji
561 }
562
563 #[must_use]
565 pub const fn safe_box(&self) -> bool {
566 self.safe_box
567 }
568
569 #[must_use]
577 pub fn get_style(&self, name: &str) -> Style {
578 self.try_get_style(name).unwrap_or_else(|_| Style::new())
579 }
580
581 pub fn try_get_style(&self, name: &str) -> Result<Style, StyleParseError> {
583 {
584 let stack = lock_recover(&self.theme_stack);
585 if let Some(style) = stack.get(name) {
586 return Ok(style.clone());
587 }
588 }
589 Style::parse(name)
590 }
591
592 pub fn push_theme(&self, theme: Theme, inherit: bool) {
594 lock_recover(&self.theme_stack).push_theme(theme, inherit);
595 }
596
597 pub fn pop_theme(&self) -> Result<(), ThemeStackError> {
599 lock_recover(&self.theme_stack).pop_theme()
600 }
601
602 #[must_use]
604 pub fn use_theme(&self, theme: Theme, inherit: bool) -> ThemeGuard<'_> {
605 self.push_theme(theme, inherit);
606 ThemeGuard { console: self }
607 }
608
609 #[must_use]
611 pub fn is_color_enabled(&self) -> bool {
612 self.color_system().is_some()
613 }
614
615 #[must_use]
617 pub const fn tab_size(&self) -> usize {
618 self.tab_size
619 }
620
621 #[must_use]
623 pub fn options(&self) -> ConsoleOptions {
624 ConsoleOptions {
625 size: self.size(),
626 legacy_windows: false,
627 min_width: 1,
628 max_width: self.width(),
629 is_terminal: self.is_terminal(),
630 encoding: String::from("utf-8"),
631 max_height: self.height(),
632 justify: None,
633 overflow: None,
634 no_wrap: None,
635 highlight: Some(self.highlight),
636 markup: Some(self.markup),
637 height: None,
638 }
639 }
640
641 pub(crate) fn apply_highlighter_to_text(&self, options: &ConsoleOptions, text: &mut Text) {
642 let highlight_enabled = options.highlight.unwrap_or(self.highlight);
643 if highlight_enabled {
644 self.highlighter.highlight(self, text);
645 }
646 }
647
648 #[must_use]
650 pub fn measure(
651 &self,
652 renderable: &dyn RichMeasure,
653 options: Option<ConsoleOptions>,
654 ) -> Measurement {
655 let options = options.unwrap_or_else(|| self.options());
656 Measurement::get(self, &options, Some(renderable))
657 }
658
659 #[must_use]
661 pub fn is_dumb_terminal(&self) -> bool {
662 terminal::is_dumb_terminal()
663 }
664
665 #[must_use]
667 pub fn is_interactive(&self) -> bool {
668 self.is_terminal() && !self.is_dumb_terminal()
669 }
670
671 pub(crate) fn push_render_hook(&self, hook: Arc<dyn RenderHook>) {
672 lock_recover(&self.render_hooks).push(hook);
673 }
674
675 pub(crate) fn pop_render_hook(&self) -> Option<Arc<dyn RenderHook>> {
676 lock_recover(&self.render_hooks).pop()
677 }
678
679 pub(crate) fn set_live(&self, live: &Arc<LiveInner>) -> bool {
680 let mut stack = lock_recover(&self.live_stack);
681 stack.push(Arc::downgrade(live));
682 stack.len() == 1
683 }
684
685 pub(crate) fn clear_live(&self) {
686 let mut stack = lock_recover(&self.live_stack);
687 if !stack.is_empty() {
688 stack.pop();
689 }
690 }
691
692 pub(crate) fn live_stack_snapshot(&self) -> Vec<Arc<LiveInner>> {
693 let mut stack = lock_recover(&self.live_stack);
694 stack.retain(|entry| entry.strong_count() > 0);
695 let mut result = Vec::new();
696 for entry in stack.iter() {
697 if let Some(live) = entry.upgrade() {
698 result.push(live);
699 }
700 }
701 result
702 }
703
704 pub(crate) fn write_control_codes(&self, control_codes: Vec<ControlCode>) -> io::Result<()> {
705 if control_codes.is_empty() {
706 return Ok(());
707 }
708 let segment = Segment::control(control_codes);
709 let mut file = lock_recover(&self.file);
710 self.write_segments_raw(&mut *file, &[segment])
711 }
712
713 pub(crate) fn swap_file(&self, writer: Box<dyn Write + Send>) -> Box<dyn Write + Send> {
714 std::mem::replace(&mut *lock_recover(&self.file), writer)
715 }
716
717 pub fn show_cursor(&self, show: bool) -> io::Result<()> {
719 let control = if show {
720 ControlCode::new(ControlType::ShowCursor)
721 } else {
722 ControlCode::new(ControlType::HideCursor)
723 };
724 self.write_control_codes(vec![control])
725 }
726
727 pub fn set_alt_screen(&self, enable: bool) -> io::Result<()> {
729 let control = if enable {
730 ControlCode::new(ControlType::EnableAltScreen)
731 } else {
732 ControlCode::new(ControlType::DisableAltScreen)
733 };
734 self.write_control_codes(vec![control])
735 }
736
737 pub fn begin_capture(&self) {
742 self.record.store(true, Ordering::Relaxed);
743 lock_recover(&self.buffer).clear();
744 }
745
746 pub fn end_capture(&self) -> Vec<Segment<'static>> {
751 self.record.store(false, Ordering::Relaxed);
752 std::mem::take(&mut *lock_recover(&self.buffer))
753 }
754
755 pub fn print(&self, content: &str) {
766 self.print_with_options(content, &PrintOptions::new().with_markup(self.markup));
767 }
768
769 pub fn print_text(&self, text: &Text) {
771 let mut file = lock_recover(&self.file);
772 let _ = self.print_text_to(&mut *file, text);
773 }
774
775 pub fn print_text_to<W: Write>(&self, writer: &mut W, text: &Text) -> io::Result<()> {
777 let segments: Vec<Segment<'static>> = text
778 .render(&text.end)
779 .into_iter()
780 .map(Segment::into_owned)
781 .collect();
782 let segments = self.apply_render_hooks(segments);
783 self.write_segments_raw(writer, &segments)
784 }
785
786 pub fn print_segments(&self, segments: &[Segment<'_>]) {
788 let mut file = lock_recover(&self.file);
789 let _ = self.print_segments_to(&mut *file, segments);
790 }
791
792 pub fn print_segments_to<W: Write>(
794 &self,
795 writer: &mut W,
796 segments: &[Segment<'_>],
797 ) -> io::Result<()> {
798 let owned: Vec<Segment<'static>> =
799 segments.iter().cloned().map(Segment::into_owned).collect();
800 let processed = self.apply_render_hooks(owned);
801 self.write_segments_raw(writer, &processed)
802 }
803
804 pub fn print_renderable(&self, renderable: &impl Renderable) {
806 let options = self.options();
807 let segments = renderable.render(self, &options);
808 self.print_segments(&segments);
809 }
810
811 fn render_rich_cast_segments(
812 &self,
813 value: &dyn RichCast,
814 options: &PrintOptions,
815 ) -> Vec<Segment<'static>> {
816 match crate::protocol::rich_cast(value) {
817 RichCastOutput::Str(text) => self.render_str_segments(&text, options),
818 RichCastOutput::Renderable(renderable) => {
819 let options = self.options();
820 renderable
821 .render(self, &options)
822 .into_iter()
823 .map(Segment::into_owned)
824 .collect()
825 }
826 RichCastOutput::Castable(renderable) => {
827 let options = self.options();
828 renderable
829 .render(self, &options)
830 .into_iter()
831 .map(Segment::into_owned)
832 .collect()
833 }
834 }
835 }
836
837 pub fn print_cast(&self, value: &dyn RichCast) {
839 self.print_cast_with_options(value, &PrintOptions::new().with_markup(self.markup));
840 }
841
842 pub fn print_cast_with_options(&self, value: &dyn RichCast, options: &PrintOptions) {
844 let mut file = lock_recover(&self.file);
845 let _ = self.print_cast_to(&mut *file, value, options);
846 }
847
848 pub fn print_cast_to<W: Write>(
850 &self,
851 writer: &mut W,
852 value: &dyn RichCast,
853 options: &PrintOptions,
854 ) -> io::Result<()> {
855 let segments = self.render_rich_cast_segments(value, options);
856 let segments = self.apply_render_hooks(segments);
857 self.write_segments_raw(writer, &segments)
858 }
859
860 pub fn print_exception(&self, traceback: &crate::renderables::Traceback) {
864 self.print_renderable(traceback);
865 }
866
867 pub fn print_with_options(&self, content: &str, options: &PrintOptions) {
869 let mut file = lock_recover(&self.file);
870 let _ = self.print_to(&mut *file, content, options);
873 }
874
875 #[must_use]
877 pub fn export_text(&self, content: &str) -> String {
878 self.export_text_with_options(content, &PrintOptions::new().with_markup(self.markup))
879 }
880
881 #[must_use]
883 pub fn export_text_with_options(&self, content: &str, options: &PrintOptions) -> String {
884 let segments = self.render_str_segments(content, options);
885 Self::segments_to_plain(&segments)
886 }
887
888 #[must_use]
890 pub fn export_cast_text(&self, value: &dyn RichCast) -> String {
891 self.export_cast_text_with_options(value, &PrintOptions::new().with_markup(self.markup))
892 }
893
894 #[must_use]
896 pub fn export_cast_text_with_options(
897 &self,
898 value: &dyn RichCast,
899 options: &PrintOptions,
900 ) -> String {
901 let segments = self.render_rich_cast_segments(value, options);
902 Self::segments_to_plain(&segments)
903 }
904
905 #[must_use]
907 pub fn export_renderable_text(&self, renderable: &impl Renderable) -> String {
908 let options = self.options();
909 let segments = renderable.render(self, &options);
910 Self::segments_to_plain(&segments)
911 }
912
913 #[must_use]
915 pub fn export_html(&self, clear: bool) -> String {
916 self.export_html_with_options(&ExportHtmlOptions {
917 clear,
918 ..ExportHtmlOptions::default()
919 })
920 }
921
922 #[must_use]
924 pub fn export_svg(&self, clear: bool) -> String {
925 self.export_svg_with_options(&ExportSvgOptions {
926 clear,
927 ..ExportSvgOptions::default()
928 })
929 }
930
931 #[must_use]
935 pub fn export_html_with_options(&self, options: &ExportHtmlOptions) -> String {
936 assert!(
937 self.record.load(Ordering::Relaxed),
938 "To export console contents call Console::begin_capture() first"
939 );
940 let segments = self.recorded_segments(options.clear);
941 export_segments_to_html_rich(&segments, options)
942 }
943
944 #[must_use]
948 pub fn export_svg_with_options(&self, options: &ExportSvgOptions) -> String {
949 assert!(
950 self.record.load(Ordering::Relaxed),
951 "To export console contents call Console::begin_capture() first"
952 );
953 let segments = self.recorded_segments(options.clear);
954 export_segments_to_svg_rich(&segments, self.width(), options)
955 }
956
957 pub fn print_to<W: Write>(
959 &self,
960 writer: &mut W,
961 content: &str,
962 options: &PrintOptions,
963 ) -> io::Result<()> {
964 let segments = self.render_str_segments(content, options);
965 let segments = self.apply_render_hooks(segments);
966 self.write_segments_raw(writer, &segments)
967 }
968
969 fn render_str_segments(&self, content: &str, options: &PrintOptions) -> Vec<Segment<'static>> {
970 let content = if self.emoji {
971 emoji::replace(content, None)
972 } else {
973 std::borrow::Cow::Borrowed(content)
974 };
975
976 let parse_markup = options.markup.unwrap_or(self.markup);
978 let mut text = if parse_markup {
979 markup::render_or_plain_with_style_resolver(content.as_ref(), |definition| {
980 self.get_style(definition)
981 })
982 } else {
983 Text::new(content.as_ref())
984 };
985
986 let highlight_enabled = options.highlight.unwrap_or(self.highlight);
987 if highlight_enabled {
988 let highlighter = options.highlighter.as_ref().unwrap_or(&self.highlighter);
989 highlighter.highlight(self, &mut text);
990 }
991
992 if let Some(justify) = options.justify {
993 text.justify = justify;
994 }
995 if let Some(overflow) = options.overflow {
996 text.overflow = overflow;
997 }
998 if let Some(no_wrap) = options.no_wrap {
999 text.no_wrap = no_wrap;
1000 }
1001 if options.crop {
1002 text.overflow = OverflowMethod::Crop;
1003 }
1004 if options.soft_wrap {
1006 text.no_wrap = false;
1007 }
1008
1009 let width = options.width.or_else(|| {
1010 if options.justify.is_some()
1011 || options.overflow.is_some()
1012 || options.no_wrap.is_some()
1013 || options.crop
1014 || options.soft_wrap
1015 {
1016 Some(self.width())
1017 } else {
1018 None
1019 }
1020 });
1021
1022 let end = if options.no_newline { "" } else { &options.end };
1023 let mut segments: Vec<Segment<'static>> = if let Some(width) = width {
1024 let mut rendered = Vec::new();
1025 let lines = if text.no_wrap {
1026 text.split_lines()
1027 } else {
1028 text.wrap(width)
1029 };
1030 let last_index = lines.len().saturating_sub(1);
1031 let justify = match text.justify {
1032 JustifyMethod::Default => JustifyMethod::Left,
1033 other => other,
1034 };
1035
1036 for (index, mut line) in lines.into_iter().enumerate() {
1037 if text.no_wrap && line.cell_len() > width {
1038 line.truncate(width, line.overflow, false);
1039 }
1040
1041 if matches!(
1042 justify,
1043 JustifyMethod::Center | JustifyMethod::Right | JustifyMethod::Full
1044 ) && line.cell_len() < width
1045 {
1046 line.pad(width, justify);
1047 }
1048
1049 let line_end = if index == last_index { end } else { "\n" };
1050 rendered.extend(line.render(line_end).into_iter().map(Segment::into_owned));
1051 }
1052
1053 rendered
1054 } else {
1055 text.render(end)
1056 .into_iter()
1057 .map(Segment::into_owned)
1058 .collect()
1059 };
1060
1061 if let Some(ref style) = options.style {
1063 for segment in &mut segments {
1064 if !segment.is_control() {
1065 segment.style = Some(match segment.style {
1066 Some(ref s) => style.combine(s),
1067 None => style.clone(),
1068 });
1069 }
1070 }
1071 }
1072
1073 segments
1074 }
1075
1076 fn segments_to_plain(segments: &[Segment<'_>]) -> String {
1077 let capacity: usize = segments
1078 .iter()
1079 .filter(|segment| !segment.is_control())
1080 .map(|segment| segment.text.len())
1081 .sum();
1082 let mut output = String::with_capacity(capacity);
1083 for segment in segments {
1084 if !segment.is_control() {
1085 output.push_str(segment.text.as_ref());
1086 }
1087 }
1088 output
1089 }
1090
1091 fn recorded_segments(&self, clear: bool) -> Vec<Segment<'static>> {
1092 let mut buffer = lock_recover(&self.buffer);
1093 let segments = buffer.clone();
1094 if clear {
1095 buffer.clear();
1096 }
1097 segments
1098 }
1099
1100 fn apply_render_hooks(&self, segments: Vec<Segment<'static>>) -> Vec<Segment<'static>> {
1101 let hooks = lock_recover(&self.render_hooks).clone();
1102 if hooks.is_empty() {
1103 return segments;
1104 }
1105 let mut current = segments;
1106 for hook in hooks {
1107 current = hook.process(self, ¤t);
1108 }
1109 current
1110 }
1111
1112 fn write_segments_raw<W: Write>(
1114 &self,
1115 writer: &mut W,
1116 segments: &[Segment<'_>],
1117 ) -> io::Result<()> {
1118 if self.record.load(Ordering::Relaxed) {
1119 lock_recover(&self.buffer).extend(segments.iter().cloned().map(Segment::into_owned));
1120 }
1121
1122 let color_system = self.color_system();
1123
1124 for segment in segments {
1125 if segment.is_control() {
1126 self.write_control_segment(writer, segment)?;
1127 continue;
1128 }
1129
1130 let ansi_codes;
1132 let (prefix, suffix) = if let Some(ref style) = segment.style {
1133 if let Some(cs) = color_system {
1134 ansi_codes = style.render_ansi(cs);
1135 (&ansi_codes.0, &ansi_codes.1)
1136 } else {
1137 static EMPTY: (String, String) = (String::new(), String::new());
1138 (&EMPTY.0, &EMPTY.1)
1139 }
1140 } else {
1141 static EMPTY: (String, String) = (String::new(), String::new());
1142 (&EMPTY.0, &EMPTY.1)
1143 };
1144
1145 write!(writer, "{prefix}{}{suffix}", segment.text)?;
1147 }
1148
1149 writer.flush()
1150 }
1151
1152 fn write_control_segment<W: Write>(
1153 &self,
1154 writer: &mut W,
1155 segment: &Segment<'_>,
1156 ) -> io::Result<()> {
1157 let Some(ref controls) = segment.control else {
1158 return Ok(());
1159 };
1160
1161 for control in controls {
1162 match control.control_type {
1163 crate::segment::ControlType::Bell => {
1164 write!(writer, "\x07")?;
1165 }
1166 crate::segment::ControlType::CarriageReturn => {
1167 write!(writer, "\r")?;
1168 }
1169 crate::segment::ControlType::Home => {
1170 write!(writer, "\x1b[H")?;
1171 }
1172 crate::segment::ControlType::Clear => {
1173 write!(writer, "\x1b[2J")?;
1174 }
1175 crate::segment::ControlType::ShowCursor => {
1176 write!(writer, "\x1b[?25h")?;
1177 }
1178 crate::segment::ControlType::HideCursor => {
1179 write!(writer, "\x1b[?25l")?;
1180 }
1181 crate::segment::ControlType::EnableAltScreen => {
1182 write!(writer, "\x1b[?1049h")?;
1183 }
1184 crate::segment::ControlType::DisableAltScreen => {
1185 write!(writer, "\x1b[?1049l")?;
1186 }
1187 crate::segment::ControlType::CursorUp => {
1188 let n = control_param(&control.params, 0, 1);
1189 write!(writer, "\x1b[{n}A")?;
1190 }
1191 crate::segment::ControlType::CursorDown => {
1192 let n = control_param(&control.params, 0, 1);
1193 write!(writer, "\x1b[{n}B")?;
1194 }
1195 crate::segment::ControlType::CursorForward => {
1196 let n = control_param(&control.params, 0, 1);
1197 write!(writer, "\x1b[{n}C")?;
1198 }
1199 crate::segment::ControlType::CursorBackward => {
1200 let n = control_param(&control.params, 0, 1);
1201 write!(writer, "\x1b[{n}D")?;
1202 }
1203 crate::segment::ControlType::CursorMoveToColumn => {
1204 let column0 = control_param(&control.params, 0, 0);
1207 write!(writer, "\x1b[{}G", column0 + 1)?;
1208 }
1209 crate::segment::ControlType::CursorMoveTo => {
1210 let x0 = control_param(&control.params, 0, 0);
1212 let y0 = control_param(&control.params, 1, 0);
1213 write!(writer, "\x1b[{};{}H", y0 + 1, x0 + 1)?;
1214 }
1215 crate::segment::ControlType::EraseInLine => {
1216 let mode = erase_in_line_mode(&control.params);
1217 write!(writer, "\x1b[{mode}K")?;
1218 }
1219 crate::segment::ControlType::SetWindowTitle => {
1220 let title = control_title(segment, control);
1221 write!(writer, "\x1b]0;{title}\x07")?;
1222 }
1223 }
1224 }
1225
1226 Ok(())
1227 }
1228
1229 pub fn line(&self) {
1231 let mut file = lock_recover(&self.file);
1232 let _ = writeln!(file);
1233 }
1234
1235 pub fn rule(&self, title: Option<&str>) {
1237 let width = self.width();
1238 let line_char = if self.safe_box { '-' } else { '\u{2500}' };
1239
1240 let mut file = lock_recover(&self.file);
1241 if let Some(title) = title {
1242 let max_title_width = width.saturating_sub(2);
1244 let title_len = crate::cells::cell_len(title);
1245
1246 let display_title = if title_len > max_title_width {
1247 let mut t = Text::new(title);
1248 t.truncate(max_title_width, OverflowMethod::Ellipsis, false);
1249 t.plain().to_string()
1250 } else {
1251 title.to_string()
1252 };
1253
1254 let display_len = crate::cells::cell_len(&display_title);
1255 let available = width.saturating_sub(display_len + 2);
1256 let left_pad = available / 2;
1257 let right_pad = available - left_pad;
1258 let left = line_char.to_string().repeat(left_pad);
1259 let right = line_char.to_string().repeat(right_pad);
1260 let _ = writeln!(file, "{left} {display_title} {right}");
1261 } else {
1262 let _ = writeln!(file, "{}", line_char.to_string().repeat(width));
1263 }
1264 }
1265
1266 pub fn clear(&self) {
1268 let mut file = lock_recover(&self.file);
1269 let _ = terminal::control::clear_screen(&mut *file);
1270 }
1271
1272 pub fn clear_line(&self) {
1274 let mut file = lock_recover(&self.file);
1275 let _ = terminal::control::clear_line(&mut *file);
1276 }
1277
1278 pub fn set_title(&self, title: &str) {
1280 let mut file = lock_recover(&self.file);
1281 let _ = terminal::control::set_title(&mut *file, title);
1282 }
1283
1284 pub fn bell(&self) {
1286 let mut file = lock_recover(&self.file);
1287 let _ = terminal::control::bell(&mut *file);
1288 }
1289
1290 pub fn print_plain(&self, content: &str) {
1292 self.print_with_options(content, &PrintOptions::new().with_markup(false));
1293 }
1294
1295 pub fn print_styled(&self, content: &str, style: Style) {
1297 self.print_with_options(
1298 content,
1299 &PrintOptions::new()
1300 .with_markup(self.markup)
1301 .with_style(style),
1302 );
1303 }
1304
1305 pub fn log(&self, message: &str, level: LogLevel) {
1320 self.log_with_options(message, level, &LogOptions::new());
1321 }
1322
1323 pub fn log_with_options(&self, message: &str, level: LogLevel, options: &LogOptions) {
1345 let (level_prefix, level_style) = match level {
1346 LogLevel::Debug => ("[DEBUG]", Style::parse("cyan").unwrap_or_default()),
1347 LogLevel::Info => ("[INFO]", Style::parse("green").unwrap_or_default()),
1348 LogLevel::Warning => ("[WARNING]", Style::parse("yellow").unwrap_or_default()),
1349 LogLevel::Error => ("[ERROR]", Style::parse("bold red").unwrap_or_default()),
1350 };
1351
1352 {
1353 let mut file = lock_recover(&self.file);
1354 if options.show_timestamp {
1356 let timestamp = Self::format_timestamp(options.timestamp_format.as_deref());
1357 let ts_style = Style::parse("dim").unwrap_or_default();
1358 let _ = self.print_to(
1359 &mut *file,
1360 ×tamp,
1361 &PrintOptions::new().with_markup(false).with_style(ts_style),
1362 );
1363 let _ = write!(file, " ");
1364 }
1365
1366 if options.file_path.is_some() || options.line_number.is_some() {
1368 let path_style = Style::parse("magenta").unwrap_or_default();
1369 let path_info = match (&options.file_path, options.line_number) {
1370 (Some(path), Some(line)) => format!("{path}:{line}"),
1371 (Some(path), None) => path.clone(),
1372 (None, Some(line)) => format!(":{line}"),
1373 (None, None) => String::new(),
1374 };
1375 if !path_info.is_empty() {
1376 let _ = self.print_to(
1377 &mut *file,
1378 &path_info,
1379 &PrintOptions::new()
1380 .with_markup(false)
1381 .with_style(path_style),
1382 );
1383 let _ = write!(file, " ");
1384 }
1385 }
1386
1387 if options.show_level {
1389 let _ = self.print_to(
1390 &mut *file,
1391 level_prefix,
1392 &PrintOptions::new()
1393 .with_markup(false)
1394 .with_style(level_style),
1395 );
1396 let _ = write!(file, " ");
1397 }
1398
1399 let _ = self.print_to(
1401 &mut *file,
1402 message,
1403 &PrintOptions::new().with_markup(self.markup),
1404 );
1405 }
1406 }
1407
1408 fn format_timestamp(format: Option<&str>) -> String {
1410 let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
1413
1414 match format {
1415 None => format!(
1416 "[{:02}:{:02}:{:02}]",
1417 now.hour(),
1418 now.minute(),
1419 now.second()
1420 ),
1421 Some(fmt) => Self::format_timestamp_strftime_subset(&now, fmt),
1422 }
1423 }
1424
1425 fn format_timestamp_strftime_subset(now: &OffsetDateTime, fmt: &str) -> String {
1428 let mut out = String::with_capacity(fmt.len().saturating_add(8));
1429 let mut it = fmt.chars();
1430
1431 while let Some(ch) = it.next() {
1432 if ch != '%' {
1433 out.push(ch);
1434 continue;
1435 }
1436
1437 let Some(code) = it.next() else {
1438 out.push('%');
1439 break;
1440 };
1441
1442 match code {
1443 '%' => out.push('%'),
1444 'H' => {
1445 let _ = write!(out, "{:02}", now.hour());
1446 }
1447 'M' => {
1448 let _ = write!(out, "{:02}", now.minute());
1449 }
1450 'S' => {
1451 let _ = write!(out, "{:02}", now.second());
1452 }
1453 'Y' => {
1454 let _ = write!(out, "{:04}", now.year());
1455 }
1456 'm' => {
1457 let _ = write!(out, "{:02}", u8::from(now.month()));
1459 }
1460 'd' => {
1461 let _ = write!(out, "{:02}", now.day());
1462 }
1463 other => {
1464 out.push('%');
1466 out.push(other);
1467 }
1468 }
1469 }
1470
1471 out
1472 }
1473}
1474
1475fn control_param(params: &[i32], index: usize, default: i32) -> i32 {
1476 params
1477 .get(index)
1478 .copied()
1479 .filter(|value| *value > 0)
1480 .unwrap_or(default)
1481}
1482
1483fn erase_in_line_mode(params: &[i32]) -> i32 {
1484 if let Some(value) = params.first().copied()
1485 && (0..=2).contains(&value)
1486 {
1487 return value;
1488 }
1489 2
1490}
1491
1492fn control_title(segment: &Segment<'_>, control: &crate::segment::ControlCode) -> String {
1493 let raw_title = if !segment.text.is_empty() {
1494 segment.text.to_string()
1495 } else if !control.params.is_empty() {
1496 let mut title = String::with_capacity(control.params.len());
1497 for param in &control.params {
1498 if let Ok(byte) = u8::try_from(*param) {
1499 title.push(byte as char);
1500 }
1501 }
1502 title
1503 } else {
1504 String::new()
1505 };
1506
1507 raw_title
1510 .chars()
1511 .filter(|c| {
1512 !c.is_control()
1515 })
1516 .collect()
1517}
1518
1519pub const CONSOLE_HTML_FORMAT: &str = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n{stylesheet}\nbody {\n color: {foreground};\n background-color: {background};\n}\n</style>\n</head>\n<body>\n <pre style=\"font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><code style=\"font-family:inherit\">{code}</code></pre>\n</body>\n</html>\n";
1525
1526pub const CONSOLE_SVG_FORMAT: &str = "<svg class=\"rich-terminal\" viewBox=\"0 0 {width} {height}\" xmlns=\"http://www.w3.org/2000/svg\">\n <!-- Generated with Rich https://www.textualize.io -->\n <style>\n\n @font-face {\n font-family: \"Fira Code\";\n src: local(\"FiraCode-Regular\"),\n url(\"https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2\") format(\"woff2\"),\n url(\"https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff\") format(\"woff\");\n font-style: normal;\n font-weight: 400;\n }\n @font-face {\n font-family: \"Fira Code\";\n src: local(\"FiraCode-Bold\"),\n url(\"https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2\") format(\"woff2\"),\n url(\"https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff\") format(\"woff\");\n font-style: bold;\n font-weight: 700;\n }\n\n .{unique_id}-matrix {\n font-family: Fira Code, monospace;\n font-size: {char_height}px;\n line-height: {line_height}px;\n font-variant-east-asian: full-width;\n }\n\n .{unique_id}-title {\n font-size: 18px;\n font-weight: bold;\n font-family: arial;\n }\n\n {styles}\n </style>\n\n <defs>\n <clipPath id=\"{unique_id}-clip-terminal\">\n <rect x=\"0\" y=\"0\" width=\"{terminal_width}\" height=\"{terminal_height}\" />\n </clipPath>\n {lines}\n </defs>\n\n {chrome}\n <g transform=\"translate({terminal_x}, {terminal_y})\" clip-path=\"url(#{unique_id}-clip-terminal)\">\n {backgrounds}\n <g class=\"{unique_id}-matrix\">\n {matrix}\n </g>\n </g>\n</svg>\n";
1528
1529#[derive(Debug, Clone)]
1531pub struct ExportHtmlOptions {
1532 pub theme: TerminalTheme,
1533 pub clear: bool,
1534 pub code_format: Option<String>,
1536 pub inline_styles: bool,
1537}
1538
1539impl Default for ExportHtmlOptions {
1540 fn default() -> Self {
1541 Self {
1542 theme: DEFAULT_TERMINAL_THEME,
1543 clear: true,
1544 code_format: None,
1545 inline_styles: false,
1546 }
1547 }
1548}
1549
1550#[derive(Debug, Clone)]
1552pub struct ExportSvgOptions {
1553 pub title: String,
1554 pub theme: TerminalTheme,
1555 pub clear: bool,
1556 pub code_format: Option<String>,
1558 pub font_aspect_ratio: f64,
1559 pub unique_id: Option<String>,
1560}
1561
1562impl Default for ExportSvgOptions {
1563 fn default() -> Self {
1564 Self {
1565 title: "Rich".to_string(),
1566 theme: SVG_EXPORT_THEME,
1567 clear: true,
1568 code_format: None,
1569 font_aspect_ratio: 0.61,
1570 unique_id: None,
1571 }
1572 }
1573}
1574
1575fn export_segments_to_html_rich(segments: &[Segment<'_>], options: &ExportHtmlOptions) -> String {
1576 let theme = options.theme;
1577 let render_code_format = options
1578 .code_format
1579 .as_deref()
1580 .unwrap_or(CONSOLE_HTML_FORMAT);
1581
1582 let simplified = crate::segment::simplify(segments.iter().cloned());
1583
1584 let mut fragments: Vec<String> = Vec::new();
1585 let mut stylesheet = String::new();
1586
1587 if options.inline_styles {
1588 for segment in simplified {
1589 if segment.is_control() {
1590 continue;
1591 }
1592 let mut text = escape_html_rich(segment.text.as_ref());
1593 if let Some(style) = &segment.style {
1594 let rule = style.get_html_style(theme);
1595 if let Some(link) = &style.link {
1596 text = format!("<a href=\"{link}\">{text}</a>");
1597 }
1598 if !rule.is_empty() {
1599 text = format!("<span style=\"{rule}\">{text}</span>");
1600 }
1601 }
1602 fragments.push(text);
1603 }
1604 } else {
1605 let mut rules_to_no: HashMap<String, usize> = HashMap::new();
1606 let mut rules_in_order: Vec<String> = Vec::new();
1607
1608 let mut get_no = |rule: &str| -> usize {
1609 if let Some(n) = rules_to_no.get(rule) {
1610 *n
1611 } else {
1612 let n = rules_in_order.len() + 1;
1613 rules_in_order.push(rule.to_string());
1614 rules_to_no.insert(rule.to_string(), n);
1615 n
1616 }
1617 };
1618
1619 for segment in simplified {
1620 if segment.is_control() {
1621 continue;
1622 }
1623 let mut text = escape_html_rich(segment.text.as_ref());
1624 if let Some(style) = &segment.style {
1625 let rule = style.get_html_style(theme);
1626 let style_no = get_no(&rule);
1627 if let Some(link) = &style.link {
1628 text = format!("<a class=\"r{style_no}\" href=\"{link}\">{text}</a>");
1629 } else {
1630 text = format!("<span class=\"r{style_no}\">{text}</span>");
1631 }
1632 }
1633 fragments.push(text);
1634 }
1635
1636 let mut stylesheet_rules: Vec<String> = Vec::new();
1637 for (idx, rule) in rules_in_order.iter().enumerate() {
1638 let style_no = idx + 1;
1639 if !rule.is_empty() {
1640 stylesheet_rules.push(format!(".r{style_no} {{{rule}}}"));
1641 }
1642 }
1643 stylesheet = stylesheet_rules.join("\n");
1644 }
1645
1646 let code = fragments.join("");
1647 let foreground = theme.foreground_color.hex();
1648 let background = theme.background_color.hex();
1649 apply_template(
1650 render_code_format,
1651 &[
1652 ("code", &code),
1653 ("stylesheet", &stylesheet),
1654 ("foreground", &foreground),
1655 ("background", &background),
1656 ],
1657 )
1658}
1659
1660#[expect(
1661 clippy::cast_precision_loss,
1662 reason = "SVG export uses f64 coordinates; console widths/heights are small in practice"
1663)]
1664fn export_segments_to_svg_rich(
1665 segments: &[Segment<'_>],
1666 console_width: usize,
1667 options: &ExportSvgOptions,
1668) -> String {
1669 use crate::cells::cell_len;
1670
1671 let theme = options.theme;
1672 let code_format = options.code_format.as_deref().unwrap_or(CONSOLE_SVG_FORMAT);
1673
1674 let width = console_width;
1675 let char_height = 20.0_f64;
1676 let char_width = char_height * options.font_aspect_ratio;
1677 let line_height = char_height * 1.22;
1678
1679 let margin_top = 1.0_f64;
1680 let margin_right = 1.0_f64;
1681 let margin_bottom = 1.0_f64;
1682 let margin_left = 1.0_f64;
1683
1684 let padding_top = 40.0_f64;
1685 let padding_right = 8.0_f64;
1686 let padding_bottom = 8.0_f64;
1687 let padding_left = 8.0_f64;
1688
1689 let padding_width = padding_left + padding_right;
1690 let padding_height = padding_top + padding_bottom;
1691 let margin_width = margin_left + margin_right;
1692 let margin_height = margin_top + margin_bottom;
1693
1694 let mut style_cache: HashMap<Style, String> = HashMap::new();
1695 let mut get_svg_style = |style: &Style| -> String {
1696 if let Some(cached) = style_cache.get(style) {
1697 return cached.clone();
1698 }
1699 let css = style.get_svg_style(theme);
1700 style_cache.insert(style.clone(), css.clone());
1701 css
1702 };
1703
1704 let mut text_backgrounds: Vec<String> = Vec::new();
1705 let mut text_group: Vec<String> = Vec::new();
1706
1707 let mut classes_to_no: HashMap<String, usize> = HashMap::new();
1708 let mut classes_in_order: Vec<String> = Vec::new();
1709 let mut get_class_no = |rules: &str| -> usize {
1710 if let Some(n) = classes_to_no.get(rules) {
1711 *n
1712 } else {
1713 let n = classes_in_order.len() + 1;
1714 classes_in_order.push(rules.to_string());
1715 classes_to_no.insert(rules.to_string(), n);
1716 n
1717 }
1718 };
1719
1720 let escape_text = |text: &str| -> String { escape_html_rich(text).replace(' ', " ") };
1721
1722 let segments: Vec<Segment<'static>> = segments
1723 .iter()
1724 .cloned()
1725 .map(Segment::into_owned)
1726 .filter(|seg| !seg.is_control())
1727 .collect();
1728
1729 let unique_id = options.unique_id.clone().unwrap_or_else(|| {
1730 let mut repr = String::new();
1731 for seg in &segments {
1732 if seg.is_control() {
1733 continue;
1734 }
1735 let _ = FmtWrite::write_fmt(
1736 &mut repr,
1737 format_args!(
1738 "Segment(text={:?},style={:?},control={:?})",
1739 seg.text, seg.style, seg.control
1740 ),
1741 );
1742 }
1743 repr.push_str(&options.title);
1744 let checksum = adler32(repr.as_bytes());
1745 format!("terminal-{checksum}")
1746 });
1747
1748 let mut y_last = 0usize;
1749 let mut lines = crate::segment::split_lines(segments.into_iter());
1750 lines = lines
1751 .into_iter()
1752 .map(|line| crate::segment::adjust_line_length(line, width, None, false))
1753 .collect();
1754
1755 let default_style = Style::default();
1756
1757 for (y, line) in lines.iter().enumerate() {
1758 y_last = y;
1759 let mut x_cells = 0usize;
1760 for segment in line {
1761 if segment.is_control() {
1762 continue;
1763 }
1764
1765 let text = segment.text.as_ref();
1766 let style = segment.style.as_ref().unwrap_or(&default_style);
1767 let rules = get_svg_style(style);
1768 let class_no = get_class_no(&rules);
1769 let class_name = format!("r{class_no}");
1770
1771 let (has_background, background_hex) = if style.attributes.contains(Attributes::REVERSE)
1772 {
1773 let bg = match &style.color {
1774 None => theme.foreground_color,
1775 Some(c) => c.get_truecolor_with_theme(theme, true),
1776 };
1777 (true, bg.hex())
1778 } else {
1779 let has_bg = style.bgcolor.as_ref().is_some_and(|c| !c.is_default());
1780 let bg = match &style.bgcolor {
1781 None => theme.background_color,
1782 Some(c) => c.get_truecolor_with_theme(theme, false),
1783 };
1784 (has_bg, bg.hex())
1785 };
1786
1787 let text_length = cell_len(text);
1788 if has_background {
1789 text_backgrounds.push(format!(
1790 "<rect fill=\"{background_hex}\" x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" shape-rendering=\"crispEdges\"/>",
1791 (x_cells as f64) * char_width,
1792 (y as f64) * line_height + 1.5,
1793 char_width * (text_length as f64),
1794 line_height + 0.25
1795 ));
1796 }
1797
1798 let all_spaces = text.chars().all(|ch| ch == ' ');
1799 if !all_spaces {
1800 let text_len_chars = text.chars().count();
1801 text_group.push(format!(
1802 "<text class=\"{unique_id}-{class_name}\" x=\"{}\" y=\"{}\" textLength=\"{}\" clip-path=\"url(#{unique_id}-line-{y})\">{}</text>",
1803 (x_cells as f64) * char_width,
1804 (y as f64) * line_height + char_height,
1805 char_width * (text_len_chars as f64),
1806 escape_text(text)
1807 ));
1808 }
1809
1810 x_cells = x_cells.saturating_add(cell_len(text));
1811 }
1812 }
1813
1814 let mut lines_defs = String::new();
1815 if y_last > 0 {
1816 for line_no in 0..y_last {
1817 let offset = (line_no as f64) * line_height + 1.5;
1818 let _ = FmtWrite::write_fmt(
1819 &mut lines_defs,
1820 format_args!(
1821 "<clipPath id=\"{unique_id}-line-{line_no}\">\n <rect x=\"0\" y=\"{offset}\" width=\"{}\" height=\"{}\"/>\n </clipPath>",
1822 char_width * (width as f64),
1823 line_height + 0.25
1824 ),
1825 );
1826 }
1827 }
1828
1829 let mut styles = String::new();
1830 for (idx, css) in classes_in_order.iter().enumerate() {
1831 let rule_no = idx + 1;
1832 let _ = FmtWrite::write_fmt(
1833 &mut styles,
1834 format_args!(".{unique_id}-r{rule_no} {{ {css} }}\n"),
1835 );
1836 }
1837
1838 let backgrounds = text_backgrounds.join("");
1839 let matrix = text_group.join("");
1840
1841 let outer_terminal_width = ((width as f64) * char_width + padding_width).ceil();
1842 let outer_terminal_height = ((y_last as f64) + 1.0) * line_height + padding_height;
1843
1844 let mut chrome = format!(
1845 "<rect fill=\"{}\" stroke=\"rgba(255,255,255,0.35)\" stroke-width=\"1\" x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" rx=\"8\"/>",
1846 theme.background_color.hex(),
1847 margin_left,
1848 margin_top,
1849 outer_terminal_width,
1850 outer_terminal_height
1851 );
1852
1853 if !options.title.is_empty() {
1854 let title_fill = theme.foreground_color.hex();
1855 let title_x = outer_terminal_width / 2.0;
1856 let title_y = margin_top + char_height + 6.0;
1857 let _ = FmtWrite::write_fmt(
1858 &mut chrome,
1859 format_args!(
1860 "<text class=\"{unique_id}-title\" fill=\"{title_fill}\" text-anchor=\"middle\" x=\"{title_x}\" y=\"{title_y}\">{}</text>",
1861 escape_text(&options.title)
1862 ),
1863 );
1864 }
1865 chrome.push_str(
1866 "\n <g transform=\"translate(26,22)\">\n <circle cx=\"0\" cy=\"0\" r=\"7\" fill=\"#ff5f57\"/>\n <circle cx=\"22\" cy=\"0\" r=\"7\" fill=\"#febc2e\"/>\n <circle cx=\"44\" cy=\"0\" r=\"7\" fill=\"#28c840\"/>\n </g>\n ",
1867 );
1868
1869 let char_width_s = char_width.to_string();
1870 let char_height_s = char_height.to_string();
1871 let line_height_s = line_height.to_string();
1872 let terminal_width_s = (char_width * (width as f64) - 1.0).to_string();
1873 let terminal_height_s = (((y_last as f64) + 1.0) * line_height - 1.0).to_string();
1874 let width_s = (outer_terminal_width + margin_width).to_string();
1875 let height_s = (outer_terminal_height + margin_height).to_string();
1876 let terminal_translate_x = (margin_left + padding_left).to_string();
1877 let terminal_translate_y = (margin_top + padding_top).to_string();
1878
1879 apply_template(
1880 code_format,
1881 &[
1882 ("unique_id", &unique_id),
1883 ("char_width", &char_width_s),
1884 ("char_height", &char_height_s),
1885 ("line_height", &line_height_s),
1886 ("terminal_width", &terminal_width_s),
1887 ("terminal_height", &terminal_height_s),
1888 ("width", &width_s),
1889 ("height", &height_s),
1890 ("terminal_x", &terminal_translate_x),
1891 ("terminal_y", &terminal_translate_y),
1892 ("styles", &styles),
1893 ("chrome", &chrome),
1894 ("backgrounds", &backgrounds),
1895 ("matrix", &matrix),
1896 ("lines", &lines_defs),
1897 ],
1898 )
1899}
1900
1901fn apply_template(template: &str, vars: &[(&str, &str)]) -> String {
1902 let mut out = template.to_string();
1903 for (key, value) in vars {
1904 out = out.replace(&format!("{{{key}}}"), value);
1905 }
1906 out
1907}
1908
1909fn escape_html_rich(text: &str) -> String {
1910 let mut escaped = String::with_capacity(text.len());
1911 for ch in text.chars() {
1912 match ch {
1913 '&' => escaped.push_str("&"),
1914 '<' => escaped.push_str("<"),
1915 '>' => escaped.push_str(">"),
1916 '"' => escaped.push_str("""),
1917 _ => escaped.push(ch),
1918 }
1919 }
1920 escaped
1921}
1922
1923fn adler32(bytes: &[u8]) -> u32 {
1924 const MOD_ADLER: u32 = 65521;
1925 let mut a: u32 = 1;
1926 let mut b: u32 = 0;
1927 for &byte in bytes {
1928 a = (a + u32::from(byte)) % MOD_ADLER;
1929 b = (b + a) % MOD_ADLER;
1930 }
1931 (b << 16) | a
1932}
1933
1934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1936pub enum LogLevel {
1937 Debug,
1938 Info,
1939 Warning,
1940 Error,
1941}
1942
1943#[derive(Debug, Clone)]
1963pub struct LogOptions {
1964 pub show_timestamp: bool,
1966 pub timestamp_format: Option<String>,
1973 pub file_path: Option<String>,
1975 pub line_number: Option<u32>,
1977 pub show_level: bool,
1979 pub highlight: bool,
1981}
1982
1983impl Default for LogOptions {
1984 fn default() -> Self {
1985 Self::new()
1986 }
1987}
1988
1989impl LogOptions {
1990 #[must_use]
1992 pub fn new() -> Self {
1993 Self {
1994 show_timestamp: false,
1995 timestamp_format: None,
1996 file_path: None,
1997 line_number: None,
1998 show_level: true,
1999 highlight: false,
2000 }
2001 }
2002
2003 #[must_use]
2005 pub fn with_timestamp(mut self, show: bool) -> Self {
2006 self.show_timestamp = show;
2007 self
2008 }
2009
2010 #[must_use]
2015 pub fn with_timestamp_format(mut self, format: impl Into<String>) -> Self {
2016 self.timestamp_format = Some(format.into());
2017 self
2018 }
2019
2020 #[must_use]
2022 pub fn with_path(mut self, file: impl Into<String>, line: u32) -> Self {
2023 self.file_path = Some(file.into());
2024 self.line_number = Some(line);
2025 self
2026 }
2027
2028 #[must_use]
2030 pub fn with_file(mut self, file: impl Into<String>) -> Self {
2031 self.file_path = Some(file.into());
2032 self
2033 }
2034
2035 #[must_use]
2037 pub fn with_line(mut self, line: u32) -> Self {
2038 self.line_number = Some(line);
2039 self
2040 }
2041
2042 #[must_use]
2044 pub fn with_level(mut self, show: bool) -> Self {
2045 self.show_level = show;
2046 self
2047 }
2048
2049 #[must_use]
2051 pub fn with_highlight(mut self, highlight: bool) -> Self {
2052 self.highlight = highlight;
2053 self
2054 }
2055}
2056
2057pub struct ThemeGuard<'a> {
2059 console: &'a Console,
2060}
2061
2062impl Drop for ThemeGuard<'_> {
2063 fn drop(&mut self) {
2064 let _ = self.console.pop_theme();
2065 }
2066}
2067
2068#[derive(Default)]
2070pub struct ConsoleBuilder {
2071 color_system: Option<ColorSystem>,
2072 force_terminal: Option<bool>,
2073 tab_size: Option<usize>,
2074 markup: Option<bool>,
2075 emoji: Option<bool>,
2076 highlight: Option<bool>,
2077 highlighter: Option<Arc<dyn Highlighter>>,
2078 width: Option<usize>,
2079 height: Option<usize>,
2080 safe_box: Option<bool>,
2081 theme: Option<Theme>,
2082 file: Option<Box<dyn Write + Send>>,
2083}
2084
2085impl std::fmt::Debug for ConsoleBuilder {
2086 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2087 f.debug_struct("ConsoleBuilder")
2088 .field("color_system", &self.color_system)
2089 .field("force_terminal", &self.force_terminal)
2090 .field("tab_size", &self.tab_size)
2091 .field("markup", &self.markup)
2092 .field("emoji", &self.emoji)
2093 .field("highlight", &self.highlight)
2094 .field(
2095 "highlighter",
2096 &self.highlighter.as_ref().map(|_| "<Highlighter>"),
2097 )
2098 .field("width", &self.width)
2099 .field("height", &self.height)
2100 .field("safe_box", &self.safe_box)
2101 .field("theme", &self.theme.as_ref().map(|_| "<Theme>"))
2102 .field("file", &self.file.as_ref().map(|_| "<dyn Write>"))
2103 .finish()
2104 }
2105}
2106
2107impl ConsoleBuilder {
2108 #[must_use]
2110 pub fn color_system(mut self, system: ColorSystem) -> Self {
2111 self.color_system = Some(system);
2112 self
2113 }
2114
2115 #[must_use]
2117 pub fn no_color(mut self) -> Self {
2118 self.color_system = None;
2119 self
2120 }
2121
2122 #[must_use]
2124 pub fn force_terminal(mut self, force: bool) -> Self {
2125 self.force_terminal = Some(force);
2126 self
2127 }
2128
2129 #[must_use]
2131 pub fn tab_size(mut self, size: usize) -> Self {
2132 self.tab_size = Some(size);
2133 self
2134 }
2135
2136 #[must_use]
2138 pub fn markup(mut self, enabled: bool) -> Self {
2139 self.markup = Some(enabled);
2140 self
2141 }
2142
2143 #[must_use]
2145 pub fn emoji(mut self, enabled: bool) -> Self {
2146 self.emoji = Some(enabled);
2147 self
2148 }
2149
2150 #[must_use]
2152 pub fn highlight(mut self, enabled: bool) -> Self {
2153 self.highlight = Some(enabled);
2154 self
2155 }
2156
2157 #[must_use]
2159 pub fn highlighter<H: Highlighter + 'static>(mut self, highlighter: H) -> Self {
2160 self.highlighter = Some(Arc::new(highlighter));
2161 self
2162 }
2163
2164 #[must_use]
2166 pub fn width(mut self, width: usize) -> Self {
2167 self.width = Some(width);
2168 self
2169 }
2170
2171 #[must_use]
2173 pub fn height(mut self, height: usize) -> Self {
2174 self.height = Some(height);
2175 self
2176 }
2177
2178 #[must_use]
2180 pub fn safe_box(mut self, safe: bool) -> Self {
2181 self.safe_box = Some(safe);
2182 self
2183 }
2184
2185 #[must_use]
2187 pub fn theme(mut self, theme: Theme) -> Self {
2188 self.theme = Some(theme);
2189 self
2190 }
2191
2192 #[must_use]
2194 pub fn file(mut self, writer: Box<dyn Write + Send>) -> Self {
2195 self.file = Some(writer);
2196 self
2197 }
2198
2199 #[must_use]
2201 pub fn build(self) -> Console {
2202 let mut console = Console::new();
2203
2204 if let Some(cs) = self.color_system {
2205 console.color_system = Some(cs);
2206 }
2207 if let Some(ft) = self.force_terminal {
2208 console.force_terminal = Some(ft);
2209 if console.color_system.is_none() {
2210 console.detected_color_system = if ft {
2211 terminal::detect_color_system_forced(true)
2212 } else {
2213 None
2214 };
2215 }
2216 }
2217 if let Some(ts) = self.tab_size {
2218 console.tab_size = ts;
2219 }
2220 if let Some(m) = self.markup {
2221 console.markup = m;
2222 }
2223 if let Some(e) = self.emoji {
2224 console.emoji = e;
2225 }
2226 if let Some(h) = self.highlight {
2227 console.highlight = h;
2228 }
2229 if let Some(highlighter) = self.highlighter {
2230 console.highlighter = highlighter;
2231 }
2232 if let Some(w) = self.width {
2233 console.width = Some(w);
2234 }
2235 if let Some(h) = self.height {
2236 console.height = Some(h);
2237 }
2238 if let Some(sb) = self.safe_box {
2239 console.safe_box = sb;
2240 }
2241 if let Some(theme) = self.theme {
2242 console.theme_stack = Mutex::new(ThemeStack::new(theme));
2243 }
2244 if let Some(f) = self.file {
2245 console.file = Mutex::new(f);
2246 }
2247
2248 console
2249 }
2250}
2251
2252#[cfg(test)]
2253mod tests {
2254 use super::*;
2255 use crate::highlighter::NullHighlighter;
2256
2257 #[test]
2258 fn test_console_new() {
2259 let console = Console::new();
2260 assert!(console.width() > 0);
2261 assert!(console.height() > 0);
2262 }
2263
2264 #[test]
2265 fn test_console_builder() {
2266 let console = Console::builder()
2267 .width(100)
2268 .height(50)
2269 .markup(false)
2270 .build();
2271
2272 assert_eq!(console.width(), 100);
2273 assert_eq!(console.height(), 50);
2274 assert!(!console.markup);
2275 }
2276
2277 #[test]
2278 fn test_console_default_highlighter_applies_when_enabled() {
2279 let console = Console::builder().markup(false).build();
2280 let opts = PrintOptions::new().with_markup(false).with_no_newline(true);
2281 let segments = console.render_str_segments("True", &opts);
2282 let expected = console.get_style("repr.bool_true");
2283 assert!(segments.iter().any(|s| s.style.as_ref() == Some(&expected)));
2284 }
2285
2286 #[test]
2287 fn test_console_highlight_override_off_disables_highlighter() {
2288 let console = Console::builder().markup(false).build();
2289 let opts = PrintOptions::new()
2290 .with_markup(false)
2291 .with_no_newline(true)
2292 .with_highlight(false);
2293 let segments = console.render_str_segments("True", &opts);
2294 let expected = console.get_style("repr.bool_true");
2295 assert!(!segments.iter().any(|s| s.style.as_ref() == Some(&expected)));
2296 }
2297
2298 #[test]
2299 fn test_console_builder_highlighter_override() {
2300 let console = Console::builder()
2301 .markup(false)
2302 .highlighter(NullHighlighter)
2303 .build();
2304 let opts = PrintOptions::new().with_markup(false).with_no_newline(true);
2305 let segments = console.render_str_segments("True", &opts);
2306 let expected = console.get_style("repr.bool_true");
2307 assert!(!segments.iter().any(|s| s.style.as_ref() == Some(&expected)));
2308 }
2309
2310 #[test]
2311 fn test_console_print_options_highlighter_override() {
2312 let console = Console::builder().markup(false).build();
2313 let opts = PrintOptions::new()
2314 .with_markup(false)
2315 .with_no_newline(true)
2316 .with_highlight(true)
2317 .with_highlighter(NullHighlighter);
2318 let segments = console.render_str_segments("True", &opts);
2319 let expected = console.get_style("repr.bool_true");
2320 assert!(!segments.iter().any(|s| s.style.as_ref() == Some(&expected)));
2321 }
2322
2323 #[test]
2324 fn test_console_options() {
2325 let console = Console::builder().width(80).build();
2326 let options = console.options();
2327
2328 assert_eq!(options.max_width, 80);
2329 assert_eq!(options.size.width, 80);
2330 }
2331
2332 #[test]
2333 fn test_print_options() {
2334 let options = PrintOptions::new()
2335 .with_markup(true)
2336 .with_style(Style::new().bold());
2337
2338 assert_eq!(options.markup, Some(true));
2339 assert!(options.style.is_some());
2340 }
2341
2342 #[test]
2343 fn test_capture() {
2344 let console = Console::new();
2345 console.begin_capture();
2346
2347 console.print_plain("capture test");
2348 let segments = console.end_capture();
2349 let captured: String = segments.iter().map(|s| s.text.as_ref()).collect();
2350 assert!(captured.contains("capture test"));
2351 }
2352
2353 #[test]
2354 fn test_capture_collects_segments() {
2355 use std::sync::{Arc, Mutex};
2356
2357 #[derive(Clone)]
2358 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
2359
2360 impl Write for SharedBuffer {
2361 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2362 self.0.lock().unwrap().write(buf)
2363 }
2364 fn flush(&mut self) -> io::Result<()> {
2365 self.0.lock().unwrap().flush()
2366 }
2367 }
2368
2369 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
2370 let console = Console::builder()
2371 .width(40)
2372 .markup(false)
2373 .file(Box::new(buffer))
2374 .build();
2375
2376 console.begin_capture();
2377 console.print_plain("Hello");
2378 let segments = console.end_capture();
2379
2380 let captured: String = segments.iter().map(|s| s.text.as_ref()).collect();
2381 assert!(captured.contains("Hello"));
2382 }
2383
2384 #[test]
2385 fn test_print_exception_renders_traceback() {
2386 use crate::renderables::{Traceback, TracebackFrame};
2387
2388 let console = Console::builder().width(60).markup(false).build();
2389 console.begin_capture();
2390
2391 let traceback = Traceback::new(
2392 vec![
2393 TracebackFrame::new("<module>", 14),
2394 TracebackFrame::new("level1", 11),
2395 ],
2396 "ErrorType",
2397 "boom",
2398 );
2399
2400 console.print_exception(&traceback);
2401 let segments = console.end_capture();
2402 let captured: String = segments.iter().map(|s| s.text.as_ref()).collect();
2403
2404 assert!(captured.contains("Traceback (most recent call last)"));
2405 assert!(captured.contains("in <module>:14"));
2406 assert!(captured.contains("ErrorType: boom"));
2407 }
2408
2409 #[test]
2410 fn test_dimensions() {
2411 let dims = ConsoleDimensions::default();
2412 assert_eq!(dims.width, 80);
2413 assert_eq!(dims.height, 24);
2414 }
2415
2416 #[test]
2417 fn test_custom_output_stream() {
2418 use std::sync::{Arc, Mutex};
2419
2420 #[derive(Clone)]
2422 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
2423
2424 impl Write for SharedBuffer {
2425 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2426 self.0.lock().unwrap().write(buf)
2427 }
2428 fn flush(&mut self) -> io::Result<()> {
2429 self.0.lock().unwrap().flush()
2430 }
2431 }
2432
2433 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
2434 let console = Console::builder()
2435 .width(80)
2436 .markup(false)
2437 .file(Box::new(buffer.clone()))
2438 .build();
2439
2440 console.print_plain("Hello, World!");
2441
2442 let output = buffer.0.lock().unwrap();
2443 let text = String::from_utf8_lossy(&output);
2444 assert!(
2445 text.contains("Hello, World!"),
2446 "Expected 'Hello, World!' in output, got: {text}"
2447 );
2448 }
2449
2450 #[test]
2451 fn test_print_plain_disables_markup() {
2452 use std::sync::{Arc, Mutex};
2453
2454 #[derive(Clone)]
2455 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
2456
2457 impl Write for SharedBuffer {
2458 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2459 self.0.lock().unwrap().write(buf)
2460 }
2461 fn flush(&mut self) -> io::Result<()> {
2462 self.0.lock().unwrap().flush()
2463 }
2464 }
2465
2466 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
2467 let console = Console::builder()
2468 .markup(true)
2469 .file(Box::new(buffer.clone()))
2470 .build();
2471
2472 console.print_plain("[bold]Hello[/]");
2473
2474 let output = buffer.0.lock().unwrap();
2475 let text = String::from_utf8_lossy(&output);
2476 assert!(
2477 text.contains("[bold]Hello[/]"),
2478 "Expected literal markup in output, got: {text}"
2479 );
2480 assert!(
2481 !text.contains("\x1b["),
2482 "Did not expect ANSI sequences in output, got: {text}"
2483 );
2484 }
2485
2486 #[test]
2487 fn test_export_text_defaults() {
2488 let console = Console::builder().markup(true).build();
2489 let output = console.export_text("[bold]Hello[/]");
2490 assert_eq!(output, "Hello\n");
2491 }
2492
2493 #[test]
2494 fn test_export_text_respects_markup_setting() {
2495 let console = Console::builder().markup(false).build();
2496 let output = console.export_text("[bold]Hello[/]");
2497 assert_eq!(output, "[bold]Hello[/]\n");
2498 }
2499
2500 #[test]
2501 fn test_export_text_replaces_emoji_codes_by_default() {
2502 let console = Console::builder().markup(false).build();
2503 let output = console.export_text("hi :smile:");
2504 assert_eq!(output, "hi π\n");
2505 }
2506
2507 #[test]
2508 fn test_export_text_does_not_replace_emoji_codes_when_disabled() {
2509 let console = Console::builder().markup(false).emoji(false).build();
2510 let output = console.export_text("hi :smile:");
2511 assert_eq!(output, "hi :smile:\n");
2512 }
2513
2514 #[test]
2515 fn test_export_text_with_options_no_newline() {
2516 let console = Console::new();
2517 let mut options = PrintOptions::new().with_markup(false);
2518 options.no_newline = true;
2519 let output = console.export_text_with_options("Hello", &options);
2520 assert_eq!(output, "Hello");
2521 }
2522
2523 #[test]
2524 fn test_export_renderable_text() {
2525 use crate::renderables::Rule;
2526
2527 let console = Console::builder().width(20).build();
2528 let rule = Rule::with_title("Title");
2529 let output = console.export_renderable_text(&rule);
2530 assert!(output.contains("Title"));
2531 assert!(output.ends_with('\n'));
2532 }
2533
2534 #[test]
2535 fn test_export_html_svg_capture() {
2536 use std::sync::{Arc, Mutex};
2537
2538 #[derive(Clone)]
2539 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
2540
2541 impl Write for SharedBuffer {
2542 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2543 self.0.lock().unwrap().write(buf)
2544 }
2545 fn flush(&mut self) -> io::Result<()> {
2546 self.0.lock().unwrap().flush()
2547 }
2548 }
2549
2550 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
2551 let console = Console::builder()
2552 .markup(false)
2553 .file(Box::new(buffer))
2554 .build();
2555
2556 console.begin_capture();
2557 console.print_plain("Hello");
2558
2559 let html = console.export_html(false);
2560 assert!(html.contains("<pre"));
2561 assert!(html.contains("Hello"));
2562
2563 let svg = console.export_svg(true);
2564 assert!(svg.contains("<svg"));
2565 assert!(svg.contains("Hello"));
2566
2567 let cleared = console.export_html(false);
2568 assert!(!cleared.contains("Hello"));
2569 }
2570
2571 #[test]
2572 fn test_escape_html_entities() {
2573 let escaped = escape_html_rich("<>&\"'");
2574 assert_eq!(escaped, "<>&"'");
2575 }
2576
2577 #[test]
2578 fn test_style_html_rule_basic_attributes() {
2579 use crate::color::Color;
2580
2581 let style = Style::new()
2582 .color(Color::from_rgb(255, 0, 0))
2583 .bgcolor(Color::from_rgb(0, 0, 255))
2584 .bold()
2585 .italic()
2586 .underline()
2587 .strike();
2588 let css = style.get_html_style(DEFAULT_TERMINAL_THEME);
2589
2590 assert!(css.contains("color: #ff0000"));
2591 assert!(css.contains("background-color: #0000ff"));
2592 assert!(css.contains("font-weight: bold"));
2593 assert!(css.contains("font-style: italic"));
2594 assert!(css.contains("text-decoration: underline"));
2595 assert!(css.contains("text-decoration: line-through"));
2596 }
2597
2598 #[test]
2599 fn test_style_html_rule_reverse_swaps_colors() {
2600 use crate::color::Color;
2601
2602 let style = Style::new()
2603 .color(Color::from_rgb(10, 20, 30))
2604 .bgcolor(Color::from_rgb(200, 210, 220))
2605 .reverse();
2606 let css = style.get_html_style(DEFAULT_TERMINAL_THEME);
2607
2608 assert!(css.contains("color: #c8d2dc"));
2609 assert!(css.contains("background-color: #0a141e"));
2610 }
2611
2612 #[test]
2613 fn test_export_html_body_links_and_spans() {
2614 let link_style = Style::new().link("https://example.com").bold();
2615 let segments = vec![
2616 Segment::new("Link", Some(link_style)),
2617 Segment::new(" ", None),
2618 Segment::new("Plain", None),
2619 ];
2620
2621 let opts = ExportHtmlOptions {
2622 inline_styles: true,
2623 code_format: Some("{code}".to_string()),
2624 ..ExportHtmlOptions::default()
2625 };
2626 let html = export_segments_to_html_rich(&segments, &opts);
2627 assert!(html.contains("href=\"https://example.com\""));
2628 assert!(html.contains("font-weight: bold"));
2629 assert!(html.contains("Plain"));
2630 }
2631
2632 #[test]
2633 fn test_export_html_escapes_text() {
2634 let segments = vec![Segment::plain("<tag> & \"quote\"")];
2635 let opts = ExportHtmlOptions {
2636 inline_styles: true,
2637 code_format: Some("{code}".to_string()),
2638 ..ExportHtmlOptions::default()
2639 };
2640 let html = export_segments_to_html_rich(&segments, &opts);
2641 assert!(html.contains("<tag>"));
2642 assert!(html.contains("&"));
2643 assert!(html.contains("""));
2644 }
2645
2646 #[test]
2647 fn test_export_html_skips_control_segments() {
2648 use crate::segment::{ControlCode, ControlType};
2649
2650 let segments = vec![
2651 Segment::control(vec![ControlCode::new(ControlType::Bell)]),
2652 Segment::new("Hi", None),
2653 ];
2654 let opts = ExportHtmlOptions {
2655 inline_styles: true,
2656 code_format: Some("{code}".to_string()),
2657 ..ExportHtmlOptions::default()
2658 };
2659 let html = export_segments_to_html_rich(&segments, &opts);
2660 assert!(html.contains("Hi"));
2661 assert!(!html.contains("Bell"));
2662 }
2663
2664 #[test]
2665 fn test_export_svg_dimensions() {
2666 let segments = vec![Segment::plain("AB"), Segment::line(), Segment::plain("C")];
2667 let opts = ExportSvgOptions {
2668 code_format: Some("{width}x{height}".to_string()),
2669 ..ExportSvgOptions::default()
2670 };
2671 let svg = export_segments_to_svg_rich(&segments, 2, &opts);
2672 assert!(svg.contains('x'));
2673 }
2674
2675 #[test]
2676 fn test_export_svg_includes_text() {
2677 let segments = vec![Segment::plain("Hello")];
2678 let opts = ExportSvgOptions {
2679 code_format: Some("{matrix}".to_string()),
2680 ..ExportSvgOptions::default()
2681 };
2682 let svg = export_segments_to_svg_rich(&segments, 10, &opts);
2683 assert!(svg.contains("Hello"));
2684 }
2685
2686 #[test]
2687 fn test_export_html_document_structure() {
2688 let segments = vec![Segment::plain("Hello")];
2689 let opts = ExportHtmlOptions::default();
2690 let html = export_segments_to_html_rich(&segments, &opts);
2691 assert!(html.starts_with("<!DOCTYPE html>"));
2692 assert!(html.contains("<meta charset=\"UTF-8\">"));
2693 assert!(html.contains("<body>"));
2694 assert!(html.contains("</html>"));
2695 }
2696
2697 #[test]
2698 fn test_export_html_includes_renderable_content() {
2699 use crate::renderables::{Column, Panel, Table, Tree, TreeNode};
2700
2701 let console = Console::builder().width(30).build();
2702 console.begin_capture();
2703
2704 let mut table = Table::new().with_column(Column::new("Col"));
2705 table.add_row_cells(["Cell"]);
2706 console.print_renderable(&table);
2707
2708 let panel = Panel::from_text("Panel").width(10);
2709 console.print_renderable(&panel);
2710
2711 let root = TreeNode::new("Root").child(TreeNode::new("Leaf"));
2712 let tree = Tree::new(root);
2713 console.print_renderable(&tree);
2714
2715 let html = console.export_html(true);
2716 assert!(html.contains("Col"));
2717 assert!(html.contains("Cell"));
2718 assert!(html.contains("Panel"));
2719 assert!(html.contains("Root"));
2720 assert!(html.contains("Leaf"));
2721 }
2722
2723 #[test]
2724 fn test_print_options_justify_uses_console_width() {
2725 let console = Console::builder().width(10).markup(false).build();
2726 let mut output = Vec::new();
2727 let mut options = PrintOptions::new().with_justify(JustifyMethod::Center);
2728 options.no_newline = true;
2729
2730 console
2731 .print_to(&mut output, "Hi", &options)
2732 .expect("failed to render");
2733
2734 let text = String::from_utf8(output).expect("invalid utf8");
2735 assert_eq!(text, " Hi ");
2736 }
2737
2738 #[test]
2739 fn test_print_options_width_wraps() {
2740 let console = Console::builder().width(80).markup(false).build();
2741 let mut output = Vec::new();
2742 let mut options = PrintOptions::new();
2743 options.width = Some(4);
2744
2745 console
2746 .print_to(&mut output, "Hello", &options)
2747 .expect("failed to render");
2748
2749 let text = String::from_utf8(output).expect("invalid utf8");
2750 assert_eq!(text, "Hell\no\n");
2751 }
2752
2753 #[test]
2754 fn test_print_options_no_wrap_ellipsis() {
2755 let console = Console::builder().width(80).markup(false).build();
2756 let mut output = Vec::new();
2757 let mut options = PrintOptions::new()
2758 .with_no_wrap(true)
2759 .with_overflow(OverflowMethod::Ellipsis);
2760 options.width = Some(4);
2761 options.no_newline = true;
2762
2763 console
2764 .print_to(&mut output, "Hello", &options)
2765 .expect("failed to render");
2766
2767 let text = String::from_utf8(output).expect("invalid utf8");
2768 assert_eq!(text, "H...");
2769 }
2770
2771 #[test]
2772 fn test_custom_output_stream_line() {
2773 use std::sync::{Arc, Mutex};
2774
2775 #[derive(Clone)]
2776 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
2777
2778 impl Write for SharedBuffer {
2779 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
2780 self.0.lock().unwrap().write(buf)
2781 }
2782 fn flush(&mut self) -> io::Result<()> {
2783 self.0.lock().unwrap().flush()
2784 }
2785 }
2786
2787 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
2788 let console = Console::builder()
2789 .width(80)
2790 .file(Box::new(buffer.clone()))
2791 .build();
2792
2793 console.line();
2794
2795 let output = buffer.0.lock().unwrap();
2796 let text = String::from_utf8_lossy(&output);
2797 assert_eq!(text, "\n", "Expected single newline, got: {text:?}");
2798 }
2799
2800 #[test]
2803 fn test_console_builder_color_system() {
2804 let console = Console::builder()
2805 .color_system(ColorSystem::TrueColor)
2806 .build();
2807 assert_eq!(console.color_system(), Some(ColorSystem::TrueColor));
2808 }
2809
2810 #[test]
2811 fn test_console_builder_no_color() {
2812 let console = Console::builder().no_color().build();
2813 assert_eq!(console.color_system, None);
2814 }
2815
2816 #[test]
2817 fn test_console_builder_force_terminal() {
2818 let console = Console::builder().force_terminal(true).build();
2819 assert!(console.is_terminal());
2820 }
2821
2822 #[test]
2823 fn test_console_builder_tab_size() {
2824 let console = Console::builder().tab_size(4).build();
2825 assert_eq!(console.tab_size(), 4);
2826 }
2827
2828 #[test]
2829 fn test_console_builder_emoji() {
2830 let console = Console::builder().emoji(false).build();
2831 assert!(!console.emoji);
2832 }
2833
2834 #[test]
2835 fn test_console_builder_highlight() {
2836 let console = Console::builder().highlight(false).build();
2837 assert!(!console.highlight);
2838 }
2839
2840 #[test]
2841 fn test_console_builder_safe_box() {
2842 let console = Console::builder().safe_box(true).build();
2843 assert!(console.safe_box);
2844 }
2845
2846 #[test]
2847 fn test_console_builder_all_options() {
2848 let console = Console::builder()
2849 .color_system(ColorSystem::EightBit)
2850 .force_terminal(true)
2851 .tab_size(2)
2852 .markup(false)
2853 .emoji(false)
2854 .highlight(false)
2855 .width(120)
2856 .height(40)
2857 .safe_box(true)
2858 .build();
2859
2860 assert_eq!(console.color_system(), Some(ColorSystem::EightBit));
2861 assert!(console.is_terminal());
2862 assert_eq!(console.tab_size(), 2);
2863 assert!(!console.markup);
2864 assert!(!console.emoji);
2865 assert!(!console.highlight);
2866 assert_eq!(console.width(), 120);
2867 assert_eq!(console.height(), 40);
2868 assert!(console.safe_box);
2869 }
2870
2871 #[test]
2874 fn test_console_size_returns_dimensions() {
2875 let console = Console::builder().width(100).height(50).build();
2876 let size = console.size();
2877 assert_eq!(size.width, 100);
2878 assert_eq!(size.height, 50);
2879 }
2880
2881 #[test]
2882 fn test_console_default_dimensions() {
2883 let console = Console::new();
2884 assert!(console.width() >= 40);
2886 assert!(console.height() >= 10);
2887 }
2888
2889 #[test]
2892 fn test_print_options_default() {
2893 let options = PrintOptions::new();
2894 assert_eq!(options.markup, None);
2895 assert!(options.style.is_none());
2896 assert_eq!(options.sep, " ");
2897 assert_eq!(options.end, "\n");
2898 assert_eq!(options.no_wrap, None);
2899 assert!(!options.no_newline);
2900 assert_eq!(options.highlight, None);
2901 }
2902
2903 #[test]
2904 fn test_print_options_with_sep() {
2905 let options = PrintOptions::new().with_sep(", ");
2906 assert_eq!(options.sep, ", ");
2907 }
2908
2909 #[test]
2910 fn test_print_options_with_end() {
2911 let options = PrintOptions::new().with_end("\r\n");
2912 assert_eq!(options.end, "\r\n");
2913 }
2914
2915 #[test]
2916 fn test_print_options_with_overflow() {
2917 let options = PrintOptions::new().with_overflow(OverflowMethod::Crop);
2918 assert_eq!(options.overflow, Some(OverflowMethod::Crop));
2919 }
2920
2921 #[test]
2922 fn test_print_options_with_crop() {
2923 let options = PrintOptions::new().with_crop(true);
2924 assert!(options.crop);
2925 }
2926
2927 #[test]
2928 fn test_print_options_with_soft_wrap() {
2929 let options = PrintOptions::new().with_soft_wrap(true);
2930 assert!(options.soft_wrap);
2931 }
2932
2933 #[test]
2934 fn test_print_options_chained() {
2935 let style = Style::new().bold().italic();
2936 let options = PrintOptions::new()
2937 .with_markup(false)
2938 .with_style(style.clone())
2939 .with_sep(" | ")
2940 .with_end("")
2941 .with_justify(JustifyMethod::Right)
2942 .with_overflow(OverflowMethod::Ellipsis)
2943 .with_no_wrap(true)
2944 .with_no_newline(true)
2945 .with_highlight(true)
2946 .with_width(40)
2947 .with_crop(true)
2948 .with_soft_wrap(true);
2949
2950 assert_eq!(options.markup, Some(false));
2951 assert!(options.style.is_some());
2952 assert_eq!(options.sep, " | ");
2953 assert_eq!(options.end, "");
2954 assert_eq!(options.justify, Some(JustifyMethod::Right));
2955 assert_eq!(options.overflow, Some(OverflowMethod::Ellipsis));
2956 assert_eq!(options.no_wrap, Some(true));
2957 assert!(options.no_newline);
2958 assert_eq!(options.highlight, Some(true));
2959 assert_eq!(options.width, Some(40));
2960 assert!(options.crop);
2961 assert!(options.soft_wrap);
2962 }
2963
2964 #[test]
2967 fn test_console_options_update_width() {
2968 let console = Console::builder().width(100).build();
2969 let options = console.options();
2970 let updated = options.update_width(80);
2972 assert_eq!(updated.max_width, 80);
2973 }
2974
2975 #[test]
2976 fn test_console_options_update_height() {
2977 let console = Console::builder().height(24).build();
2978 let options = console.options();
2979 let updated = options.update_height(50);
2981 assert_eq!(updated.height, Some(50));
2982 }
2983
2984 #[test]
2987 fn test_console_is_color_enabled_with_system() {
2988 let console = Console::builder()
2989 .color_system(ColorSystem::Standard)
2990 .build();
2991 assert!(console.is_color_enabled());
2992 }
2993
2994 #[test]
2995 fn test_console_is_color_enabled_no_color() {
2996 let console = Console::builder().no_color().build();
2997 assert!(!console.is_color_enabled());
2998 }
2999
3000 #[test]
3003 fn test_capture_empty() {
3004 let console = Console::new();
3005 console.begin_capture();
3006 let segments = console.end_capture();
3007 assert_eq!(segments.len(), 0);
3008 }
3009
3010 #[test]
3011 fn test_capture_with_styled_text() {
3012 use std::sync::{Arc, Mutex};
3013
3014 #[derive(Clone)]
3015 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3016
3017 impl Write for SharedBuffer {
3018 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3019 self.0.lock().unwrap().write(buf)
3020 }
3021 fn flush(&mut self) -> io::Result<()> {
3022 self.0.lock().unwrap().flush()
3023 }
3024 }
3025
3026 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3027 let console = Console::builder()
3028 .width(80)
3029 .markup(true)
3030 .color_system(ColorSystem::TrueColor)
3031 .file(Box::new(buffer))
3032 .build();
3033
3034 console.begin_capture();
3035 console.print("[bold]Test[/]");
3036 let segments = console.end_capture();
3037
3038 assert_ne!(segments.len(), 0);
3040 }
3041
3042 #[test]
3043 fn test_capture_multiple_prints() {
3044 use std::sync::{Arc, Mutex};
3045
3046 #[derive(Clone)]
3047 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3048
3049 impl Write for SharedBuffer {
3050 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3051 self.0.lock().unwrap().write(buf)
3052 }
3053 fn flush(&mut self) -> io::Result<()> {
3054 self.0.lock().unwrap().flush()
3055 }
3056 }
3057
3058 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3059 let console = Console::builder()
3060 .width(80)
3061 .markup(false)
3062 .file(Box::new(buffer))
3063 .build();
3064
3065 console.begin_capture();
3066 console.print_plain("First");
3067 console.print_plain("Second");
3068 let segments = console.end_capture();
3069
3070 let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
3071 assert!(text.contains("First"));
3072 assert!(text.contains("Second"));
3073 }
3074
3075 #[test]
3078 fn test_print_text_direct() {
3079 use std::sync::{Arc, Mutex};
3080
3081 #[derive(Clone)]
3082 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3083
3084 impl Write for SharedBuffer {
3085 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3086 self.0.lock().unwrap().write(buf)
3087 }
3088 fn flush(&mut self) -> io::Result<()> {
3089 self.0.lock().unwrap().flush()
3090 }
3091 }
3092
3093 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3094 let console = Console::builder()
3095 .width(80)
3096 .markup(false)
3097 .file(Box::new(buffer.clone()))
3098 .build();
3099
3100 let text = Text::new("Direct text");
3101 console.print_text(&text);
3102
3103 let output = buffer.0.lock().unwrap();
3104 let result = String::from_utf8_lossy(&output);
3105 assert!(result.contains("Direct text"));
3106 }
3107
3108 #[test]
3109 fn test_print_styled() {
3110 use std::sync::{Arc, Mutex};
3111
3112 #[derive(Clone)]
3113 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3114
3115 impl Write for SharedBuffer {
3116 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3117 self.0.lock().unwrap().write(buf)
3118 }
3119 fn flush(&mut self) -> io::Result<()> {
3120 self.0.lock().unwrap().flush()
3121 }
3122 }
3123
3124 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3125 let console = Console::builder()
3126 .width(80)
3127 .color_system(ColorSystem::TrueColor)
3128 .file(Box::new(buffer.clone()))
3129 .build();
3130
3131 console.print_styled("Styled", Style::new().bold());
3132
3133 let output = buffer.0.lock().unwrap();
3134 let result = String::from_utf8_lossy(&output);
3135 assert!(result.contains("Styled"));
3136 assert!(result.contains("\x1b["));
3138 }
3139
3140 #[test]
3141 fn test_print_to_writer() {
3142 let console = Console::builder().width(80).markup(false).build();
3143 let mut output = Vec::new();
3144 let options = PrintOptions::new();
3145
3146 console
3147 .print_to(&mut output, "Writer test", &options)
3148 .expect("failed to print");
3149
3150 let text = String::from_utf8(output).expect("invalid utf8");
3151 assert!(text.contains("Writer test"));
3152 }
3153
3154 #[test]
3155 fn test_print_segments() {
3156 use std::sync::{Arc, Mutex};
3157
3158 #[derive(Clone)]
3159 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3160
3161 impl Write for SharedBuffer {
3162 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3163 self.0.lock().unwrap().write(buf)
3164 }
3165 fn flush(&mut self) -> io::Result<()> {
3166 self.0.lock().unwrap().flush()
3167 }
3168 }
3169
3170 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3171 let console = Console::builder()
3172 .width(80)
3173 .file(Box::new(buffer.clone()))
3174 .build();
3175
3176 let segments = vec![Segment::plain("Hello "), Segment::plain("World")];
3177 console.print_segments(&segments);
3178
3179 let output = buffer.0.lock().unwrap();
3180 let result = String::from_utf8_lossy(&output);
3181 assert!(result.contains("Hello "));
3182 assert!(result.contains("World"));
3183 }
3184
3185 #[test]
3188 fn test_rule_without_title() {
3189 use std::sync::{Arc, Mutex};
3190
3191 #[derive(Clone)]
3192 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3193
3194 impl Write for SharedBuffer {
3195 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3196 self.0.lock().unwrap().write(buf)
3197 }
3198 fn flush(&mut self) -> io::Result<()> {
3199 self.0.lock().unwrap().flush()
3200 }
3201 }
3202
3203 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3204 let console = Console::builder()
3205 .width(20)
3206 .file(Box::new(buffer.clone()))
3207 .build();
3208
3209 console.rule(None);
3210
3211 let output = buffer.0.lock().unwrap();
3212 let result = String::from_utf8_lossy(&output);
3213 assert!(result.contains('β') || result.contains('-'));
3215 }
3216
3217 #[test]
3218 fn test_rule_with_title() {
3219 use std::sync::{Arc, Mutex};
3220
3221 #[derive(Clone)]
3222 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3223
3224 impl Write for SharedBuffer {
3225 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3226 self.0.lock().unwrap().write(buf)
3227 }
3228 fn flush(&mut self) -> io::Result<()> {
3229 self.0.lock().unwrap().flush()
3230 }
3231 }
3232
3233 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3234 let console = Console::builder()
3235 .width(40)
3236 .file(Box::new(buffer.clone()))
3237 .build();
3238
3239 console.rule(Some("Section"));
3240
3241 let output = buffer.0.lock().unwrap();
3242 let result = String::from_utf8_lossy(&output);
3243 assert!(result.contains("Section"));
3244 }
3245
3246 #[test]
3249 fn test_log_debug() {
3250 use std::sync::{Arc, Mutex};
3251
3252 #[derive(Clone)]
3253 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3254
3255 impl Write for SharedBuffer {
3256 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3257 self.0.lock().unwrap().write(buf)
3258 }
3259 fn flush(&mut self) -> io::Result<()> {
3260 self.0.lock().unwrap().flush()
3261 }
3262 }
3263
3264 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3265 let console = Console::builder()
3266 .width(80)
3267 .file(Box::new(buffer.clone()))
3268 .build();
3269
3270 console.log("Debug message", LogLevel::Debug);
3271
3272 let output = buffer.0.lock().unwrap();
3273 let result = String::from_utf8_lossy(&output);
3274 assert!(result.contains("Debug message"));
3275 }
3276
3277 #[test]
3278 fn test_log_info() {
3279 use std::sync::{Arc, Mutex};
3280
3281 #[derive(Clone)]
3282 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3283
3284 impl Write for SharedBuffer {
3285 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3286 self.0.lock().unwrap().write(buf)
3287 }
3288 fn flush(&mut self) -> io::Result<()> {
3289 self.0.lock().unwrap().flush()
3290 }
3291 }
3292
3293 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3294 let console = Console::builder()
3295 .width(80)
3296 .file(Box::new(buffer.clone()))
3297 .build();
3298
3299 console.log("Info message", LogLevel::Info);
3300
3301 let output = buffer.0.lock().unwrap();
3302 let result = String::from_utf8_lossy(&output);
3303 assert!(result.contains("Info message"));
3304 }
3305
3306 #[test]
3307 fn test_log_warning() {
3308 use std::sync::{Arc, Mutex};
3309
3310 #[derive(Clone)]
3311 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3312
3313 impl Write for SharedBuffer {
3314 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3315 self.0.lock().unwrap().write(buf)
3316 }
3317 fn flush(&mut self) -> io::Result<()> {
3318 self.0.lock().unwrap().flush()
3319 }
3320 }
3321
3322 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3323 let console = Console::builder()
3324 .width(80)
3325 .file(Box::new(buffer.clone()))
3326 .build();
3327
3328 console.log("Warning message", LogLevel::Warning);
3329
3330 let output = buffer.0.lock().unwrap();
3331 let result = String::from_utf8_lossy(&output);
3332 assert!(result.contains("Warning message"));
3333 }
3334
3335 #[test]
3336 fn test_log_error() {
3337 use std::sync::{Arc, Mutex};
3338
3339 #[derive(Clone)]
3340 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3341
3342 impl Write for SharedBuffer {
3343 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3344 self.0.lock().unwrap().write(buf)
3345 }
3346 fn flush(&mut self) -> io::Result<()> {
3347 self.0.lock().unwrap().flush()
3348 }
3349 }
3350
3351 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3352 let console = Console::builder()
3353 .width(80)
3354 .file(Box::new(buffer.clone()))
3355 .build();
3356
3357 console.log("Error message", LogLevel::Error);
3358
3359 let output = buffer.0.lock().unwrap();
3360 let result = String::from_utf8_lossy(&output);
3361 assert!(result.contains("Error message"));
3362 }
3363
3364 #[test]
3367 fn test_log_with_timestamp() {
3368 use std::sync::{Arc, Mutex};
3369
3370 #[derive(Clone)]
3371 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3372
3373 impl Write for SharedBuffer {
3374 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3375 self.0.lock().unwrap().write(buf)
3376 }
3377 fn flush(&mut self) -> io::Result<()> {
3378 self.0.lock().unwrap().flush()
3379 }
3380 }
3381
3382 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3383 let console = Console::builder()
3384 .width(80)
3385 .file(Box::new(buffer.clone()))
3386 .build();
3387
3388 let opts = LogOptions::new().with_timestamp(true);
3389 console.log_with_options("Test message", LogLevel::Info, &opts);
3390
3391 let output = buffer.0.lock().unwrap();
3392 let result = String::from_utf8_lossy(&output);
3393 assert!(result.contains('['));
3395 assert!(result.contains(']'));
3396 assert!(result.contains(':'));
3397 assert!(result.contains("Test message"));
3398 }
3399
3400 #[test]
3401 fn test_log_with_file_path() {
3402 use std::sync::{Arc, Mutex};
3403
3404 #[derive(Clone)]
3405 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3406
3407 impl Write for SharedBuffer {
3408 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3409 self.0.lock().unwrap().write(buf)
3410 }
3411 fn flush(&mut self) -> io::Result<()> {
3412 self.0.lock().unwrap().flush()
3413 }
3414 }
3415
3416 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3417 let console = Console::builder()
3418 .width(80)
3419 .file(Box::new(buffer.clone()))
3420 .build();
3421
3422 let opts = LogOptions::new().with_path("src/main.rs", 42);
3423 console.log_with_options("Debug info", LogLevel::Debug, &opts);
3424
3425 let output = buffer.0.lock().unwrap();
3426 let result = String::from_utf8_lossy(&output);
3427 assert!(result.contains("src/main.rs"));
3428 assert!(result.contains("42"));
3429 assert!(result.contains("Debug info"));
3430 }
3431
3432 #[test]
3433 fn test_log_with_timestamp_and_path() {
3434 use std::sync::{Arc, Mutex};
3435
3436 #[derive(Clone)]
3437 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3438
3439 impl Write for SharedBuffer {
3440 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3441 self.0.lock().unwrap().write(buf)
3442 }
3443 fn flush(&mut self) -> io::Result<()> {
3444 self.0.lock().unwrap().flush()
3445 }
3446 }
3447
3448 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3449 let console = Console::builder()
3450 .width(80)
3451 .file(Box::new(buffer.clone()))
3452 .build();
3453
3454 let opts = LogOptions::new()
3455 .with_timestamp(true)
3456 .with_path("test.rs", 100);
3457 console.log_with_options("Combined test", LogLevel::Warning, &opts);
3458
3459 let output = buffer.0.lock().unwrap();
3460 let result = String::from_utf8_lossy(&output);
3461 assert!(result.contains('[')); assert!(result.contains("test.rs"));
3463 assert!(result.contains("100"));
3464 assert!(result.contains("Combined test"));
3465 }
3466
3467 #[test]
3468 fn test_log_without_level() {
3469 use std::sync::{Arc, Mutex};
3470
3471 #[derive(Clone)]
3472 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3473
3474 impl Write for SharedBuffer {
3475 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3476 self.0.lock().unwrap().write(buf)
3477 }
3478 fn flush(&mut self) -> io::Result<()> {
3479 self.0.lock().unwrap().flush()
3480 }
3481 }
3482
3483 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3484 let console = Console::builder()
3485 .width(80)
3486 .file(Box::new(buffer.clone()))
3487 .build();
3488
3489 let opts = LogOptions::new().with_level(false);
3490 console.log_with_options("No level prefix", LogLevel::Info, &opts);
3491
3492 let output = buffer.0.lock().unwrap();
3493 let result = String::from_utf8_lossy(&output);
3494 assert!(!result.contains("[INFO]"));
3495 assert!(result.contains("No level prefix"));
3496 }
3497
3498 #[test]
3499 fn test_log_options_default() {
3500 let opts = LogOptions::default();
3501 assert!(!opts.show_timestamp);
3502 assert!(opts.timestamp_format.is_none());
3503 assert!(opts.file_path.is_none());
3504 assert!(opts.line_number.is_none());
3505 assert!(opts.show_level);
3506 assert!(!opts.highlight);
3507 }
3508
3509 #[test]
3510 fn test_log_options_builder() {
3511 let opts = LogOptions::new()
3512 .with_timestamp(true)
3513 .with_timestamp_format("%Y-%m-%d %H:%M:%S")
3514 .with_file("test.rs")
3515 .with_line(123)
3516 .with_level(false)
3517 .with_highlight(true);
3518
3519 assert!(opts.show_timestamp);
3520 assert_eq!(opts.timestamp_format, Some("%Y-%m-%d %H:%M:%S".to_string()));
3521 assert_eq!(opts.file_path, Some("test.rs".to_string()));
3522 assert_eq!(opts.line_number, Some(123));
3523 assert!(!opts.show_level);
3524 assert!(opts.highlight);
3525 }
3526
3527 #[test]
3528 fn test_format_timestamp_default() {
3529 let ts = Console::format_timestamp(None);
3530 assert!(ts.starts_with('['));
3532 assert!(ts.ends_with(']'));
3533 assert_eq!(ts.matches(':').count(), 2);
3534 }
3535
3536 #[test]
3537 fn test_format_timestamp_custom() {
3538 let ts = Console::format_timestamp(Some("%H-%M-%S"));
3539 assert_eq!(ts.matches('-').count(), 2);
3541 assert!(!ts.contains(':'));
3542 }
3543
3544 #[test]
3545 fn test_format_timestamp_custom_with_date_tokens() {
3546 let ts = Console::format_timestamp(Some("%Y-%m-%d %H:%M:%S"));
3547 assert_eq!(ts.len(), "0000-00-00 00:00:00".len());
3549 assert_eq!(ts.matches('-').count(), 2);
3550 assert_eq!(ts.matches(':').count(), 2);
3551 }
3552
3553 #[test]
3556 fn test_markup_enabled() {
3557 use std::sync::{Arc, Mutex};
3558
3559 #[derive(Clone)]
3560 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3561
3562 impl Write for SharedBuffer {
3563 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3564 self.0.lock().unwrap().write(buf)
3565 }
3566 fn flush(&mut self) -> io::Result<()> {
3567 self.0.lock().unwrap().flush()
3568 }
3569 }
3570
3571 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3572 let console = Console::builder()
3573 .width(80)
3574 .markup(true)
3575 .color_system(ColorSystem::TrueColor)
3576 .file(Box::new(buffer.clone()))
3577 .build();
3578
3579 console.print("[bold]Bold text[/]");
3580
3581 let output = buffer.0.lock().unwrap();
3582 let result = String::from_utf8_lossy(&output);
3583 assert!(!result.contains("[bold]"));
3585 assert!(result.contains("\x1b["));
3586 }
3587
3588 #[test]
3589 fn test_markup_disabled() {
3590 use std::sync::{Arc, Mutex};
3591
3592 #[derive(Clone)]
3593 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3594
3595 impl Write for SharedBuffer {
3596 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3597 self.0.lock().unwrap().write(buf)
3598 }
3599 fn flush(&mut self) -> io::Result<()> {
3600 self.0.lock().unwrap().flush()
3601 }
3602 }
3603
3604 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3605 let console = Console::builder()
3606 .width(80)
3607 .markup(false)
3608 .file(Box::new(buffer.clone()))
3609 .build();
3610
3611 console.print("[bold]Literal markup[/]");
3612
3613 let output = buffer.0.lock().unwrap();
3614 let result = String::from_utf8_lossy(&output);
3615 assert!(result.contains("[bold]"));
3617 }
3618
3619 #[test]
3622 fn test_print_with_width_constraint() {
3623 let console = Console::builder().width(80).markup(false).build();
3624 let mut output = Vec::new();
3625 let mut options = PrintOptions::new();
3626 options.width = Some(10);
3627
3628 console
3629 .print_to(
3630 &mut output,
3631 "This is a long text that should wrap",
3632 &options,
3633 )
3634 .expect("failed to print");
3635
3636 let text = String::from_utf8(output).expect("invalid utf8");
3637 let lines: Vec<&str> = text.lines().collect();
3639 assert!(lines.len() > 1);
3640 }
3641
3642 #[test]
3643 fn test_justify_left() {
3644 let console = Console::builder().width(20).markup(false).build();
3645 let mut output = Vec::new();
3646 let mut options = PrintOptions::new().with_justify(JustifyMethod::Left);
3647 options.no_newline = true;
3648
3649 console
3650 .print_to(&mut output, "Left", &options)
3651 .expect("failed to print");
3652
3653 let text = String::from_utf8(output).expect("invalid utf8");
3654 assert!(text.starts_with("Left"));
3655 }
3656
3657 #[test]
3658 fn test_justify_right() {
3659 let console = Console::builder().width(20).markup(false).build();
3660 let mut output = Vec::new();
3661 let mut options = PrintOptions::new().with_justify(JustifyMethod::Right);
3662 options.no_newline = true;
3663
3664 console
3665 .print_to(&mut output, "Right", &options)
3666 .expect("failed to print");
3667
3668 let text = String::from_utf8(output).expect("invalid utf8");
3669 assert!(text.ends_with("Right"));
3670 assert_eq!(text.len(), 20);
3671 }
3672
3673 #[test]
3676 fn test_console_dimensions_default() {
3677 let dims = ConsoleDimensions::default();
3678 assert_eq!(dims.width, 80);
3679 assert_eq!(dims.height, 24);
3680 }
3681
3682 #[test]
3683 fn test_console_dimensions_custom() {
3684 let dims = ConsoleDimensions {
3685 width: 120,
3686 height: 40,
3687 };
3688 assert_eq!(dims.width, 120);
3689 assert_eq!(dims.height, 40);
3690 }
3691
3692 #[test]
3695 fn test_print_options_implements_default() {
3696 let default_options = PrintOptions::default();
3699 assert_eq!(default_options.sep, "");
3700 assert_eq!(default_options.end, "");
3701
3702 let new_options = PrintOptions::new();
3704 assert_eq!(new_options.sep, " ");
3705 assert_eq!(new_options.end, "\n");
3706 }
3707
3708 #[test]
3711 fn test_print_empty_string() {
3712 use std::sync::{Arc, Mutex};
3713
3714 #[derive(Clone)]
3715 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3716
3717 impl Write for SharedBuffer {
3718 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3719 self.0.lock().unwrap().write(buf)
3720 }
3721 fn flush(&mut self) -> io::Result<()> {
3722 self.0.lock().unwrap().flush()
3723 }
3724 }
3725
3726 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3727 let console = Console::builder()
3728 .width(80)
3729 .file(Box::new(buffer.clone()))
3730 .build();
3731
3732 console.print_plain("");
3733
3734 let output = buffer.0.lock().unwrap();
3735 let result = String::from_utf8_lossy(&output);
3736 assert_eq!(result.trim(), "");
3738 }
3739
3740 #[test]
3741 fn test_print_unicode() {
3742 use std::sync::{Arc, Mutex};
3743
3744 #[derive(Clone)]
3745 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3746
3747 impl Write for SharedBuffer {
3748 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3749 self.0.lock().unwrap().write(buf)
3750 }
3751 fn flush(&mut self) -> io::Result<()> {
3752 self.0.lock().unwrap().flush()
3753 }
3754 }
3755
3756 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3757 let console = Console::builder()
3758 .width(80)
3759 .file(Box::new(buffer.clone()))
3760 .build();
3761
3762 console.print_plain("Hello δΈη π");
3763
3764 let output = buffer.0.lock().unwrap();
3765 let result = String::from_utf8_lossy(&output);
3766 assert!(result.contains("δΈη"));
3767 assert!(result.contains("π"));
3768 }
3769
3770 #[test]
3771 fn test_print_with_newlines() {
3772 use std::sync::{Arc, Mutex};
3773
3774 #[derive(Clone)]
3775 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
3776
3777 impl Write for SharedBuffer {
3778 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3779 self.0.lock().unwrap().write(buf)
3780 }
3781 fn flush(&mut self) -> io::Result<()> {
3782 self.0.lock().unwrap().flush()
3783 }
3784 }
3785
3786 let buffer = SharedBuffer(Arc::new(Mutex::new(Vec::new())));
3787 let console = Console::builder()
3788 .width(80)
3789 .file(Box::new(buffer.clone()))
3790 .build();
3791
3792 console.print_plain("Line 1\nLine 2\nLine 3");
3793
3794 let output = buffer.0.lock().unwrap();
3795 let result = String::from_utf8_lossy(&output);
3796 let lines: Vec<&str> = result.lines().collect();
3797 assert!(lines.len() >= 3);
3798 }
3799
3800 #[test]
3801 fn test_overflow_crop() {
3802 let console = Console::builder().width(80).markup(false).build();
3803 let mut output = Vec::new();
3804 let mut options = PrintOptions::new()
3805 .with_no_wrap(true)
3806 .with_overflow(OverflowMethod::Crop);
3807 options.width = Some(5);
3808 options.no_newline = true;
3809
3810 console
3811 .print_to(&mut output, "Hello World", &options)
3812 .expect("failed to print");
3813
3814 let text = String::from_utf8(output).expect("invalid utf8");
3815 assert_eq!(text, "Hello");
3816 }
3817
3818 struct FailingWriter;
3824
3825 impl Write for FailingWriter {
3826 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
3827 Err(io::Error::new(io::ErrorKind::BrokenPipe, "write failed"))
3828 }
3829
3830 fn flush(&mut self) -> io::Result<()> {
3831 Ok(())
3832 }
3833 }
3834
3835 struct FlushFailingWriter {
3837 buffer: Vec<u8>,
3838 }
3839
3840 impl FlushFailingWriter {
3841 fn new() -> Self {
3842 Self { buffer: Vec::new() }
3843 }
3844 }
3845
3846 impl Write for FlushFailingWriter {
3847 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3848 self.buffer.extend_from_slice(buf);
3849 Ok(buf.len())
3850 }
3851
3852 fn flush(&mut self) -> io::Result<()> {
3853 Err(io::Error::other("flush failed: disk full"))
3854 }
3855 }
3856
3857 struct LimitedWriter {
3859 limit: usize,
3860 written: usize,
3861 }
3862
3863 impl LimitedWriter {
3864 fn new(limit: usize) -> Self {
3865 Self { limit, written: 0 }
3866 }
3867 }
3868
3869 impl Write for LimitedWriter {
3870 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3871 if self.written >= self.limit {
3872 return Err(io::Error::new(io::ErrorKind::WriteZero, "buffer full"));
3873 }
3874 let available = self.limit - self.written;
3875 let to_write = buf.len().min(available);
3876 self.written += to_write;
3877 Ok(to_write)
3878 }
3879
3880 fn flush(&mut self) -> io::Result<()> {
3881 Ok(())
3882 }
3883 }
3884
3885 struct TrackingWriter {
3887 writes: Arc<Mutex<Vec<usize>>>,
3888 flushes: Arc<Mutex<usize>>,
3889 }
3890
3891 impl TrackingWriter {
3892 fn new() -> Self {
3893 Self {
3894 writes: Arc::new(Mutex::new(Vec::new())),
3895 flushes: Arc::new(Mutex::new(0)),
3896 }
3897 }
3898
3899 fn write_count(&self) -> usize {
3900 self.writes.lock().unwrap().len()
3901 }
3902
3903 #[allow(dead_code)]
3904 fn flush_count(&self) -> usize {
3905 *self.flushes.lock().unwrap()
3906 }
3907
3908 fn total_bytes(&self) -> usize {
3909 self.writes.lock().unwrap().iter().sum()
3910 }
3911 }
3912
3913 impl Clone for TrackingWriter {
3914 fn clone(&self) -> Self {
3915 Self {
3916 writes: Arc::clone(&self.writes),
3917 flushes: Arc::clone(&self.flushes),
3918 }
3919 }
3920 }
3921
3922 impl Write for TrackingWriter {
3923 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3924 self.writes.lock().unwrap().push(buf.len());
3925 Ok(buf.len())
3926 }
3927
3928 fn flush(&mut self) -> io::Result<()> {
3929 *self.flushes.lock().unwrap() += 1;
3930 Ok(())
3931 }
3932 }
3933
3934 #[test]
3935 fn test_io_write_failure() {
3936 let console = Console::builder().width(80).markup(false).build();
3938
3939 let mut failing_writer = FailingWriter;
3940 let result = console.print_to(&mut failing_writer, "Hello", &PrintOptions::new());
3941
3942 assert!(result.is_err());
3943 let err = result.unwrap_err();
3944 assert_eq!(err.kind(), io::ErrorKind::BrokenPipe);
3945 }
3946
3947 #[test]
3948 fn test_io_write_partial() {
3949 let console = Console::builder().width(80).markup(false).build();
3951
3952 let mut limited = LimitedWriter::new(5);
3953 let _result = console.print_to(&mut limited, "Hello World!", &PrintOptions::new());
3954
3955 assert!(limited.written > 0);
3958 }
3959
3960 #[test]
3961 fn test_io_flush_failure() {
3962 let mut writer = FlushFailingWriter::new();
3964
3965 let write_result = writer.write(b"Hello");
3967 assert!(write_result.is_ok());
3968 assert_eq!(write_result.unwrap(), 5);
3969
3970 let flush_result = writer.flush();
3972 assert!(flush_result.is_err());
3973 let err = flush_result.unwrap_err();
3974 assert!(err.to_string().contains("flush failed"));
3975 }
3976
3977 #[test]
3978 fn test_io_write_segments_to_failing() {
3979 let console = Console::builder().width(80).markup(false).build();
3980
3981 let segments = vec![
3983 Segment::plain("Hello "),
3984 Segment::styled("World", Style::new().bold()),
3985 ];
3986
3987 let mut failing_writer = FailingWriter;
3988 let result = console.print_segments_to(&mut failing_writer, &segments);
3989
3990 assert!(result.is_err());
3991 }
3992
3993 #[test]
3994 fn test_io_print_text_to_failing() {
3995 let console = Console::builder().width(80).markup(false).build();
3996
3997 let text = Text::new("Hello World");
3998 let mut failing_writer = FailingWriter;
3999 let result = console.print_text_to(&mut failing_writer, &text);
4000
4001 assert!(result.is_err());
4002 }
4003
4004 #[test]
4005 fn test_io_write_tracking() {
4006 let tracking = TrackingWriter::new();
4008 let console = Console::builder()
4009 .width(80)
4010 .markup(false)
4011 .file(Box::new(tracking.clone()))
4012 .build();
4013
4014 console.print_plain("Line 1");
4015 console.print_plain("Line 2");
4016
4017 assert!(tracking.write_count() >= 2, "Expected writes to occur");
4019 assert!(tracking.total_bytes() > 0, "Expected bytes written");
4020 }
4021
4022 #[test]
4023 fn test_io_empty_write() {
4024 let console = Console::builder().width(80).markup(false).build();
4026
4027 let mut output = Vec::new();
4028 let result = console.print_to(&mut output, "", &PrintOptions::new().with_no_newline(true));
4029
4030 assert!(result.is_ok());
4031 assert!(output.is_empty() || output == b"\n");
4033 }
4034
4035 #[test]
4036 fn test_io_large_write() {
4037 let console = Console::builder().width(1000).markup(false).build();
4039
4040 let large_content = "x".repeat(10000);
4041 let mut output = Vec::new();
4042 let result = console.print_to(&mut output, &large_content, &PrintOptions::new());
4043
4044 assert!(result.is_ok());
4045 assert!(output.len() >= 10000);
4047 }
4048
4049 #[test]
4050 fn test_io_control_code_write_failure() {
4051 let console = Console::builder().width(80).markup(false).build();
4054
4055 let segments = vec![Segment {
4057 text: std::borrow::Cow::Borrowed(""),
4058 style: None,
4059 control: Some(vec![ControlCode::new(ControlType::Home)]),
4060 }];
4061
4062 let mut failing_writer = FailingWriter;
4063 let result = console.print_segments_to(&mut failing_writer, &segments);
4064
4065 let _ = result;
4069 }
4070
4071 #[test]
4072 fn test_control_cursor_move_to_column_is_zero_based() {
4073 let console = Console::builder()
4074 .width(80)
4075 .markup(false)
4076 .force_terminal(true)
4077 .build();
4078 let segments = vec![Segment::control(vec![ControlCode::with_params_vec(
4079 ControlType::CursorMoveToColumn,
4080 vec![0],
4081 )])];
4082 let mut output = Vec::new();
4083 console
4084 .print_segments_to(&mut output, &segments)
4085 .expect("print_segments_to");
4086
4087 assert_eq!(String::from_utf8(output).expect("utf8 output"), "\x1b[1G");
4088 }
4089
4090 #[test]
4091 fn test_control_cursor_move_to_is_zero_based_xy() {
4092 let console = Console::builder()
4093 .width(80)
4094 .markup(false)
4095 .force_terminal(true)
4096 .build();
4097 let segments = vec![Segment::control(vec![ControlCode::with_params_vec(
4098 ControlType::CursorMoveTo,
4099 vec![3, 4],
4100 )])];
4101 let mut output = Vec::new();
4102 console
4103 .print_segments_to(&mut output, &segments)
4104 .expect("print_segments_to");
4105
4106 assert_eq!(String::from_utf8(output).expect("utf8 output"), "\x1b[5;4H");
4107 }
4108
4109 #[test]
4110 fn test_control_set_window_title_emits_empty_title_sequence() {
4111 let console = Console::builder()
4112 .width(80)
4113 .markup(false)
4114 .force_terminal(true)
4115 .build();
4116 let segments = vec![Segment {
4117 text: std::borrow::Cow::Borrowed(""),
4118 style: None,
4119 control: Some(vec![ControlCode::new(ControlType::SetWindowTitle)]),
4120 }];
4121 let mut output = Vec::new();
4122 console
4123 .print_segments_to(&mut output, &segments)
4124 .expect("print_segments_to");
4125
4126 assert_eq!(
4127 String::from_utf8(output).expect("utf8 output"),
4128 "\x1b]0;\x07"
4129 );
4130 }
4131
4132 #[test]
4133 fn test_io_error_types() {
4134 struct NotFoundWriter;
4136 impl Write for NotFoundWriter {
4137 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
4138 Err(io::Error::new(io::ErrorKind::NotFound, "file not found"))
4139 }
4140 fn flush(&mut self) -> io::Result<()> {
4141 Ok(())
4142 }
4143 }
4144
4145 struct PermissionWriter;
4146 impl Write for PermissionWriter {
4147 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
4148 Err(io::Error::new(
4149 io::ErrorKind::PermissionDenied,
4150 "access denied",
4151 ))
4152 }
4153 fn flush(&mut self) -> io::Result<()> {
4154 Ok(())
4155 }
4156 }
4157
4158 let console = Console::builder().width(80).markup(false).build();
4160 let mut not_found = NotFoundWriter;
4161 let result1 = console.print_to(&mut not_found, "test", &PrintOptions::new());
4162 assert!(matches!(
4163 result1.as_ref().map_err(std::io::Error::kind),
4164 Err(io::ErrorKind::NotFound)
4165 ));
4166
4167 let mut permission = PermissionWriter;
4168 let result2 = console.print_to(&mut permission, "test", &PrintOptions::new());
4169 assert!(matches!(
4170 result2.as_ref().map_err(std::io::Error::kind),
4171 Err(io::ErrorKind::PermissionDenied)
4172 ));
4173 }
4174
4175 #[test]
4176 fn test_io_concurrent_writes() {
4177 use std::thread;
4178
4179 struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
4180
4181 impl Clone for SharedBuffer {
4182 fn clone(&self) -> Self {
4183 Self(Arc::clone(&self.0))
4184 }
4185 }
4186
4187 impl Write for SharedBuffer {
4188 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
4189 self.0.lock().unwrap().extend_from_slice(buf);
4190 Ok(buf.len())
4191 }
4192 fn flush(&mut self) -> io::Result<()> {
4193 Ok(())
4194 }
4195 }
4196
4197 let buffer = Arc::new(Mutex::new(Vec::new()));
4199 let shared = SharedBuffer(Arc::clone(&buffer));
4200 let console = Console::builder()
4201 .width(80)
4202 .markup(false)
4203 .file(Box::new(shared))
4204 .build()
4205 .shared();
4206
4207 let mut handles = vec![];
4209 for i in 0..4 {
4210 let console_clone = Arc::clone(&console);
4211 let handle = thread::spawn(move || {
4212 console_clone.print_plain(&format!("Thread {i}"));
4213 });
4214 handles.push(handle);
4215 }
4216
4217 for handle in handles {
4218 handle.join().expect("thread panicked");
4219 }
4220
4221 let output = buffer.lock().unwrap();
4223 let text = String::from_utf8_lossy(&output);
4224 assert!(text.contains("Thread"), "Expected thread output");
4226 }
4227
4228 #[test]
4229 fn test_io_interrupted_write() {
4230 struct InterruptedWriter {
4232 attempts: Arc<Mutex<usize>>,
4233 succeed_after: usize,
4234 }
4235
4236 impl Write for InterruptedWriter {
4237 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
4238 let mut attempts = self.attempts.lock().unwrap();
4239 *attempts += 1;
4240 if *attempts <= self.succeed_after {
4241 Err(io::Error::new(io::ErrorKind::Interrupted, "interrupted"))
4242 } else {
4243 Ok(buf.len())
4244 }
4245 }
4246 fn flush(&mut self) -> io::Result<()> {
4247 Ok(())
4248 }
4249 }
4250
4251 let console = Console::builder().width(80).markup(false).build();
4252
4253 let attempts = Arc::new(Mutex::new(0));
4255 let mut writer = InterruptedWriter {
4256 attempts: Arc::clone(&attempts),
4257 succeed_after: 0, };
4259
4260 let result = console.print_to(&mut writer, "test", &PrintOptions::new());
4261 assert!(
4262 result.is_ok()
4263 || result.as_ref().map_err(std::io::Error::kind) == Err(io::ErrorKind::Interrupted)
4264 );
4265 }
4266}