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