Skip to main content

rich_rust/
console.rs

1//! Console - the central entry point for styled terminal output.
2//!
3//! The [`Console`] handles rendering styled content to the terminal,
4//! including color detection, width calculation, and ANSI code generation.
5//!
6//! # Examples
7//!
8//! ## Basic Printing with Markup
9//!
10//! ```rust,ignore
11//! use rich_rust::Console;
12//!
13//! let console = Console::new();
14//!
15//! // Print with markup syntax
16//! console.print("[bold red]Error:[/] Something went wrong");
17//! console.print("[green]Success![/] Operation completed");
18//!
19//! // Markup supports colors, attributes, and combinations
20//! console.print("[bold italic #ff8800 on blue]Custom styling[/]");
21//! ```
22//!
23//! ## Console Builder
24//!
25//! ```rust,ignore
26//! use rich_rust::console::{Console, ConsoleBuilder};
27//! use rich_rust::color::ColorSystem;
28//!
29//! let console = Console::builder()
30//!     .color_system(ColorSystem::EightBit)  // Force 256 colors
31//!     .width(80)                            // Fixed width
32//!     .markup(true)                         // Enable markup parsing
33//!     .build();
34//! ```
35//!
36//! ## Print Options
37//!
38//! ```rust,ignore
39//! use rich_rust::console::{Console, PrintOptions};
40//! use rich_rust::style::Style;
41//! use rich_rust::text::JustifyMethod;
42//!
43//! let console = Console::new();
44//!
45//! let options = PrintOptions::new()
46//!     .with_style(Style::new().bold())
47//!     .with_justify(JustifyMethod::Center)
48//!     .with_markup(true);
49//!
50//! console.print_with_options("Centered bold text", &options);
51//! ```
52//!
53//! ## Capturing Output
54//!
55//! ```rust,ignore
56//! use rich_rust::Console;
57//!
58//! let mut console = Console::new();
59//!
60//! // Start capturing
61//! console.begin_capture();
62//! console.print("[bold]Hello[/]");
63//!
64//! // Get captured segments
65//! let segments = console.end_capture();
66//! for seg in &segments {
67//!     println!("Text: {:?}, Style: {:?}", seg.text, seg.style);
68//! }
69//! ```
70//!
71//! # Terminal Detection
72//!
73//! The Console automatically detects terminal capabilities:
74//!
75//! - **Color system**: `TrueColor` (24-bit), 256 colors, or 16 colors
76//! - **Terminal dimensions**: Width and height in character cells
77//! - **TTY status**: Whether output is to an interactive terminal
78//!
79//! You can override these with the builder pattern or by setting explicit values.
80
81use 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/// Console dimensions in cells.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct ConsoleDimensions {
108    /// Width in cells.
109    pub width: usize,
110    /// Height in rows.
111    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/// Options for rendering.
124#[derive(Debug, Clone)]
125pub struct ConsoleOptions {
126    /// Terminal dimensions.
127    pub size: ConsoleDimensions,
128    /// Using legacy Windows console.
129    pub legacy_windows: bool,
130    /// Minimum width constraint.
131    pub min_width: usize,
132    /// Maximum width constraint.
133    pub max_width: usize,
134    /// Output is a terminal (vs file/pipe).
135    pub is_terminal: bool,
136    /// Output encoding.
137    pub encoding: String,
138    /// Maximum height for rendering.
139    pub max_height: usize,
140    /// Default justification.
141    pub justify: Option<JustifyMethod>,
142    /// Default overflow handling.
143    pub overflow: Option<OverflowMethod>,
144    /// Default `no_wrap` setting.
145    pub no_wrap: Option<bool>,
146    /// Enable highlighting.
147    pub highlight: Option<bool>,
148    /// Parse markup in strings.
149    pub markup: Option<bool>,
150    /// Explicit height override.
151    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    /// Create options with a different `max_width`.
176    #[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    /// Create options with a different height.
185    #[must_use]
186    pub fn update_height(&self, height: usize) -> Self {
187        Self {
188            height: Some(height),
189            ..self.clone()
190        }
191    }
192
193    /// Create options with updated width and height.
194    #[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/// Print options for controlling output.
207#[derive(Clone, Default)]
208pub struct PrintOptions {
209    /// String to separate multiple objects.
210    pub sep: String,
211    /// String to append at end.
212    pub end: String,
213    /// Apply style to output.
214    pub style: Option<Style>,
215    /// Override justification.
216    pub justify: Option<JustifyMethod>,
217    /// Override overflow handling.
218    pub overflow: Option<OverflowMethod>,
219    /// Override `no_wrap`.
220    pub no_wrap: Option<bool>,
221    /// Suppress newline.
222    pub no_newline: bool,
223    /// Parse markup.
224    pub markup: Option<bool>,
225    /// Enable/disable highlighting (None = inherit Console setting).
226    pub highlight: Option<bool>,
227    /// Override the highlighter used when highlighting is enabled.
228    pub highlighter: Option<Arc<dyn Highlighter>>,
229    /// Override width.
230    pub width: Option<usize>,
231    /// Crop output to width.
232    pub crop: bool,
233    /// Soft wrap at width.
234    pub soft_wrap: bool,
235}
236
237impl PrintOptions {
238    /// Create new print options with defaults.
239    #[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    /// Set markup parsing.
249    #[must_use]
250    pub fn with_markup(mut self, markup: bool) -> Self {
251        self.markup = Some(markup);
252        self
253    }
254
255    /// Set style.
256    #[must_use]
257    pub fn with_style(mut self, style: Style) -> Self {
258        self.style = Some(style);
259        self
260    }
261
262    /// Set the separator between objects.
263    #[must_use]
264    pub fn with_sep(mut self, sep: impl Into<String>) -> Self {
265        self.sep = sep.into();
266        self
267    }
268
269    /// Set the end string appended after output.
270    #[must_use]
271    pub fn with_end(mut self, end: impl Into<String>) -> Self {
272        self.end = end.into();
273        self
274    }
275
276    /// Override justification.
277    #[must_use]
278    pub fn with_justify(mut self, justify: JustifyMethod) -> Self {
279        self.justify = Some(justify);
280        self
281    }
282
283    /// Override overflow handling.
284    #[must_use]
285    pub fn with_overflow(mut self, overflow: OverflowMethod) -> Self {
286        self.overflow = Some(overflow);
287        self
288    }
289
290    /// Override `no_wrap`.
291    #[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    /// Suppress newline at end.
298    #[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    /// Enable/disable highlighting.
305    #[must_use]
306    pub fn with_highlight(mut self, highlight: bool) -> Self {
307        self.highlight = Some(highlight);
308        self
309    }
310
311    /// Override the highlighter for this print call.
312    #[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    /// Override width.
319    #[must_use]
320    pub fn with_width(mut self, width: usize) -> Self {
321        self.width = Some(width);
322        self
323    }
324
325    /// Crop output to width.
326    #[must_use]
327    pub fn with_crop(mut self, crop: bool) -> Self {
328        self.crop = crop;
329        self
330    }
331
332    /// Soft wrap at width.
333    #[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
363/// Hook for intercepting rendered segments before output.
364pub trait RenderHook: Send + Sync {
365    fn process(&self, console: &Console, segments: &[Segment<'static>]) -> Vec<Segment<'static>>;
366}
367
368/// The main Console for rendering styled output.
369///
370/// `Console` is the central entry point for all terminal output operations.
371/// It handles color detection, terminal dimensions, markup parsing, and
372/// ANSI escape code generation.
373///
374/// # Thread Safety
375///
376/// `Console` is `Send + Sync` and can be safely shared between threads using
377/// `Arc<Console>`. All internal state is protected by mutexes that use poison
378/// recovery (see the [`sync`](crate::sync) module).
379///
380/// When multiple threads print concurrently, their output may interleave at
381/// the line level. For strictly ordered output, synchronize at the application
382/// level or use a single printing thread.
383///
384/// # Example
385///
386/// ```rust,ignore
387/// use std::sync::Arc;
388/// use std::thread;
389/// use rich_rust::Console;
390///
391/// let console = Arc::new(Console::new());
392///
393/// let handles: Vec<_> = (0..4).map(|i| {
394///     let c = Arc::clone(&console);
395///     thread::spawn(move || {
396///         c.print(&format!("Hello from thread {i}"));
397///     })
398/// }).collect();
399///
400/// for h in handles {
401///     h.join().unwrap();
402/// }
403/// ```
404pub struct Console {
405    /// Color system to use (None = auto-detect).
406    color_system: Option<ColorSystem>,
407    /// Force terminal mode.
408    force_terminal: Option<bool>,
409    /// Tab expansion size.
410    tab_size: usize,
411    /// Buffer output for export.
412    record: AtomicBool,
413    /// Parse markup by default.
414    markup: bool,
415    /// Enable emoji rendering.
416    emoji: bool,
417    /// Enable syntax highlighting.
418    highlight: bool,
419    /// Highlighter used when `highlight` is enabled (Python Rich `rich.highlighter` parity).
420    highlighter: Arc<dyn Highlighter>,
421    /// Theme stack for named styles (Python Rich parity).
422    theme_stack: Mutex<ThemeStack>,
423    /// Override width.
424    width: Option<usize>,
425    /// Override height.
426    height: Option<usize>,
427    /// Use ASCII-safe box characters.
428    safe_box: bool,
429    /// Output stream (defaults to stdout).
430    file: Mutex<Box<dyn Write + Send>>,
431    /// Recording buffer.
432    buffer: Mutex<Vec<Segment<'static>>>,
433    /// Cached terminal detection.
434    is_terminal: bool,
435    /// Detected/configured color system.
436    detected_color_system: Option<ColorSystem>,
437    /// Render hooks (Live uses this).
438    render_hooks: Mutex<Vec<Arc<dyn RenderHook>>>,
439    /// Active Live stack for nested Live handling.
440    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    /// Create a new console with default settings.
472    #[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    /// Create a console builder for custom configuration.
504    #[must_use]
505    pub fn builder() -> ConsoleBuilder {
506        ConsoleBuilder::default()
507    }
508
509    /// Convert this Console into a shared reference-counted handle.
510    #[must_use]
511    pub fn shared(self) -> Arc<Self> {
512        Arc::new(self)
513    }
514
515    /// Get the console width.
516    #[must_use]
517    pub fn width(&self) -> usize {
518        self.width.unwrap_or_else(terminal::get_terminal_width)
519    }
520
521    /// Get the console height.
522    #[must_use]
523    pub fn height(&self) -> usize {
524        self.height.unwrap_or_else(terminal::get_terminal_height)
525    }
526
527    /// Get the console dimensions.
528    #[must_use]
529    pub fn size(&self) -> ConsoleDimensions {
530        ConsoleDimensions {
531            width: self.width(),
532            height: self.height(),
533        }
534    }
535
536    /// Check if this console outputs to a terminal.
537    #[must_use]
538    pub fn is_terminal(&self) -> bool {
539        self.force_terminal.unwrap_or(self.is_terminal)
540    }
541
542    /// Terminal detection result without `force_terminal` overrides.
543    ///
544    /// This is used for behaviors that must not affect non-TTY contexts even if
545    /// a caller forces terminal rendering (e.g. process-wide stdio redirection).
546    #[must_use]
547    pub(crate) const fn is_terminal_detected(&self) -> bool {
548        self.is_terminal
549    }
550
551    /// Get the color system in use.
552    #[must_use]
553    pub fn color_system(&self) -> Option<ColorSystem> {
554        self.color_system.or(self.detected_color_system)
555    }
556
557    /// Check if Rich-style emoji code replacement is enabled.
558    #[must_use]
559    pub const fn emoji(&self) -> bool {
560        self.emoji
561    }
562
563    /// Check if ASCII-safe box drawing is enabled.
564    #[must_use]
565    pub const fn safe_box(&self) -> bool {
566        self.safe_box
567    }
568
569    /// Get a style by theme name or parse a style definition.
570    ///
571    /// Mirrors Python Rich `Console.get_style()`:
572    /// - Check the active theme stack for an exact name match
573    /// - Fall back to parsing a style definition
574    ///
575    /// If parsing fails, this returns an empty style.
576    #[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    /// Like [`Self::get_style`], but returns an error if the style can't be parsed.
582    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    /// Push a theme on to the theme stack.
593    pub fn push_theme(&self, theme: Theme, inherit: bool) {
594        lock_recover(&self.theme_stack).push_theme(theme, inherit);
595    }
596
597    /// Pop the current theme from the theme stack.
598    pub fn pop_theme(&self) -> Result<(), ThemeStackError> {
599        lock_recover(&self.theme_stack).pop_theme()
600    }
601
602    /// Use a theme for the duration of the returned guard.
603    #[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    /// Check if colors are enabled.
610    #[must_use]
611    pub fn is_color_enabled(&self) -> bool {
612        self.color_system().is_some()
613    }
614
615    /// Get the tab size.
616    #[must_use]
617    pub const fn tab_size(&self) -> usize {
618        self.tab_size
619    }
620
621    /// Create console options for rendering.
622    #[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    /// Measure a renderable via the measurement protocol (Python Rich `Console.measure` parity).
649    #[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    /// Check if the terminal is "dumb".
660    #[must_use]
661    pub fn is_dumb_terminal(&self) -> bool {
662        terminal::is_dumb_terminal()
663    }
664
665    /// Check if the console is interactive (TTY and not dumb).
666    #[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    /// Show or hide the cursor.
718    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    /// Enable or disable the alternate screen buffer.
728    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    /// Enable recording mode.
738    ///
739    /// All subsequent console output will be captured to an internal buffer
740    /// until [`end_capture`](Self::end_capture) is called.
741    pub fn begin_capture(&self) {
742        self.record.store(true, Ordering::Relaxed);
743        lock_recover(&self.buffer).clear();
744    }
745
746    /// End recording and return captured segments.
747    ///
748    /// Returns all segments captured since [`begin_capture`](Self::begin_capture)
749    /// was called, and clears the internal buffer.
750    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    /// Print styled text to the console.
756    ///
757    /// # Examples
758    ///
759    /// ```ignore
760    /// use rich_rust::Console;
761    ///
762    /// let console = Console::new();
763    /// console.print("[bold red]Hello[/] World!");
764    /// ```
765    pub fn print(&self, content: &str) {
766        self.print_with_options(content, &PrintOptions::new().with_markup(self.markup));
767    }
768
769    /// Print a prepared Text object.
770    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    /// Print a prepared Text object to a specific writer.
776    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    /// Print prepared segments.
787    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    /// Print prepared segments to a specific writer.
793    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    /// Print any object implementing the Renderable trait.
805    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    /// Print a value via the protocol casting hook (Python Rich `rich.protocol.rich_cast` parity).
838    pub fn print_cast(&self, value: &dyn RichCast) {
839        self.print_cast_with_options(value, &PrintOptions::new().with_markup(self.markup));
840    }
841
842    /// Print a castable value with custom options (string options apply when the cast yields a string).
843    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    /// Print a castable value to a specific writer.
849    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    /// Print an exception / traceback renderable.
861    ///
862    /// This is a convenience wrapper mirroring Python Rich's `Console.print_exception`.
863    pub fn print_exception(&self, traceback: &crate::renderables::Traceback) {
864        self.print_renderable(traceback);
865    }
866
867    /// Print with custom options.
868    pub fn print_with_options(&self, content: &str, options: &PrintOptions) {
869        let mut file = lock_recover(&self.file);
870        // Keep `Console::print_*` infallible (matches Rich's ergonomics). If callers need
871        // I/O error handling they can use `Console::print_to(...)` directly.
872        let _ = self.print_to(&mut *file, content, options);
873    }
874
875    /// Export rendered text (no ANSI) using default print options.
876    #[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    /// Export rendered text (no ANSI) using custom print options.
882    #[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    /// Export a castable value to plain text (no ANSI).
889    #[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    /// Export a castable value to plain text (no ANSI) using custom print options.
895    #[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    /// Export a renderable to plain text (no ANSI).
906    #[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    /// Export recorded output to HTML.
914    #[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    /// Export recorded output to SVG.
923    #[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    /// Export recorded output to HTML with Rich-style options.
932    ///
933    /// Mirrors Python Rich's `Console.export_html(...)` behavior.
934    #[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    /// Export recorded output to SVG with Rich-style options.
945    ///
946    /// Mirrors Python Rich's `Console.export_svg(...)` behavior.
947    #[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    /// Print to a specific writer.
958    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        // Parse markup if enabled
977        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        // soft_wrap enables wrapping by overriding text's no_wrap setting
1005        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        // Apply any overall style
1062        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, &current);
1108        }
1109        current
1110    }
1111
1112    /// Write segments to a writer without invoking render hooks.
1113    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            // Get ANSI codes for style
1131            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 styled text
1146            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                    // Python Rich expects 0-based columns in ControlCode parameters and
1205                    // formats with +1 (terminal control sequences are 1-based).
1206                    let column0 = control_param(&control.params, 0, 0);
1207                    write!(writer, "\x1b[{}G", column0 + 1)?;
1208                }
1209                crate::segment::ControlType::CursorMoveTo => {
1210                    // Python Rich stores (x, y) 0-based and formats as (y+1; x+1).
1211                    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    /// Print a blank line.
1230    pub fn line(&self) {
1231        let mut file = lock_recover(&self.file);
1232        let _ = writeln!(file);
1233    }
1234
1235    /// Print a rule (horizontal line).
1236    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            // Ensure title fits within width, accounting for 2 spaces padding
1243            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    /// Clear the screen.
1267    pub fn clear(&self) {
1268        let mut file = lock_recover(&self.file);
1269        let _ = terminal::control::clear_screen(&mut *file);
1270    }
1271
1272    /// Clear the current line.
1273    pub fn clear_line(&self) {
1274        let mut file = lock_recover(&self.file);
1275        let _ = terminal::control::clear_line(&mut *file);
1276    }
1277
1278    /// Set the terminal title.
1279    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    /// Ring the terminal bell.
1285    pub fn bell(&self) {
1286        let mut file = lock_recover(&self.file);
1287        let _ = terminal::control::bell(&mut *file);
1288    }
1289
1290    /// Print text without parsing markup.
1291    pub fn print_plain(&self, content: &str) {
1292        self.print_with_options(content, &PrintOptions::new().with_markup(false));
1293    }
1294
1295    /// Print a styled message.
1296    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    /// Print a log message with a level indicator.
1306    ///
1307    /// This is a simple version that just shows the level prefix and message.
1308    /// For timestamps and file/line info, use [`log_with_options`](Self::log_with_options).
1309    ///
1310    /// # Examples
1311    ///
1312    /// ```rust,ignore
1313    /// use rich_rust::console::{Console, LogLevel};
1314    ///
1315    /// let console = Console::new();
1316    /// console.log("Starting server", LogLevel::Info);
1317    /// console.log("Something went wrong", LogLevel::Error);
1318    /// ```
1319    pub fn log(&self, message: &str, level: LogLevel) {
1320        self.log_with_options(message, level, &LogOptions::new());
1321    }
1322
1323    /// Print a log message with a level indicator, timestamp, and optional file/line info.
1324    ///
1325    /// # Examples
1326    ///
1327    /// ```rust,ignore
1328    /// use rich_rust::console::{Console, LogLevel, LogOptions};
1329    ///
1330    /// let console = Console::new();
1331    ///
1332    /// // With timestamp
1333    /// let opts = LogOptions::new().with_timestamp(true);
1334    /// console.log_with_options("Server started", LogLevel::Info, &opts);
1335    /// // Output: [12:34:56] [INFO] Server started
1336    ///
1337    /// // With timestamp and file/line
1338    /// let opts = LogOptions::new()
1339    ///     .with_timestamp(true)
1340    ///     .with_path("src/main.rs", 42);
1341    /// console.log_with_options("Debug info", LogLevel::Debug, &opts);
1342    /// // Output: [12:34:56] src/main.rs:42 [DEBUG] Debug info
1343    /// ```
1344    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            // Print timestamp if enabled
1355            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                    &timestamp,
1361                    &PrintOptions::new().with_markup(false).with_style(ts_style),
1362                );
1363                let _ = write!(file, " ");
1364            }
1365
1366            // Print file/line info if provided
1367            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            // Print level prefix if enabled
1388            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            // Print the message
1400            let _ = self.print_to(
1401                &mut *file,
1402                message,
1403                &PrintOptions::new().with_markup(self.markup),
1404            );
1405        }
1406    }
1407
1408    /// Format the current time as a timestamp string.
1409    fn format_timestamp(format: Option<&str>) -> String {
1410        // Prefer local time for parity with typical "console logger" expectations, but
1411        // fall back to UTC when local offset can't be determined (e.g., sandboxed envs).
1412        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    // Intentionally supports a small, stable subset of strftime:
1426    // %Y %m %d %H %M %S and %%.
1427    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                    // time::Month implements `From<Month> for u8`.
1458                    let _ = write!(out, "{:02}", u8::from(now.month()));
1459                }
1460                'd' => {
1461                    let _ = write!(out, "{:02}", now.day());
1462                }
1463                other => {
1464                    // Preserve unknown tokens literally to avoid surprising callers.
1465                    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    // Sanitize title to prevent terminal injection:
1508    // Remove control characters that could break or escape the OSC sequence
1509    raw_title
1510        .chars()
1511        .filter(|c| {
1512            // Allow printable characters only, excluding control chars
1513            // BEL (\x07) terminates OSC, ESC (\x1b) starts new sequences
1514            !c.is_control()
1515        })
1516        .collect()
1517}
1518
1519// ============================================================================
1520// HTML/SVG Export (Python Rich parity)
1521// ============================================================================
1522
1523/// Default HTML export template (Rich 13.9.4).
1524pub 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
1526/// Default SVG export template (Rich 13.9.4).
1527pub 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/// Options for controlling HTML export.
1530#[derive(Debug, Clone)]
1531pub struct ExportHtmlOptions {
1532    pub theme: TerminalTheme,
1533    pub clear: bool,
1534    /// Optional template override. If `None`, uses [`CONSOLE_HTML_FORMAT`].
1535    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/// Options for controlling SVG export.
1551#[derive(Debug, Clone)]
1552pub struct ExportSvgOptions {
1553    pub title: String,
1554    pub theme: TerminalTheme,
1555    pub clear: bool,
1556    /// Optional template override. If `None`, uses [`CONSOLE_SVG_FORMAT`].
1557    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(' ', "&#160;") };
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("&amp;"),
1914            '<' => escaped.push_str("&lt;"),
1915            '>' => escaped.push_str("&gt;"),
1916            '"' => escaped.push_str("&quot;"),
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/// Log level for `console.log()`.
1935#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1936pub enum LogLevel {
1937    Debug,
1938    Info,
1939    Warning,
1940    Error,
1941}
1942
1943/// Options for controlling log output format.
1944///
1945/// # Examples
1946///
1947/// ```rust,ignore
1948/// use rich_rust::console::{Console, LogLevel, LogOptions};
1949///
1950/// let console = Console::new();
1951///
1952/// // Log with timestamp
1953/// let opts = LogOptions::new().with_timestamp(true);
1954/// console.log_with_options("Something happened", LogLevel::Info, &opts);
1955///
1956/// // Log with file/line info
1957/// let opts = LogOptions::new()
1958///     .with_timestamp(true)
1959///     .with_path("src/main.rs", 42);
1960/// console.log_with_options("Debug info", LogLevel::Debug, &opts);
1961/// ```
1962#[derive(Debug, Clone)]
1963pub struct LogOptions {
1964    /// Whether to show a timestamp.
1965    pub show_timestamp: bool,
1966    /// Custom timestamp format (strftime-like subset).
1967    ///
1968    /// Supported codes: `%Y` `%m` `%d` `%H` `%M` `%S` and `%%`.
1969    /// Unknown codes are preserved literally.
1970    ///
1971    /// If None, uses default format: `"[HH:MM:SS]"`.
1972    pub timestamp_format: Option<String>,
1973    /// File path (e.g., "src/main.rs").
1974    pub file_path: Option<String>,
1975    /// Line number within the file.
1976    pub line_number: Option<u32>,
1977    /// Whether to show the log level prefix.
1978    pub show_level: bool,
1979    /// Whether to highlight keywords in the message.
1980    pub highlight: bool,
1981}
1982
1983impl Default for LogOptions {
1984    fn default() -> Self {
1985        Self::new()
1986    }
1987}
1988
1989impl LogOptions {
1990    /// Create new log options with default values.
1991    #[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    /// Enable or disable timestamp display.
2004    #[must_use]
2005    pub fn with_timestamp(mut self, show: bool) -> Self {
2006        self.show_timestamp = show;
2007        self
2008    }
2009
2010    /// Set a custom timestamp format.
2011    ///
2012    /// Simple format using: `%H` (hour), `%M` (minute), `%S` (second),
2013    /// `%Y` (year), `%m` (month), `%d` (day).
2014    #[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    /// Set the file path and line number for caller info.
2021    #[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    /// Set just the file path (without line number).
2029    #[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    /// Set just the line number.
2036    #[must_use]
2037    pub fn with_line(mut self, line: u32) -> Self {
2038        self.line_number = Some(line);
2039        self
2040    }
2041
2042    /// Enable or disable level prefix display.
2043    #[must_use]
2044    pub fn with_level(mut self, show: bool) -> Self {
2045        self.show_level = show;
2046        self
2047    }
2048
2049    /// Enable or disable keyword highlighting.
2050    #[must_use]
2051    pub fn with_highlight(mut self, highlight: bool) -> Self {
2052        self.highlight = highlight;
2053        self
2054    }
2055}
2056
2057/// RAII guard returned by [`Console::use_theme`].
2058pub 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/// Builder for creating a Console with custom settings.
2069#[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    /// Set the color system.
2109    #[must_use]
2110    pub fn color_system(mut self, system: ColorSystem) -> Self {
2111        self.color_system = Some(system);
2112        self
2113    }
2114
2115    /// Disable colors.
2116    #[must_use]
2117    pub fn no_color(mut self) -> Self {
2118        self.color_system = None;
2119        self
2120    }
2121
2122    /// Force terminal mode.
2123    #[must_use]
2124    pub fn force_terminal(mut self, force: bool) -> Self {
2125        self.force_terminal = Some(force);
2126        self
2127    }
2128
2129    /// Set tab size.
2130    #[must_use]
2131    pub fn tab_size(mut self, size: usize) -> Self {
2132        self.tab_size = Some(size);
2133        self
2134    }
2135
2136    /// Enable/disable markup parsing.
2137    #[must_use]
2138    pub fn markup(mut self, enabled: bool) -> Self {
2139        self.markup = Some(enabled);
2140        self
2141    }
2142
2143    /// Enable/disable emoji.
2144    #[must_use]
2145    pub fn emoji(mut self, enabled: bool) -> Self {
2146        self.emoji = Some(enabled);
2147        self
2148    }
2149
2150    /// Enable/disable highlighting.
2151    #[must_use]
2152    pub fn highlight(mut self, enabled: bool) -> Self {
2153        self.highlight = Some(enabled);
2154        self
2155    }
2156
2157    /// Set the console's default highlighter.
2158    #[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    /// Set console width.
2165    #[must_use]
2166    pub fn width(mut self, width: usize) -> Self {
2167        self.width = Some(width);
2168        self
2169    }
2170
2171    /// Set console height.
2172    #[must_use]
2173    pub fn height(mut self, height: usize) -> Self {
2174        self.height = Some(height);
2175        self
2176    }
2177
2178    /// Use ASCII-safe box characters.
2179    #[must_use]
2180    pub fn safe_box(mut self, safe: bool) -> Self {
2181        self.safe_box = Some(safe);
2182        self
2183    }
2184
2185    /// Set the initial console theme.
2186    #[must_use]
2187    pub fn theme(mut self, theme: Theme) -> Self {
2188        self.theme = Some(theme);
2189        self
2190    }
2191
2192    /// Set the output stream.
2193    #[must_use]
2194    pub fn file(mut self, writer: Box<dyn Write + Send>) -> Self {
2195        self.file = Some(writer);
2196        self
2197    }
2198
2199    /// Build the console.
2200    #[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        // Thread-safe buffer that implements Write + Send
2421        #[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, "&lt;&gt;&amp;&quot;'");
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("&lt;tag&gt;"));
2642        assert!(html.contains("&amp;"));
2643        assert!(html.contains("&quot;"));
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    // ========== ConsoleBuilder Tests ==========
2801
2802    #[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    // ========== Console Size Tests ==========
2872
2873    #[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        // Default should be reasonable terminal size
2885        assert!(console.width() >= 40);
2886        assert!(console.height() >= 10);
2887    }
2888
2889    // ========== PrintOptions Tests ==========
2890
2891    #[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    // ========== ConsoleOptions Tests ==========
2965
2966    #[test]
2967    fn test_console_options_update_width() {
2968        let console = Console::builder().width(100).build();
2969        let options = console.options();
2970        // update_width clamps to the new width (min of current and new)
2971        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        // update_height sets the height in the options
2980        let updated = options.update_height(50);
2981        assert_eq!(updated.height, Some(50));
2982    }
2983
2984    // ========== Color System Tests ==========
2985
2986    #[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    // ========== Capture Mode Tests ==========
3001
3002    #[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        // Should have captured at least one segment
3039        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    // ========== Print Method Tests ==========
3076
3077    #[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        // Should contain ANSI codes for bold
3137        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    // ========== Rule Method Tests ==========
3186
3187    #[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        // Rule should contain horizontal line characters
3214        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    // ========== Log Method Tests ==========
3247
3248    #[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    // ========== Log with Options Tests ==========
3365
3366    #[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        // Should contain timestamp format [HH:MM:SS]
3394        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('[')); // timestamp bracket
3462        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        // Default format: [HH:MM:SS]
3531        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        // Custom format: HH-MM-SS
3540        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        // We don't assert wall-clock values; we only assert the substitutions happened.
3548        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    // ========== Markup Integration Tests ==========
3554
3555    #[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        // Should contain ANSI codes, not literal [bold]
3584        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        // Should contain literal markup tags
3616        assert!(result.contains("[bold]"));
3617    }
3618
3619    // ========== Width Constraint Tests ==========
3620
3621    #[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        // Text should be wrapped at width 10
3638        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    // ========== ConsoleDimensions Tests ==========
3674
3675    #[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    // ========== PrintOptions Default Trait ==========
3693
3694    #[test]
3695    fn test_print_options_implements_default() {
3696        // Default::default() uses derived defaults (empty strings)
3697        // PrintOptions::new() sets explicit defaults (sep=" ", end="\n")
3698        let default_options = PrintOptions::default();
3699        assert_eq!(default_options.sep, "");
3700        assert_eq!(default_options.end, "");
3701
3702        // new() provides the typical defaults
3703        let new_options = PrintOptions::new();
3704        assert_eq!(new_options.sep, " ");
3705        assert_eq!(new_options.end, "\n");
3706    }
3707
3708    // ========== Edge Case Tests ==========
3709
3710    #[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        // Should only have newline
3737        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    // ========================================================================
3819    // Console I/O Error Path Tests (bd-3761)
3820    // ========================================================================
3821
3822    /// A writer that always fails on write
3823    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    /// A writer that fails on flush
3836    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    /// A writer that fails after N bytes
3858    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    /// A writer that tracks operations for verification
3886    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        // Test that write errors are properly propagated via print_to
3937        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        // Test writer that accepts only partial writes
3950        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        // May succeed partially or fail depending on implementation
3956        // The writer should have accepted at least some bytes
3957        assert!(limited.written > 0);
3958    }
3959
3960    #[test]
3961    fn test_io_flush_failure() {
3962        // Test that flush errors are handled
3963        let mut writer = FlushFailingWriter::new();
3964
3965        // Write should succeed
3966        let write_result = writer.write(b"Hello");
3967        assert!(write_result.is_ok());
3968        assert_eq!(write_result.unwrap(), 5);
3969
3970        // Flush should fail
3971        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        // Create segments
3982        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        // Verify writes are actually occurring
4007        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        // Should have multiple writes
4018        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        // Writing empty content should not cause errors
4025        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        // Empty string with no_newline should produce empty output
4032        assert!(output.is_empty() || output == b"\n");
4033    }
4034
4035    #[test]
4036    fn test_io_large_write() {
4037        // Test with a large string to ensure no buffer issues
4038        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        // Should contain all the content plus newline
4046        assert!(output.len() >= 10000);
4047    }
4048
4049    #[test]
4050    fn test_io_control_code_write_failure() {
4051        // Test that control code writes handle errors
4052        // Note: This tests internal behavior, so we use print_segments_to
4053        let console = Console::builder().width(80).markup(false).build();
4054
4055        // Create a segment with control codes
4056        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        // Should handle the error (either succeed because control codes are skipped
4066        // in non-terminal mode, or fail gracefully)
4067        // The important thing is no panic
4068        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        // Create writers with different error types
4135        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        // Verify different error types are preserved
4159        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        // Test thread-safe writes to shared buffer
4198        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        // Spawn multiple threads writing concurrently
4208        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        // Verify all writes completed
4222        let output = buffer.lock().unwrap();
4223        let text = String::from_utf8_lossy(&output);
4224        // All 4 threads should have written something
4225        assert!(text.contains("Thread"), "Expected thread output");
4226    }
4227
4228    #[test]
4229    fn test_io_interrupted_write() {
4230        // Test handling of interrupted writes (EINTR-like scenario)
4231        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        // Writer that returns Interrupted initially
4254        let attempts = Arc::new(Mutex::new(0));
4255        let mut writer = InterruptedWriter {
4256            attempts: Arc::clone(&attempts),
4257            succeed_after: 0, // Succeed on first try
4258        };
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}