Skip to main content

rusty_rich/
console.rs

1//! Console — the central rendering engine. Equivalent to Rich's `console.py`.
2//!
3//! The `Console` is the main entry point for rendering. It manages terminal
4//! detection, color system support, and dispatching renderables to produce
5//! styled output.
6
7use std::fmt;
8use std::io::{self, IsTerminal, Write};
9use std::sync::{Arc, Mutex};
10
11use crate::align::AlignMethod;
12use crate::color::{Color, ColorSystem};
13use crate::segment::Segment;
14use crate::style::Style;
15use crate::text::Text;
16use crate::theme::Theme;
17
18// ---------------------------------------------------------------------------
19// ConsoleDimensions
20// ---------------------------------------------------------------------------
21
22/// Size of the terminal in cells.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct ConsoleDimensions {
25    pub width: usize,
26    pub height: usize,
27}
28
29impl ConsoleDimensions {
30    /// Detect the terminal size, falling back to 80x25 if detection fails.
31    pub fn detect() -> Self {
32        if let Some((w, h)) = terminal_size::terminal_size() {
33            Self {
34                width: w.0 as usize,
35                height: h.0 as usize,
36            }
37        } else {
38            Self {
39                width: 80,
40                height: 25,
41            }
42        }
43    }
44}
45
46// ---------------------------------------------------------------------------
47// OverflowMethod
48// ---------------------------------------------------------------------------
49
50/// How to handle text that overflows the available width.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum OverflowMethod {
53    /// Wrap text onto the next line.
54    Fold,
55    /// Crop text at the boundary.
56    Crop,
57    /// Crop and append "…".
58    Ellipsis,
59    /// Let text overflow (don't clip).
60    Ignore,
61}
62
63// ---------------------------------------------------------------------------
64// ConsoleOptions
65// ---------------------------------------------------------------------------
66
67/// Options passed to renderables during rendering.
68#[derive(Debug, Clone)]
69pub struct ConsoleOptions {
70    /// Terminal size.
71    pub size: ConsoleDimensions,
72    /// True if output is a terminal.
73    pub is_terminal: bool,
74    /// The encoding (almost always UTF-8).
75    pub encoding: String,
76    /// Minimum render width.
77    pub min_width: usize,
78    /// Maximum render width.
79    pub max_width: usize,
80    /// Maximum height.
81    pub max_height: usize,
82    /// Override for text justification.
83    pub justify: Option<AlignMethod>,
84    /// Override for overflow handling.
85    pub overflow: Option<OverflowMethod>,
86    /// Disable text wrapping.
87    pub no_wrap: bool,
88    /// If true, use ASCII-only box characters.
89    pub ascii_only: bool,
90    /// If true, enable markup interpretation.
91    pub markup: bool,
92    /// If true, enable syntax highlighting of strings.
93    pub highlight: bool,
94    /// Optional fixed height for the renderable.
95    pub height: Option<usize>,
96    /// For legacy Windows console.
97    pub legacy_windows: bool,
98}
99
100impl Default for ConsoleOptions {
101    fn default() -> Self {
102        Self {
103            size: ConsoleDimensions::detect(),
104            is_terminal: true,
105            encoding: "utf-8".into(),
106            min_width: 1,
107            max_width: 80,
108            max_height: 25,
109            justify: None,
110            overflow: None,
111            no_wrap: false,
112            ascii_only: false,
113            markup: true,
114            highlight: true,
115            height: None,
116            legacy_windows: false,
117        }
118    }
119}
120
121impl ConsoleOptions {
122    /// Update the max width.
123    #[must_use]
124    pub fn update_width(&self, max_width: usize) -> Self {
125        let mut opts = self.clone();
126        opts.max_width = max_width;
127        opts
128    }
129
130    /// Update the height.
131    #[must_use]
132    pub fn update_height(&self, height: usize) -> Self {
133        let mut opts = self.clone();
134        opts.height = Some(height);
135        opts
136    }
137
138    /// Shrink the max width by an amount (for padding).
139    #[must_use]
140    pub fn shrink_width(&self, amount: usize) -> Self {
141        let mut opts = self.clone();
142        opts.max_width = opts.max_width.saturating_sub(amount);
143        opts
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Renderable trait
149// ---------------------------------------------------------------------------
150
151/// A single item in a render result — either a final `Segment` or a nested
152/// renderable that will be recursively flattened by `Console::render()`.
153///
154/// Equivalent to Python Rich's `RenderResult = Iterable[Union[Segment, RenderableType]]`.
155#[derive(Clone)]
156pub enum RenderItem {
157    /// A fully-rendered [`Segment`].
158    Segment(Segment),
159    /// A nested [`DynRenderable`] that will be recursively flattened.
160    Nested(DynRenderable),
161}
162
163impl fmt::Debug for RenderItem {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            Self::Segment(s) => write!(f, "Segment({})", &s.text),
167            Self::Nested(_) => write!(f, "Nested(...)"),
168        }
169    }
170}
171
172impl From<Segment> for RenderItem {
173    fn from(s: Segment) -> Self {
174        Self::Segment(s)
175    }
176}
177
178impl From<DynRenderable> for RenderItem {
179    fn from(r: DynRenderable) -> Self {
180        Self::Nested(r)
181    }
182}
183
184/// The result of rendering: a list of lines, each line being a list of
185/// segments.  Also carries an optional `items` list for recursive rendering.
186#[derive(Debug, Clone)]
187pub struct RenderResult {
188    /// Flat line-oriented segments (backward-compatible).
189    pub lines: Vec<Vec<Segment>>,
190    /// Optional render items for recursive flattening. When present,
191    /// `Console::render()` recurses into nested renderables.
192    pub items: Vec<RenderItem>,
193}
194
195impl Default for RenderResult {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl RenderResult {
202    /// Create an empty [`RenderResult`].
203    pub fn new() -> Self {
204        Self {
205            lines: Vec::new(),
206            items: Vec::new(),
207        }
208    }
209
210    /// Create a [`RenderResult`] from a plain text string.
211    ///
212    /// The text becomes a single line with one segment.
213    pub fn from_text(text: &str) -> Self {
214        Self {
215            lines: vec![vec![Segment::new(text)]],
216            items: vec![RenderItem::Segment(Segment::new(text))],
217        }
218    }
219
220    /// Create a [`RenderResult`] from a list of [`Segment`]s on a single line.
221    pub fn from_segments(segments: Vec<Segment>) -> Self {
222        let items: Vec<RenderItem> = segments
223            .iter()
224            .map(|s| RenderItem::Segment(s.clone()))
225            .collect();
226        Self {
227            lines: vec![segments],
228            items,
229        }
230    }
231
232    /// Create a [`RenderResult`] from pre-computed lines of [`Segment`]s.
233    pub fn from_lines(lines: Vec<Vec<Segment>>) -> Self {
234        Self {
235            lines,
236            items: Vec::new(),
237        }
238    }
239
240    /// Create a [`RenderResult`] from [`RenderItem`]s for recursive flattening.
241    pub fn from_items(items: Vec<RenderItem>) -> Self {
242        Self {
243            lines: Vec::new(),
244            items,
245        }
246    }
247
248    /// Push a segment item.
249    pub fn push_item(&mut self, item: impl Into<RenderItem>) {
250        self.items.push(item.into());
251    }
252
253    /// Push a nested renderable for recursive flattening.
254    pub fn push_renderable(&mut self, r: impl Renderable + Send + Sync + 'static) {
255        self.items.push(RenderItem::Nested(DynRenderable::new(r)));
256    }
257
258    /// Recursively flatten items into segments using the given options.
259    /// This is called by `Console::render()` to resolve nested renderables.
260    pub fn flatten(&self, options: &ConsoleOptions) -> Vec<Segment> {
261        let mut out: Vec<Segment> = Vec::new();
262        flatten_items(&self.items, options, &mut out);
263        // Also flatten lines for backward compat
264        if out.is_empty() {
265            for line in &self.lines {
266                for seg in line {
267                    out.push(seg.clone());
268                }
269            }
270        }
271        out
272    }
273
274    /// Flatten all segments into a single ANSI string.
275    pub fn to_ansi(&self) -> String {
276        let mut out = String::new();
277        // Use items if present, otherwise fall back to lines
278        if !self.items.is_empty() {
279            let flat = self.flatten(&ConsoleOptions::default());
280            for seg in &flat {
281                out.push_str(&seg.to_ansi());
282            }
283        } else {
284            for line in &self.lines {
285                for seg in line {
286                    out.push_str(&seg.to_ansi());
287                }
288            }
289        }
290        out
291    }
292}
293
294/// Recursively flatten `RenderItem`s into a `Vec<Segment>`.
295fn flatten_items(items: &[RenderItem], options: &ConsoleOptions, out: &mut Vec<Segment>) {
296    for item in items {
297        match item {
298            RenderItem::Segment(seg) => out.push(seg.clone()),
299            RenderItem::Nested(renderable) => {
300                let nested = renderable.render(options);
301                flatten_items(&nested.items, options, out);
302            }
303        }
304    }
305}
306
307/// Trait for anything that can be rendered to the console.
308///
309/// Equivalent to `__rich_console__` in Python Rich.
310pub trait Renderable {
311    /// Render this object into a [`RenderResult`] using the provided options.
312    ///
313    /// Implementing types produce [`Segment`]s or nested [`Renderable`]s
314    /// that are recursively flattened by [`Console::render`].
315    fn render(&self, options: &ConsoleOptions) -> RenderResult;
316
317    /// Optional width-measurement hook (equivalent to `__rich_measure__`).
318    /// Override to provide min/max width constraints for layout.
319    fn measure(&self, _options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
320        None
321    }
322}
323
324// -- Implementations for common types ---------------------------------------
325
326/// Allows a [`String`] to be used as a renderable.
327impl Renderable for String {
328    fn render(&self, options: &ConsoleOptions) -> RenderResult {
329        self.as_str().render(options)
330    }
331}
332
333/// Allows a [`&str`] to be used as a renderable (rendered as plain text).
334impl Renderable for &str {
335    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
336        RenderResult::from_text(self)
337    }
338}
339
340/// Allows a [`Text`] object to be used as a renderable.
341impl Renderable for Text {
342    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
343        let rendered = self.render();
344        // Simple: just treat the rendered ANSI string as one segment per line
345        let lines: Vec<Vec<Segment>> = rendered.lines().map(|l| vec![Segment::new(l)]).collect();
346        RenderResult {
347            lines,
348            items: Vec::new(),
349        }
350    }
351}
352
353/// A wrapper that provides `Clone` + `Debug` for trait-object renderables.
354///
355/// [`DynRenderable`] boxes any [`Renderable`] behind an [`Arc`] so it can be
356/// stored in collections like [`Group`] and [`Panel`](crate::Panel).
357#[derive(Clone)]
358pub struct DynRenderable {
359    inner: Arc<dyn Renderable + Send + Sync>,
360}
361
362impl DynRenderable {
363    /// Wrap a [`Renderable`] in a [`DynRenderable`].
364    pub fn new(r: impl Renderable + Send + Sync + 'static) -> Self {
365        Self { inner: Arc::new(r) }
366    }
367}
368
369impl fmt::Debug for DynRenderable {
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        f.debug_struct("DynRenderable").finish()
372    }
373}
374
375/// Delegates rendering to the inner trait object.
376impl Renderable for DynRenderable {
377    fn render(&self, options: &ConsoleOptions) -> RenderResult {
378        self.inner.render(options)
379    }
380
381    fn measure(&self, options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
382        self.inner.measure(options)
383    }
384}
385
386/// A renderable that renders multiple children one after another (vertically).
387///
388/// Equivalent to Python Rich's `Group`.
389#[derive(Debug, Clone)]
390pub struct Group {
391    /// The child renderables to render in sequence.
392    pub children: Vec<DynRenderable>,
393}
394
395impl Default for Group {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401impl Group {
402    /// Create an empty [`Group`].
403    pub fn new() -> Self {
404        Self {
405            children: Vec::new(),
406        }
407    }
408
409    /// Add a renderable child to the group.
410    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
411        self.children.push(DynRenderable::new(renderable));
412    }
413}
414
415/// Renders each child sequentially and concatenates their output lines.
416impl Renderable for Group {
417    fn render(&self, options: &ConsoleOptions) -> RenderResult {
418        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
419        for child in &self.children {
420            let result = child.render(options);
421            all_lines.extend(result.lines);
422        }
423        RenderResult {
424            lines: all_lines,
425            items: Vec::new(),
426        }
427    }
428}
429
430// ---------------------------------------------------------------------------
431// Capture system — redirect console output to a buffer
432// ---------------------------------------------------------------------------
433
434/// Private writer that captures output into a shared buffer.
435struct CaptureWriter {
436    buf: Arc<Mutex<Vec<u8>>>,
437}
438
439impl Write for CaptureWriter {
440    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
441        let mut data = self.buf.lock().unwrap();
442        data.extend_from_slice(buf);
443        Ok(buf.len())
444    }
445    fn flush(&mut self) -> io::Result<()> {
446        Ok(())
447    }
448}
449
450/// Captured console output. Created by [`Console::end_capture`].
451#[derive(Debug)]
452pub struct Capture {
453    buf: Arc<Mutex<Vec<u8>>>,
454}
455
456impl Capture {
457    /// Create an empty Capture (not connected to any console).
458    pub fn new(_console: &Console) -> Self {
459        Self {
460            buf: Arc::new(Mutex::new(Vec::new())),
461        }
462    }
463
464    /// Get the captured text.
465    pub fn get(&self) -> String {
466        let data = self.buf.lock().unwrap();
467        String::from_utf8_lossy(&data).to_string()
468    }
469}
470
471// Re-export pager types from the dedicated pager module
472pub use crate::pager::{Pager, PagerContext, SystemPager};
473
474// ---------------------------------------------------------------------------
475// CaptureError
476// ---------------------------------------------------------------------------
477
478/// Error type for capture operations.
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub enum CaptureError {
481    /// Capture is already in progress.
482    AlreadyCapturing,
483    /// No capture is currently active.
484    NotCapturing,
485    /// The captured output could not be decoded as UTF-8.
486    InvalidUtf8,
487}
488
489impl fmt::Display for CaptureError {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        match self {
492            Self::AlreadyCapturing => write!(f, "capture already in progress"),
493            Self::NotCapturing => write!(f, "no capture active"),
494            Self::InvalidUtf8 => write!(f, "captured output is not valid UTF-8"),
495        }
496    }
497}
498
499impl std::error::Error for CaptureError {}
500
501// ---------------------------------------------------------------------------
502// NewLine / NoChange renderables
503// ---------------------------------------------------------------------------
504
505/// A renderable that outputs a single newline.
506pub struct NewLine;
507
508impl Renderable for NewLine {
509    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
510        RenderResult::from_text("\n")
511    }
512}
513
514/// A renderable that outputs nothing (used as a sentinel).
515pub struct NoChange;
516
517impl Renderable for NoChange {
518    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
519        RenderResult::new()
520    }
521}
522
523// ---------------------------------------------------------------------------
524// RenderHook — modify render output before display
525// ---------------------------------------------------------------------------
526
527/// Type alias for the render hook closure.
528pub type RenderHookFn = dyn Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send;
529
530/// A hook that can modify render output before display.
531pub struct RenderHook {
532    hook: Box<RenderHookFn>,
533}
534
535impl RenderHook {
536    /// Create a new RenderHook from a closure.
537    pub fn new<F: Fn(&[Vec<Segment>]) -> Vec<Vec<Segment>> + Send + 'static>(f: F) -> Self {
538        Self { hook: Box::new(f) }
539    }
540
541    /// Apply the hook to a set of rendered lines.
542    pub fn apply(&self, lines: &[Vec<Segment>]) -> Vec<Vec<Segment>> {
543        (self.hook)(lines)
544    }
545}
546
547impl fmt::Debug for RenderHook {
548    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
549        f.debug_struct("RenderHook").finish()
550    }
551}
552
553// ---------------------------------------------------------------------------
554// ThemeContext — temporarily switch themes with RAII restoration
555// ---------------------------------------------------------------------------
556
557/// A RAII guard that restores a previous theme when dropped.
558///
559/// Created by [`Console::use_theme`]. While alive, the console uses the new
560/// theme. When the context is dropped, the original theme is restored.
561// SAFETY: The PhantomData<'a mut Console> ensures the compiler enforces that
562// Console outlives ThemeContext. The raw pointer is valid because:
563// 1. ThemeContext is not Send or Sync (raw pointer prevents auto-derive)
564// 2. The pointer comes from a &'a mut reference
565// 3. PhantomData links the borrow lifetime to ThemeContext's lifetime
566pub struct ThemeContext<'a> {
567    _phantom: std::marker::PhantomData<&'a mut Console>,
568    console_ptr: *mut Console,
569    previous_theme: Theme,
570    // Explicitly prevents Send + Sync auto-derive (VULN-004)
571    _not_send_sync: std::marker::PhantomData<*const ()>,
572}
573
574// SAFETY: ThemeContext is not Send or Sync because of the raw pointer.
575// It must only be used on the same thread as the Console.
576// The pointer is valid because Console creates ThemeContext and outlives it.
577
578impl<'a> ThemeContext<'a> {
579    /// Create a new ThemeContext (internal — use [`Console::use_theme`]).
580    pub(crate) fn new(console: &'a mut Console, previous_theme: Theme) -> Self {
581        Self {
582            _phantom: std::marker::PhantomData,
583            console_ptr: console as *mut Console,
584            previous_theme,
585            _not_send_sync: std::marker::PhantomData,
586        }
587    }
588}
589
590impl<'a> Drop for ThemeContext<'a> {
591    fn drop(&mut self) {
592        unsafe {
593            (*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
594        }
595    }
596}
597
598// ---------------------------------------------------------------------------
599// Console
600// ---------------------------------------------------------------------------
601
602/// The main console for rendering rich output.
603pub struct Console {
604    /// The output writer.
605    pub file: Box<dyn Write + Send>,
606    /// Detected color system.
607    pub color_system: ColorSystem,
608    /// Current theme.
609    pub theme: Theme,
610    /// Default options.
611    pub options: ConsoleOptions,
612    /// Current width (may be overridden).
613    width: Option<usize>,
614    /// Current height (may be overridden).
615    height: Option<usize>,
616    /// Is this output a terminal?
617    is_terminal: bool,
618    /// If true, suppress all output.
619    pub quiet: bool,
620    /// If true, text wraps at word boundaries.
621    pub soft_wrap: bool,
622    /// Is the alternate screen active?
623    alt_screen: bool,
624    /// Is the cursor visible?
625    cursor_visible: bool,
626    /// Active render hooks that modify output before display.
627    render_hooks: Vec<RenderHook>,
628    /// Captured output buffer (active when capturing).
629    capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
630    /// Original file writer saved during capture.
631    saved_file: Option<Box<dyn Write + Send>>,
632}
633
634impl Console {
635    /// Create a new Console writing to stdout.
636    pub fn new() -> Self {
637        let is_terminal = std::io::stdout().is_terminal();
638        let color_system = detect_color_system();
639
640        let size = ConsoleDimensions::detect();
641        // Subtract 1 column from the width to prevent line wrapping in
642        // terminals where output that exactly fills the last column causes
643        // the cursor to advance and wrap visually before the newline.
644        let render_width = size.width.saturating_sub(1);
645
646        Self {
647            file: Box::new(io::stdout()) as Box<dyn Write + Send>,
648            color_system,
649            theme: crate::theme::default_theme(),
650            options: ConsoleOptions {
651                size,
652                is_terminal,
653                max_width: render_width,
654                max_height: size.height,
655                ..Default::default()
656            },
657            width: None,
658            height: None,
659            is_terminal,
660            quiet: false,
661            soft_wrap: false,
662            alt_screen: false,
663            cursor_visible: true,
664            render_hooks: Vec::new(),
665            capture_buf: None,
666            saved_file: None,
667        }
668    }
669
670    /// Create a Console that writes to a file.
671    pub fn with_file(file: Box<dyn Write + Send>) -> Self {
672        let _is_terminal = false;
673        Self {
674            file,
675            color_system: ColorSystem::Standard,
676            theme: crate::theme::default_theme(),
677            options: ConsoleOptions {
678                size: ConsoleDimensions {
679                    width: 80,
680                    height: 25,
681                },
682                is_terminal: false,
683                max_width: 80,
684                max_height: 25,
685                ..Default::default()
686            },
687            width: None,
688            height: None,
689            is_terminal: false,
690            quiet: false,
691            soft_wrap: false,
692            alt_screen: false,
693            cursor_visible: true,
694            render_hooks: Vec::new(),
695            capture_buf: None,
696            saved_file: None,
697        }
698    }
699
700    /// Set the console width (overrides auto-detected terminal width).
701    pub fn set_width(&mut self, width: usize) {
702        self.width = Some(width);
703        self.options.max_width = width;
704    }
705
706    /// Set the console height.
707    pub fn set_height(&mut self, height: usize) {
708        self.height = Some(height);
709        self.options.max_height = height;
710    }
711
712    /// Get the effective width.
713    pub fn width(&self) -> usize {
714        self.width.unwrap_or(self.options.size.width)
715    }
716
717    /// Get the effective height.
718    pub fn height(&self) -> usize {
719        self.height.unwrap_or(self.options.size.height)
720    }
721
722    /// Render a renderable and return the segment lines.
723    pub fn render_lines(
724        &self,
725        renderable: &dyn Renderable,
726        options: &ConsoleOptions,
727        style: Option<&Style>,
728        _pad: bool,
729    ) -> Vec<Vec<Segment>> {
730        let result = renderable.render(options);
731
732        if let Some(st) = style {
733            result
734                .lines
735                .into_iter()
736                .map(|line| {
737                    line.into_iter()
738                        .map(|seg| {
739                            let new_style = if let Some(ref s) = seg.style {
740                                s.combine(st)
741                            } else {
742                                st.clone()
743                            };
744                            Segment::styled(seg.text, new_style)
745                        })
746                        .collect()
747                })
748                .collect()
749        } else {
750            result.lines
751        }
752    }
753
754    /// Look up a style by name from the theme.
755    pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
756        self.theme.get(name).cloned().or_else(|| {
757            if !default.is_empty() {
758                Some(Style::from_str(default))
759            } else {
760                None
761            }
762        })
763    }
764
765    /// Render a string (with optional style).
766    pub fn render_str(&self, text: &str, style: &str) -> Text {
767        let st = self.get_style(style, "");
768        let mut t = Text::new(text);
769        if let Some(s) = st {
770            t = t.style(s);
771        }
772        t
773    }
774
775    // -----------------------------------------------------------------------
776    // print / log methods
777    // -----------------------------------------------------------------------
778
779    /// Print one or more renderable objects, separated by `sep`, ending with
780    /// `end`.
781    pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
782        if self.quiet {
783            return;
784        }
785        let mut first = true;
786        for obj in objects {
787            if !first {
788                let _ = write!(self.file, "{sep}");
789            }
790            first = false;
791            let result = obj.render(&self.options);
792            let ansi = result.to_ansi();
793            let _ = write!(self.file, "{ansi}");
794        }
795        let _ = write!(self.file, "{end}");
796        let _ = self.file.flush();
797    }
798
799    /// Print a single renderable followed by a newline.
800    ///
801    /// Re-detects the terminal size on each call so that the output
802    /// adapts when the user resizes the terminal window.
803    pub fn println(&mut self, renderable: &dyn Renderable) {
804        if self.quiet {
805            return;
806        }
807        self.refresh_size();
808        let result = renderable.render(&self.options);
809        let ansi = result.to_ansi();
810        let _ = writeln!(self.file, "{ansi}");
811        let _ = self.file.flush();
812    }
813
814    /// Update `max_width` / `max_height` from the current terminal size.
815    fn refresh_size(&mut self) {
816        if self.is_terminal {
817            let size = ConsoleDimensions::detect();
818            self.options.size = size;
819            // Subtract 1 column to prevent edge-of-screen line wrapping.
820            self.options.max_width = size.width.saturating_sub(1);
821            self.options.max_height = size.height;
822        }
823    }
824
825    /// Print a plain string (supports markup by default when `markup` is
826    /// enabled).
827    pub fn print_str(&mut self, text: &str) {
828        if self.quiet {
829            return;
830        }
831        let ansi = if self.options.markup {
832            let parsed = crate::markup::render(text);
833            parsed.render()
834        } else {
835            // Strip raw ANSI escapes from plain text to prevent injection
836            crate::export::strip_ansi_escapes(text)
837        };
838        let _ = write!(self.file, "{ansi}");
839        let _ = self.file.flush();
840    }
841
842    /// Print formatted JSON.
843    pub fn print_json(&mut self, data: &serde_json::Value) {
844        if self.quiet {
845            return;
846        }
847        let formatted = crate::json::render_json(data);
848        let result = formatted.render(&self.options);
849        let ansi = result.to_ansi();
850        let _ = writeln!(self.file, "{ansi}");
851        let _ = self.file.flush();
852    }
853
854    /// Clear the screen.
855    pub fn clear(&mut self) {
856        if self.quiet {
857            return;
858        }
859        let _ = write!(self.file, "{}", crate::control::CLEAR_HOME);
860        let _ = self.file.flush();
861    }
862
863    /// Show the cursor.
864    pub fn show_cursor(&mut self) {
865        self.cursor_visible = true;
866        let _ = write!(self.file, "{}", crate::control::CURSOR_SHOW);
867        let _ = self.file.flush();
868    }
869
870    /// Hide the cursor.
871    pub fn hide_cursor(&mut self) {
872        self.cursor_visible = false;
873        let _ = write!(self.file, "{}", crate::control::CURSOR_HIDE);
874        let _ = self.file.flush();
875    }
876
877    /// Set the terminal window title.
878    pub fn set_window_title(&mut self, title: &str) {
879        let _ = write!(
880            self.file,
881            "{}{}{}",
882            crate::control::OSC,
883            title,
884            crate::control::ST
885        );
886        let _ = self.file.flush();
887    }
888
889    /// Get the ANSI escape string for a given color as this console supports.
890    pub fn color_ansi(&self, color: &Color) -> String {
891        let downgraded = color.downgrade(self.color_system);
892        downgraded.to_string()
893    }
894
895    // -- Recursive rendering ------------------------------------------------
896
897    /// Render a renderable by recursively flattening nested items into
898    /// segments.  This is equivalent to Python Rich's `Console.render()`.
899    /// It handles `Group` composition and any renderable that yields other
900    /// renderables.
901    pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
902        let result = renderable.render(options);
903        result.flatten(options)
904    }
905
906    /// Measure a renderable's width constraints.
907    /// Equivalent to Python Rich's `Measurement.get(console, options, renderable)`.
908    pub fn measure(
909        &self,
910        renderable: &dyn Renderable,
911        options: &ConsoleOptions,
912    ) -> crate::measure::Measurement {
913        if let Some(m) = renderable.measure(options) {
914            return m;
915        }
916        let segments = self.render(renderable, options);
917        let max_w = segments.iter().map(|s| s.cell_length()).max().unwrap_or(0);
918        crate::measure::Measurement::new(max_w, options.max_width)
919    }
920
921    // -- Convenience render methods -----------------------------------------
922
923    /// Render a rule with the given title.
924    /// Equivalent to `Console.rule()`.
925    pub fn rule(
926        &mut self,
927        title: impl Into<String>,
928        characters: Option<&str>,
929        style: Option<Style>,
930        align: Option<AlignMethod>,
931    ) {
932        if self.quiet {
933            return;
934        }
935        let mut rule = crate::rule::Rule::new().title(title);
936        if let Some(chars) = characters {
937            rule = rule.characters(chars);
938        }
939        if let Some(st) = style {
940            rule = rule.style(st);
941        }
942        if let Some(a) = align {
943            rule = rule.align(a);
944        }
945        let result = rule.render(&self.options);
946        let ansi = result.to_ansi();
947        let _ = write!(self.file, "{ansi}");
948        let _ = self.file.flush();
949    }
950
951    /// Output a bell character.
952    pub fn bell(&mut self) {
953        if self.quiet {
954            return;
955        }
956        let _ = write!(self.file, "\x07");
957        let _ = self.file.flush();
958    }
959
960    /// Output blank lines.
961    pub fn line(&mut self, count: usize) {
962        if self.quiet {
963            return;
964        }
965        for _ in 0..count {
966            let _ = writeln!(self.file);
967        }
968        let _ = self.file.flush();
969    }
970
971    /// Output a log entry with timestamp, caller info.
972    pub fn log(&mut self, objects: &[&dyn Renderable]) {
973        if self.quiet {
974            return;
975        }
976        let now = chrono::Local::now();
977        let time_str = format!("[{}]", now.format("%H:%M:%S"));
978        let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
979        let _ = write!(self.file, "{time_str} ");
980        let _ = write!(self.file, "{}", Style::new().reset_ansi());
981        self.print(objects, " ", "\n");
982    }
983
984    // -- Theme stack --------------------------------------------------------
985
986    /// Push a theme onto the stack.
987    pub fn push_theme(&mut self, theme: Theme) {
988        let mut new_theme = theme.clone();
989        new_theme.inherit = Some(Box::new(self.theme.clone()));
990        self.theme = new_theme;
991    }
992
993    /// Pop the current theme, restoring the previous one.
994    pub fn pop_theme(&mut self) {
995        if let Some(ref inherit) = self.theme.inherit {
996            self.theme = *inherit.clone();
997        }
998    }
999
1000    // -- Export methods ------------------------------------------------------
1001
1002    /// Export the current console output as an HTML document.
1003    ///
1004    /// Renders the given renderable and wraps it in a styled HTML page.
1005    /// Export the current console output as an HTML document.
1006    ///
1007    /// Renders the given renderable to segments, converts to styled HTML spans,
1008    /// and wraps in a full HTML document. Colors and styles are preserved.
1009    pub fn export_html(&self, renderable: &dyn Renderable) -> String {
1010        let segments = self.render(renderable, &self.options);
1011        let code =
1012            crate::export::segments_to_html(&segments, &crate::export::ExportTheme::default());
1013        crate::export::export_html(&crate::export::ExportHtmlOptions {
1014            code,
1015            ..Default::default()
1016        })
1017    }
1018
1019    /// Save rendered output as an HTML file.
1020    pub fn save_html(
1021        &self,
1022        path: impl AsRef<std::path::Path>,
1023        renderable: &dyn Renderable,
1024    ) -> std::io::Result<()> {
1025        let html = self.export_html(renderable);
1026        std::fs::write(path.as_ref(), html)
1027    }
1028
1029    /// Export the current console output as an SVG document.
1030    /// Export the current console output as an SVG document.
1031    ///
1032    /// Renders the given renderable to segments, converts to styled SVG
1033    /// `<tspan>` elements, and wraps in a full SVG document.
1034    pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
1035        let segments = self.render(renderable, &self.options);
1036        let code =
1037            crate::export::segments_to_svg(&segments, &crate::export::ExportTheme::default());
1038        crate::export::export_svg(&crate::export::ExportSvgOptions {
1039            code,
1040            ..Default::default()
1041        })
1042    }
1043
1044    /// Save rendered output as an SVG file.
1045    pub fn save_svg(
1046        &self,
1047        path: impl AsRef<std::path::Path>,
1048        renderable: &dyn Renderable,
1049    ) -> std::io::Result<()> {
1050        let svg = self.export_svg(renderable);
1051        crate::export::save_svg(
1052            path,
1053            &crate::export::ExportSvgOptions {
1054                code: svg,
1055                ..Default::default()
1056            },
1057        )
1058    }
1059
1060    /// Export the current console output as plain text (strips ANSI).
1061    pub fn export_text(&self, renderable: &dyn Renderable) -> String {
1062        let result = renderable.render(&self.options);
1063        let ansi = result.to_ansi();
1064        crate::export::export_text(&crate::export::ExportTextOptions {
1065            text: ansi,
1066            strip_ansi: true,
1067        })
1068    }
1069
1070    /// Save rendered output as a plain text file.
1071    pub fn save_text(
1072        &self,
1073        path: impl AsRef<std::path::Path>,
1074        renderable: &dyn Renderable,
1075    ) -> std::io::Result<()> {
1076        let text = self.export_text(renderable);
1077        crate::export::save_text(
1078            path,
1079            &crate::export::ExportTextOptions {
1080                text,
1081                strip_ansi: false,
1082            },
1083        )
1084    }
1085
1086    // -- Quiet / Soft-wrap setters ------------------------------------------
1087
1088    /// Set the quiet flag (suppress all output when true).
1089    pub fn set_quiet(&mut self, quiet: bool) {
1090        self.quiet = quiet;
1091    }
1092
1093    /// Builder-style setter for quiet.
1094    pub fn quiet(mut self, quiet: bool) -> Self {
1095        self.quiet = quiet;
1096        self
1097    }
1098
1099    /// Set the soft-wrap flag (wrap text at word boundaries when true).
1100    pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
1101        self.soft_wrap = soft_wrap;
1102    }
1103
1104    /// Builder-style setter for soft_wrap.
1105    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
1106        self.soft_wrap = soft_wrap;
1107        self
1108    }
1109
1110    // -- Input --------------------------------------------------------------
1111
1112    /// Read a line of input from the user.
1113    ///
1114    /// Writes `prompt` to the console, then reads a line from stdin.
1115    /// When `password` is true, the input is masked with `*` characters
1116    /// (using raw terminal mode via crossterm).
1117    pub fn input(&mut self, prompt: &str, password: bool) -> String {
1118        let _ = write!(self.file, "{prompt}");
1119        let _ = self.file.flush();
1120
1121        if password {
1122            self.read_password()
1123        } else {
1124            let mut input = String::new();
1125            let _ = io::stdin().read_line(&mut input);
1126            input.trim().to_string()
1127        }
1128    }
1129
1130    /// Read a password from stdin with character masking.
1131    fn read_password(&mut self) -> String {
1132        use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
1133        use std::io::Read;
1134
1135        match enable_raw_mode() {
1136            Ok(()) => {
1137                let stdin = io::stdin();
1138                let mut handle = stdin.lock();
1139                let mut buf = [0u8; 1];
1140                let mut password = String::new();
1141
1142                while let Ok(()) = handle.read_exact(&mut buf) {
1143                    match buf[0] {
1144                        b'\r' | b'\n' => {
1145                            let _ = writeln!(self.file);
1146                            let _ = self.file.flush();
1147                            break;
1148                        }
1149                        b'\x03' => {
1150                            // Ctrl+C — break and return what we have
1151                            let _ = writeln!(self.file);
1152                            let _ = self.file.flush();
1153                            break;
1154                        }
1155                        b'\x7f' | b'\x08' => {
1156                            // Backspace
1157                            password.pop();
1158                        }
1159                        c => {
1160                            password.push(c as char);
1161                            let _ = write!(self.file, "*");
1162                            let _ = self.file.flush();
1163                        }
1164                    }
1165                }
1166                let _ = disable_raw_mode();
1167                password
1168            }
1169            Err(_) => {
1170                // Fallback: read without masking
1171                let mut input = String::new();
1172                let _ = io::stdin().read_line(&mut input);
1173                input.trim().to_string()
1174            }
1175        }
1176    }
1177
1178    // -- Screen / alternate screen ------------------------------------------
1179
1180    /// Create a [`ScreenContext`](crate::screen::ScreenContext) that enters the
1181    /// alternate screen buffer. The context automatically exits the alternate
1182    /// screen when dropped.
1183    pub fn screen(&mut self) -> crate::screen::ScreenContext {
1184        let mut ctx = crate::screen::ScreenContext::new();
1185        ctx.enter();
1186        ctx
1187    }
1188
1189    /// Enter or exit the alternate screen buffer by writing the corresponding
1190    /// escape sequences [`crate::control::ALT_SCREEN_ENTER`] / [`crate::control::ALT_SCREEN_EXIT`].
1191    pub fn set_alt_screen(&mut self, enable: bool) {
1192        self.alt_screen = enable;
1193        let seq = if enable {
1194            crate::control::ALT_SCREEN_ENTER
1195        } else {
1196            crate::control::ALT_SCREEN_EXIT
1197        };
1198        let _ = write!(self.file, "{seq}");
1199        let _ = self.file.flush();
1200    }
1201
1202    /// Get whether the output is a terminal.
1203    pub fn is_terminal(&self) -> bool {
1204        self.is_terminal
1205    }
1206
1207    /// Set the terminal size (overrides auto-detected dimensions).
1208    pub fn set_size(&mut self, width: usize, height: usize) {
1209        self.width = Some(width);
1210        self.height = Some(height);
1211        self.options.max_width = width;
1212        self.options.max_height = height;
1213        self.options.size = crate::console::ConsoleDimensions { width, height };
1214    }
1215
1216    /// Handle broken pipe errors gracefully.
1217    ///
1218    /// In Rust, `write()` returns `ErrorKind::BrokenPipe` instead of raising
1219    /// `SIGPIPE`, so broken pipes are not fatal. The Console already uses
1220    /// `let _ = write!(...)` throughout, which silently discards all write
1221    /// errors including EPIPE. This method is provided for API compatibility
1222    /// with Python Rich and as a documentation point.
1223    pub fn on_broken_pipe(&self) {
1224        // No-op: Rust handles EPIPE via ErrorKind, not signals.
1225        // All Console write operations use `let _ = write!()` which
1226        // already discards BrokenPipe errors without panicking.
1227    }
1228}
1229
1230impl Default for Console {
1231    fn default() -> Self {
1232        Self::new()
1233    }
1234}
1235
1236impl fmt::Debug for Console {
1237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1238        f.debug_struct("Console")
1239            .field("color_system", &self.color_system)
1240            .field("width", &self.width())
1241            .field("height", &self.height())
1242            .field("is_terminal", &self.is_terminal)
1243            .field("alt_screen", &self.alt_screen)
1244            .field("cursor_visible", &self.cursor_visible)
1245            .field("quiet", &self.quiet)
1246            .field("soft_wrap", &self.soft_wrap)
1247            .finish()
1248    }
1249}
1250
1251// ===========================================================================
1252// New feature methods (Capture, Pager, Terminal Control, Hooks, etc.)
1253// ===========================================================================
1254
1255impl Console {
1256    // -- Capture System ------------------------------------------------------
1257
1258    /// Start capturing all output. All subsequent writes to this console are
1259    /// redirected to an internal buffer. Call [`end_capture`](Self::end_capture)
1260    /// to stop capturing and retrieve the captured content.
1261    pub fn begin_capture(&mut self) {
1262        let buf = Arc::new(Mutex::new(Vec::new()));
1263        let writer = Box::new(CaptureWriter { buf: buf.clone() });
1264        self.saved_file = Some(std::mem::replace(&mut self.file, writer));
1265        self.capture_buf = Some(buf);
1266    }
1267
1268    /// End capture mode and return the [`Capture`] containing all output written
1269    /// while capturing was active. The console's output is restored to its
1270    /// original destination.
1271    pub fn end_capture(&mut self) -> Result<Capture, CaptureError> {
1272        let buf = self.capture_buf.take().ok_or(CaptureError::NotCapturing)?;
1273        if let Some(saved) = self.saved_file.take() {
1274            self.file = saved;
1275        }
1276        Ok(Capture { buf })
1277    }
1278
1279    /// Run the given closure with output captured, returning the captured text.
1280    ///
1281    /// This is the most ergonomic way to capture output in Rust:
1282    ///
1283    /// ```rust,no_run
1284    /// # use rusty_rich::Console;
1285    /// let mut console = Console::new();
1286    /// let output = console.capture(|c| {
1287    ///     c.print_str("Hello, world!");
1288    /// }).unwrap();
1289    /// assert_eq!(output, "Hello, world!");
1290    /// ```
1291    pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> Result<String, CaptureError> {
1292        self.begin_capture();
1293        f(self);
1294        self.end_capture().map(|cap| cap.get())
1295    }
1296
1297    // -- Pager System --------------------------------------------------------
1298
1299    /// Get a [`PagerContext`]. Content rendered while the context is alive is
1300    /// collected and displayed through the system pager (`$PAGER` or `less`)
1301    /// when the context is dropped.
1302    ///
1303    /// `styles` controls whether ANSI styles are preserved when paging.
1304    pub fn pager(&mut self, styles: bool) -> PagerContext {
1305        PagerContext::new(Pager::new().color(styles))
1306    }
1307
1308    // -- Input with Renderable prompt ----------------------------------------
1309
1310    /// Display a [`Renderable`] prompt and read a line of input from stdin.
1311    ///
1312    /// The prompt is rendered through the console's current options and theme.
1313    pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
1314        if !self.quiet {
1315            let result = prompt.render(&self.options);
1316            let ansi = result.to_ansi();
1317            let _ = write!(self.file, "{ansi}");
1318            let _ = self.file.flush();
1319        }
1320        let mut input = String::new();
1321        let _ = io::stdin().read_line(&mut input);
1322        input.trim().to_string()
1323    }
1324
1325    // -- Exception / Traceback -----------------------------------------------
1326
1327    /// Print the current exception as a rich traceback.
1328    ///
1329    /// In Rust, this is a best-effort rendering; it captures the current
1330    /// thread's panic info if available. `width` overrides the output width,
1331    /// and `extra_lines` controls how many lines of source context to show
1332    /// around each frame.
1333    pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
1334        if self.quiet {
1335            return;
1336        }
1337        // Note: Rust does not have Python's sys.exc_info(). A full traceback
1338        // renderer would need std::panic::catch_unwind or custom error capture.
1339        // This method provides the API surface; for actual panic tracebacks
1340        // see crate::traceback::install().
1341        let msg = "[bold red]Exception[/bold red]: No current exception info. ";
1342        let msg_text = crate::text::Text::from_markup(msg);
1343        let result = msg_text.render();
1344        let _ = writeln!(self.file, "{result}");
1345        let _ = self.file.flush();
1346    }
1347
1348    // -- JSON pretty-print (string overload) -----------------------------------
1349
1350    /// Pretty-print a JSON string. Parses the string and renders it with
1351    /// syntax highlighting.
1352    pub fn print_json_str(&mut self, json: &str) {
1353        if self.quiet {
1354            return;
1355        }
1356        if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
1357            self.print_json(&value);
1358        } else {
1359            let _ = writeln!(self.file, "[invalid JSON]");
1360            let _ = self.file.flush();
1361        }
1362    }
1363
1364    // -- Render lines (simple version) ----------------------------------------
1365
1366    /// Render a renderable to a vector of segment lines.
1367    ///
1368    /// This is the lower-level render entry point, returning raw lines instead
1369    /// of an ANSI string. Compare with [`render`](Self::render) which returns
1370    /// flat segments.
1371    pub fn render_to_lines(
1372        &self,
1373        renderable: &dyn Renderable,
1374        options: &ConsoleOptions,
1375    ) -> Vec<Vec<Segment>> {
1376        let result = renderable.render(options);
1377        let has_items = !result.items.is_empty();
1378        let mut lines = if result.lines.is_empty() && has_items {
1379            let flat = result.flatten(options);
1380            if flat.is_empty() {
1381                Vec::new() // also empty after flatten — keep empty
1382            } else {
1383                vec![flat]
1384            }
1385        } else {
1386            result.lines
1387        };
1388        // Apply any render hooks
1389        if !self.render_hooks.is_empty() {
1390            for hook in &self.render_hooks {
1391                lines = hook.apply(&lines);
1392            }
1393        }
1394        lines
1395    }
1396
1397    // -- Render ANSI string ---------------------------------------------------
1398
1399    /// Render a plain string to ANSI text, applying the current theme and
1400    /// styles. Returns the ANSI-formatted string.
1401    pub fn render_ansi(&self, text: &str) -> String {
1402        let t = self.render_str(text, "");
1403        t.render()
1404    }
1405
1406    // -- Export SVG with options ----------------------------------------------
1407
1408    /// Export the console output as an SVG document with explicit options.
1409    ///
1410    /// This delegates to [`crate::export::export_svg`] with the given
1411    /// [`ExportSvgOptions`](crate::export::ExportSvgOptions).
1412    pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
1413        crate::export::export_svg(options)
1414    }
1415
1416    // -- Console Properties ---------------------------------------------------
1417
1418    /// Get the terminal size as [`ConsoleDimensions`].
1419    pub fn size(&self) -> ConsoleDimensions {
1420        ConsoleDimensions {
1421            width: self.width(),
1422            height: self.height(),
1423        }
1424    }
1425
1426    /// Check if the terminal is a "dumb" terminal (no color support).
1427    pub fn is_dumb_terminal(&self) -> bool {
1428        std::env::var("TERM").is_ok_and(|t| t == "dumb")
1429    }
1430
1431    /// Check if the console is currently in the alternate screen buffer.
1432    pub fn is_alt_screen(&self) -> bool {
1433        self.alt_screen
1434    }
1435
1436    // -- Terminal Control ----------------------------------------------------
1437
1438    /// Show or hide the cursor based on the boolean parameter.
1439    ///
1440    /// `true` shows the cursor, `false` hides it. Tracks the current state
1441    /// so it can be queried via internal fields.
1442    pub fn set_cursor_visible(&mut self, visible: bool) {
1443        self.cursor_visible = visible;
1444        let seq = if visible {
1445            crate::control::CURSOR_SHOW
1446        } else {
1447            crate::control::CURSOR_HIDE
1448        };
1449        let _ = write!(self.file, "{seq}");
1450        let _ = self.file.flush();
1451    }
1452
1453    /// Temporarily switch to a different theme. Returns a [`ThemeContext`]
1454    /// that restores the original theme when dropped.
1455    ///
1456    /// # Example
1457    ///
1458    /// ```rust,no_run
1459    /// # use rusty_rich::{Console, Theme};
1460    /// let mut console = Console::new();
1461    /// let custom = Theme::new();
1462    /// {
1463    ///     let _ctx = console.use_theme(custom);
1464    ///     // console uses custom theme here
1465    /// }
1466    /// // original theme restored here
1467    /// ```
1468    pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
1469        let prev = std::mem::replace(&mut self.theme, theme);
1470        ThemeContext::new(self, prev)
1471    }
1472
1473    /// Clear the live display region. When in alt-screen mode, this clears
1474    /// the entire alternate screen. Otherwise, it's equivalent to
1475    /// [`clear`](Self::clear).
1476    pub fn clear_live(&mut self) {
1477        let _ = write!(self.file, "{}", crate::control::CLEAR_HOME);
1478        let _ = self.file.flush();
1479    }
1480
1481    /// Set the active live display. Stores a reference to the
1482    /// [`Live`](crate::live::Live) renderer for integration with the
1483    /// console's rendering pipeline.
1484    ///
1485    /// Note: [`Live`](crate::live::Live) manages its own refresh cycle;
1486    /// this method is primarily for API compatibility with Python Rich.
1487    pub fn set_live(&mut self, _live: &crate::live::Live) {
1488        // Live manages its own refresh cycle; this method provides the
1489        // API surface for attaching a live display to the console.
1490    }
1491
1492    /// Update the full screen (enter alt-screen, render content, exit).
1493    ///
1494    /// Clears the screen and renders the given renderable. If `options` is
1495    /// `None`, the console's current options are used.
1496    pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
1497        let opts = options.unwrap_or(&self.options);
1498        let segments = self.render(renderable, opts);
1499        let mut output = String::new();
1500        for seg in &segments {
1501            output.push_str(&seg.to_ansi());
1502        }
1503        let _ = write!(self.file, "{}{output}", crate::control::CLEAR_HOME);
1504        let _ = self.file.flush();
1505    }
1506
1507    /// Update the screen from pre-rendered lines of segments.
1508    ///
1509    /// Takes already-rendered lines and displays them as the full screen
1510    /// content, clearing existing content first.
1511    pub fn update_screen_lines(
1512        &mut self,
1513        lines: &[Vec<Segment>],
1514        options: Option<&ConsoleOptions>,
1515    ) {
1516        let _ = options;
1517        let mut output = String::new();
1518        for line in lines {
1519            for seg in line {
1520                output.push_str(&seg.to_ansi());
1521            }
1522            output.push('\n');
1523        }
1524        let _ = write!(self.file, "{}{output}", crate::control::CLEAR_HOME);
1525        let _ = self.file.flush();
1526    }
1527
1528    // -- Render Hooks --------------------------------------------------------
1529
1530    /// Add a [`RenderHook`] to the console. Hooks are applied in order and
1531    /// can modify the rendered lines before they are displayed.
1532    pub fn push_render_hook(&mut self, hook: RenderHook) {
1533        self.render_hooks.push(hook);
1534    }
1535
1536    /// Remove and return the most recently added [`RenderHook`], if any.
1537    pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
1538        self.render_hooks.pop()
1539    }
1540}
1541
1542// ---------------------------------------------------------------------------
1543// Color system detection
1544// ---------------------------------------------------------------------------
1545
1546/// Detect the terminal color system from environment variables.
1547///
1548/// Checks `COLORTERM`, `TERM`, `NO_COLOR`, and `CLICOLOR` to determine
1549/// whether the terminal supports true color, 8-bit, or standard 16 colors.
1550fn detect_color_system() -> ColorSystem {
1551    // Check common env vars
1552    if let Ok(val) = std::env::var("COLORTERM") {
1553        if val == "truecolor" || val == "24bit" {
1554            return ColorSystem::TrueColor;
1555        }
1556    }
1557    if let Ok(term) = std::env::var("TERM") {
1558        if term.contains("256color") {
1559            return ColorSystem::EightBit;
1560        }
1561        if term == "xterm-kitty" {
1562            return ColorSystem::TrueColor;
1563        }
1564    }
1565    // Check NO_COLOR / CLICOLOR
1566    if std::env::var("NO_COLOR").is_ok() {
1567        return ColorSystem::Standard;
1568    }
1569    // Default to true color on modern terminals
1570    if std::io::stdout().is_terminal() {
1571        ColorSystem::TrueColor
1572    } else {
1573        ColorSystem::Standard
1574    }
1575}
1576
1577// ---------------------------------------------------------------------------
1578// Global console instance (like Rich's `get_console()`)
1579// ---------------------------------------------------------------------------
1580
1581use std::sync::LazyLock;
1582
1583static GLOBAL_CONSOLE: LazyLock<Mutex<Console>> = LazyLock::new(|| Mutex::new(Console::new()));
1584
1585/// Get a reference to the global Console.
1586pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1587    GLOBAL_CONSOLE.lock().unwrap_or_else(|e| e.into_inner())
1588}
1589
1590// ---------------------------------------------------------------------------
1591// Convenience functions (like Rich's `print()`)
1592// ---------------------------------------------------------------------------
1593
1594/// Print objects using the global console.
1595pub fn print_objects(objects: &[&dyn Renderable]) {
1596    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1597    console.print(objects, " ", "\n");
1598}
1599
1600/// Print a string with markup support.
1601pub fn print_str(text: &str) {
1602    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1603    console.print_str(text);
1604}
1605
1606/// Print formatted JSON.
1607pub fn print_json_val(data: &serde_json::Value) {
1608    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1609    console.print_json(data);
1610}
1611
1612// ---------------------------------------------------------------------------
1613// Reconfigure global console
1614// ---------------------------------------------------------------------------
1615
1616/// Reconfigure the global Console singleton with new dimensions and/or
1617/// color system. This updates the shared global console instance used by
1618/// [`print_objects`], [`print_str`], and [`print_json_val`].
1619///
1620/// # Parameters
1621///
1622/// * `width` — New terminal width (None to keep current).
1623/// * `height` — New terminal height (None to keep current).
1624/// * `color_system` — New color system level (None to keep current).
1625pub fn reconfigure(width: Option<usize>, height: Option<usize>, color_system: Option<ColorSystem>) {
1626    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1627    if let Some(w) = width {
1628        console.set_width(w);
1629    }
1630    if let Some(h) = height {
1631        console.set_height(h);
1632    }
1633    if let Some(cs) = color_system {
1634        console.color_system = cs;
1635    }
1636}
1637
1638#[cfg(test)]
1639mod tests {
1640    use super::*;
1641
1642    #[test]
1643    fn test_render_result_from_text() {
1644        let r = RenderResult::from_text("hello");
1645        assert_eq!(r.lines.len(), 1);
1646        assert_eq!(r.lines[0][0].text, "hello");
1647    }
1648
1649    #[test]
1650    fn test_console_options_default() {
1651        let opts = ConsoleOptions::default();
1652        assert!(opts.markup);
1653    }
1654
1655    #[test]
1656    fn test_console_quiet_default() {
1657        let console = Console::new();
1658        assert!(!console.quiet);
1659    }
1660
1661    #[test]
1662    fn test_console_quiet_setter() {
1663        let mut console = Console::new();
1664        console.set_quiet(true);
1665        assert!(console.quiet);
1666    }
1667
1668    #[test]
1669    fn test_console_quiet_builder() {
1670        let console = Console::new().quiet(true);
1671        assert!(console.quiet);
1672    }
1673
1674    #[test]
1675    fn test_console_quiet_suppresses_print() {
1676        let mut console = Console::new();
1677        console.quiet = true;
1678        // Should not panic
1679        console.print(&[], " ", "\n");
1680        console.println(&"test");
1681        console.print_str("test");
1682    }
1683
1684    #[test]
1685    fn test_console_soft_wrap_default() {
1686        let console = Console::new();
1687        assert!(!console.soft_wrap);
1688    }
1689
1690    #[test]
1691    fn test_console_soft_wrap_setter() {
1692        let mut console = Console::new();
1693        console.set_soft_wrap(true);
1694        assert!(console.soft_wrap);
1695    }
1696
1697    #[test]
1698    fn test_console_soft_wrap_builder() {
1699        let console = Console::new().soft_wrap(true);
1700        assert!(console.soft_wrap);
1701    }
1702
1703    #[test]
1704    fn test_console_is_terminal() {
1705        let console = Console::new();
1706        // is_terminal depends on whether stdout is a terminal
1707        let detected = console.is_terminal();
1708        assert_eq!(detected, std::io::stdout().is_terminal());
1709    }
1710
1711    #[test]
1712    fn test_console_set_size() {
1713        let mut console = Console::new();
1714        console.set_size(120, 30);
1715        assert_eq!(console.width(), 120);
1716        assert_eq!(console.height(), 30);
1717        assert_eq!(console.options.max_width, 120);
1718        assert_eq!(console.options.max_height, 30);
1719    }
1720
1721    #[test]
1722    fn test_console_set_alt_screen() {
1723        let mut console = Console::new();
1724        // Just ensure it doesn't panic
1725        console.set_alt_screen(true);
1726        console.set_alt_screen(false);
1727    }
1728
1729    #[test]
1730    fn test_console_on_broken_pipe() {
1731        let console = Console::new();
1732        console.on_broken_pipe(); // no-op
1733    }
1734
1735    #[test]
1736    fn test_console_input_normal() {
1737        // We can't easily test stdin in unit tests, but we can verify
1738        // the method signature compiles and matches.
1739        let _console = Console::new();
1740        // input() cannot be meaningfully tested without actual stdin.
1741    }
1742
1743    #[test]
1744    fn test_console_debug() {
1745        let console = Console::new();
1746        let debug = format!("{:?}", console);
1747        assert!(debug.contains("Console"));
1748    }
1749
1750    #[test]
1751    fn test_console_with_file_has_no_terminal() {
1752        let console = Console::with_file(Box::new(std::io::sink()));
1753        assert!(!console.is_terminal());
1754    }
1755
1756    // -- New feature tests ---------------------------------------------------
1757
1758    #[test]
1759    fn test_newline_renderable() {
1760        let nl = NewLine;
1761        let result = nl.render(&ConsoleOptions::default());
1762        let ansi = result.to_ansi();
1763        assert_eq!(ansi, "\n");
1764    }
1765
1766    #[test]
1767    fn test_nochange_renderable() {
1768        let nc = NoChange;
1769        let result = nc.render(&ConsoleOptions::default());
1770        assert!(result.lines.is_empty());
1771        assert!(result.items.is_empty());
1772    }
1773
1774    #[test]
1775    fn test_capture_begin_end() {
1776        let mut console = Console::with_file(Box::new(std::io::sink()));
1777        console.begin_capture();
1778        let _ = write!(console.file, "captured text");
1779        let cap = console.end_capture().unwrap();
1780        assert_eq!(cap.get(), "captured text");
1781    }
1782
1783    #[test]
1784    fn test_capture_with_closure() {
1785        let mut console = Console::with_file(Box::new(std::io::sink()));
1786        let output = console
1787            .capture(|c| {
1788                let _ = write!(c.file, "hello from capture");
1789            })
1790            .unwrap();
1791        assert_eq!(output, "hello from capture");
1792    }
1793
1794    #[test]
1795    fn test_capture_new_empty() {
1796        let console = Console::new();
1797        let cap = Capture::new(&console);
1798        assert_eq!(cap.get(), "");
1799    }
1800
1801    #[test]
1802    fn test_end_capture_not_capturing() {
1803        let mut console = Console::new();
1804        let result = console.end_capture();
1805        assert!(result.is_err());
1806        assert_eq!(result.unwrap_err(), CaptureError::NotCapturing);
1807    }
1808
1809    #[test]
1810    fn test_system_pager_default() {
1811        let pager = SystemPager::new();
1812        // SystemPager should be constructable and show() should not panic
1813        // when called with empty content (even if pager command doesn't exist)
1814        let _ = pager.show("");
1815    }
1816
1817    #[test]
1818    fn test_pager_enabled() {
1819        let pager = Pager::new();
1820        assert!(pager.is_enabled());
1821        let disabled = pager.enabled(false);
1822        assert!(!disabled.is_enabled());
1823    }
1824
1825    #[test]
1826    fn test_render_hook() {
1827        let hook = RenderHook::new(|lines| {
1828            // Add a bold "HOOKED" segment to every line
1829            let hooked: Vec<Vec<Segment>> = lines
1830                .iter()
1831                .map(|line| {
1832                    let mut new_line = line.clone();
1833                    new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
1834                    new_line
1835                })
1836                .collect();
1837            hooked
1838        });
1839        let lines = vec![vec![Segment::new("test")]];
1840        let result = hook.apply(&lines);
1841        assert_eq!(result.len(), 1);
1842        assert_eq!(result[0].len(), 2);
1843        assert_eq!(result[0][1].text, "HOOKED");
1844    }
1845
1846    #[test]
1847    fn test_console_size() {
1848        let mut console = Console::new();
1849        console.set_size(100, 40);
1850        let dims = console.size();
1851        assert_eq!(dims.width, 100);
1852        assert_eq!(dims.height, 40);
1853    }
1854
1855    #[test]
1856    fn test_console_is_dumb_terminal() {
1857        let console = Console::new();
1858        // In test environment, TERM is typically not "dumb"
1859        // Just verify it doesn't panic and returns a bool
1860        let _ = console.is_dumb_terminal();
1861    }
1862
1863    #[test]
1864    fn test_console_is_alt_screen() {
1865        let mut console = Console::new();
1866        assert!(!console.is_alt_screen());
1867        console.alt_screen = true;
1868        assert!(console.is_alt_screen());
1869    }
1870
1871    #[test]
1872    fn test_console_render_ansi() {
1873        let console = Console::new();
1874        let ansi = console.render_ansi("test");
1875        // Should return plain text if no style applied
1876        assert!(ansi.contains("test") || ansi.contains("\x1b["));
1877    }
1878
1879    #[test]
1880    fn test_console_render_to_lines() {
1881        let console = Console::new();
1882        let opts = ConsoleOptions::default();
1883        let lines = console.render_to_lines(&"hello", &opts);
1884        assert_eq!(lines.len(), 1);
1885        assert_eq!(lines[0][0].text, "hello");
1886    }
1887
1888    #[test]
1889    fn test_console_input_renderable() {
1890        // input_renderable reads from stdin, which is hard to test
1891        // Verify the method signature compiles
1892    }
1893
1894    #[test]
1895    fn test_console_print_exception_noop() {
1896        let mut console = Console::new();
1897        // Should not panic
1898        console.print_exception(None, 3);
1899    }
1900
1901    #[test]
1902    fn test_console_render_hooks_push_pop() {
1903        let mut console = Console::new();
1904        let hook = RenderHook::new(|lines| lines.to_vec());
1905        console.push_render_hook(hook);
1906        assert_eq!(console.render_hooks.len(), 1);
1907        let popped = console.pop_render_hook();
1908        assert!(popped.is_some());
1909        assert!(console.render_hooks.is_empty());
1910    }
1911
1912    #[test]
1913    fn test_console_reconfigure() {
1914        // Test that reconfigure doesn't panic
1915        reconfigure(Some(120), Some(40), None);
1916        reconfigure(None, None, Some(ColorSystem::Standard));
1917        // Reset
1918        reconfigure(None, None, None);
1919    }
1920
1921    #[test]
1922    fn test_pager_context_write() {
1923        let pager = Pager::new().enabled(false);
1924        let mut ctx = PagerContext::new(pager);
1925        ctx.feed("test content");
1926        // Drop should not panic since pager is disabled
1927    }
1928
1929    #[test]
1930    fn test_theme_context() {
1931        let mut console = Console::new();
1932        let custom_theme = Theme::new();
1933        let original = console.theme.clone();
1934        {
1935            let _ctx = console.use_theme(custom_theme);
1936            // Theme should be the custom one now
1937        }
1938        // After ctx drops, original theme should be restored
1939        assert_eq!(console.theme.styles.len(), original.styles.len());
1940    }
1941}