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.
516pub struct ThemeContext {
517    console_ptr: *mut Console,
518    previous_theme: Theme,
519}
520
521// SAFETY: ThemeContext is not Send or Sync because of the raw pointer.
522// It must only be used on the same thread as the Console.
523// The pointer is valid because Console creates ThemeContext and outlives it.
524
525impl ThemeContext {
526    /// Create a new ThemeContext (internal — use [`Console::use_theme`]).
527    pub(crate) fn new(console: &mut Console, previous_theme: Theme) -> Self {
528        Self {
529            console_ptr: console as *mut Console,
530            previous_theme,
531        }
532    }
533}
534
535impl Drop for ThemeContext {
536    fn drop(&mut self) {
537        unsafe {
538            (*self.console_ptr).theme = std::mem::take(&mut self.previous_theme);
539        }
540    }
541}
542
543// ---------------------------------------------------------------------------
544// Console
545// ---------------------------------------------------------------------------
546
547/// The main console for rendering rich output.
548pub struct Console {
549    /// The output writer.
550    pub file: Box<dyn Write + Send>,
551    /// Detected color system.
552    pub color_system: ColorSystem,
553    /// Current theme.
554    pub theme: Theme,
555    /// Default options.
556    pub options: ConsoleOptions,
557    /// Current width (may be overridden).
558    width: Option<usize>,
559    /// Current height (may be overridden).
560    height: Option<usize>,
561    /// Is this output a terminal?
562    is_terminal: bool,
563    /// If true, suppress all output.
564    pub quiet: bool,
565    /// If true, text wraps at word boundaries.
566    pub soft_wrap: bool,
567    /// Is the alternate screen active?
568    alt_screen: bool,
569    /// Is the cursor visible?
570    cursor_visible: bool,
571    /// Active render hooks that modify output before display.
572    render_hooks: Vec<RenderHook>,
573    /// Captured output buffer (active when capturing).
574    capture_buf: Option<Arc<Mutex<Vec<u8>>>>,
575    /// Original file writer saved during capture.
576    saved_file: Option<Box<dyn Write + Send>>,
577}
578
579impl Console {
580    /// Create a new Console writing to stdout.
581    pub fn new() -> Self {
582        let is_terminal = atty::is(atty::Stream::Stdout);
583        let color_system = detect_color_system();
584
585        let size = ConsoleDimensions::detect();
586
587        Self {
588            file: Box::new(io::stdout()) as Box<dyn Write + Send>,
589            color_system,
590            theme: crate::theme::default_theme(),
591            options: ConsoleOptions {
592                size,
593                is_terminal,
594                max_width: size.width,
595                max_height: size.height,
596                ..Default::default()
597            },
598            width: None,
599            height: None,
600            is_terminal,
601            quiet: false,
602            soft_wrap: false,
603            alt_screen: false,
604            cursor_visible: true,
605            render_hooks: Vec::new(),
606            capture_buf: None,
607            saved_file: None,
608        }
609    }
610
611    /// Create a Console that writes to a file.
612    pub fn with_file(file: Box<dyn Write + Send>) -> Self {
613        let _is_terminal = false;
614        Self {
615            file,
616            color_system: ColorSystem::Standard,
617            theme: crate::theme::default_theme(),
618            options: ConsoleOptions {
619                size: ConsoleDimensions { width: 80, height: 25 },
620                is_terminal: false,
621                max_width: 80,
622                max_height: 25,
623                ..Default::default()
624            },
625            width: None,
626            height: None,
627            is_terminal: false,
628            quiet: false,
629            soft_wrap: false,
630            alt_screen: false,
631            cursor_visible: true,
632            render_hooks: Vec::new(),
633            capture_buf: None,
634            saved_file: None,
635        }
636    }
637
638    /// Set the console width (overrides auto-detected terminal width).
639    pub fn set_width(&mut self, width: usize) {
640        self.width = Some(width);
641        self.options.max_width = width;
642    }
643
644    /// Set the console height.
645    pub fn set_height(&mut self, height: usize) {
646        self.height = Some(height);
647        self.options.max_height = height;
648    }
649
650    /// Get the effective width.
651    pub fn width(&self) -> usize {
652        self.width.unwrap_or(self.options.size.width)
653    }
654
655    /// Get the effective height.
656    pub fn height(&self) -> usize {
657        self.height.unwrap_or(self.options.size.height)
658    }
659
660    /// Render a renderable and return the segment lines.
661    pub fn render_lines(
662        &self,
663        renderable: &dyn Renderable,
664        options: &ConsoleOptions,
665        style: Option<&Style>,
666        _pad: bool,
667    ) -> Vec<Vec<Segment>> {
668        let result = renderable.render(options);
669
670        if let Some(st) = style {
671            result
672                .lines
673                .into_iter()
674                .map(|line| {
675                    line.into_iter()
676                        .map(|seg| {
677                            let new_style = if let Some(ref s) = seg.style {
678                                s.combine(st)
679                            } else {
680                                st.clone()
681                            };
682                            Segment::styled(seg.text, new_style)
683                        })
684                        .collect()
685                })
686                .collect()
687        } else {
688            result.lines
689        }
690    }
691
692    /// Look up a style by name from the theme.
693    pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
694        self.theme
695            .get(name)
696            .cloned()
697            .or_else(|| {
698                if !default.is_empty() {
699                    Some(Style::from_str(default))
700                } else {
701                    None
702                }
703            })
704    }
705
706    /// Render a string (with optional style).
707    pub fn render_str(&self, text: &str, style: &str) -> Text {
708        let st = self.get_style(style, "");
709        let mut t = Text::new(text);
710        if let Some(s) = st {
711            t = t.style(s);
712        }
713        t
714    }
715
716    // -----------------------------------------------------------------------
717    // print / log methods
718    // -----------------------------------------------------------------------
719
720    /// Print one or more renderable objects, separated by `sep`, ending with
721    /// `end`.
722    pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
723        if self.quiet { return; }
724        let mut first = true;
725        for obj in objects {
726            if !first {
727                let _ = write!(self.file, "{sep}");
728            }
729            first = false;
730            let result = obj.render(&self.options);
731            let ansi = result.to_ansi();
732            let _ = write!(self.file, "{ansi}");
733        }
734        let _ = write!(self.file, "{end}");
735        let _ = self.file.flush();
736    }
737
738    /// Print a single renderable followed by a newline.
739    pub fn println(&mut self, renderable: &dyn Renderable) {
740        if self.quiet { return; }
741        let result = renderable.render(&self.options);
742        let ansi = result.to_ansi();
743        let _ = writeln!(self.file, "{ansi}");
744        let _ = self.file.flush();
745    }
746
747    /// Print a plain string (supports markup by default when `markup` is
748    /// enabled).
749    pub fn print_str(&mut self, text: &str) {
750        if self.quiet { return; }
751        let ansi = if self.options.markup {
752            let parsed = crate::markup::render(text);
753            parsed.render()
754        } else {
755            text.to_string()
756        };
757        let _ = write!(self.file, "{ansi}");
758        let _ = self.file.flush();
759    }
760
761    /// Print formatted JSON.
762    pub fn print_json(&mut self, data: &serde_json::Value) {
763        if self.quiet { return; }
764        let formatted = crate::json::render_json(data);
765        let result = formatted.render(&self.options);
766        let ansi = result.to_ansi();
767        let _ = writeln!(self.file, "{ansi}");
768        let _ = self.file.flush();
769    }
770
771    /// Clear the screen.
772    pub fn clear(&mut self) {
773        if self.quiet { return; }
774        let _ = write!(self.file, "\x1b[2J\x1b[H");
775        let _ = self.file.flush();
776    }
777
778    /// Show the cursor.
779    pub fn show_cursor(&mut self) {
780        self.cursor_visible = true;
781        let _ = write!(self.file, "\x1b[?25h");
782        let _ = self.file.flush();
783    }
784
785    /// Hide the cursor.
786    pub fn hide_cursor(&mut self) {
787        self.cursor_visible = false;
788        let _ = write!(self.file, "\x1b[?25l");
789        let _ = self.file.flush();
790    }
791
792    /// Set the terminal window title.
793    pub fn set_window_title(&mut self, title: &str) {
794        let _ = write!(self.file, "\x1b]0;{title}\x07");
795        let _ = self.file.flush();
796    }
797
798    /// Get the ANSI escape string for a given color as this console supports.
799    pub fn color_ansi(&self, color: &Color) -> String {
800        let downgraded = color.downgrade(self.color_system);
801        downgraded.to_string()
802    }
803
804    // -- Recursive rendering ------------------------------------------------
805
806    /// Render a renderable by recursively flattening nested items into
807    /// segments.  This is equivalent to Python Rich's `Console.render()`.
808    /// It handles `Group` composition and any renderable that yields other
809    /// renderables.
810    pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
811        let result = renderable.render(options);
812        result.flatten(options)
813    }
814
815    /// Measure a renderable's width constraints.
816    /// Equivalent to Python Rich's `Measurement.get(console, options, renderable)`.
817    pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
818        if let Some(m) = renderable.measure(options) {
819            return m;
820        }
821        let segments = self.render(renderable, options);
822        let max_w = segments.iter()
823            .map(|s| s.cell_length())
824            .max()
825            .unwrap_or(0);
826        crate::measure::Measurement::new(max_w, options.max_width)
827    }
828
829    // -- Convenience render methods -----------------------------------------
830
831    /// Render a rule with the given title.
832    /// Equivalent to `Console.rule()`.
833    pub fn rule(
834        &mut self,
835        title: impl Into<String>,
836        characters: Option<&str>,
837        style: Option<Style>,
838        align: Option<AlignMethod>,
839    ) {
840        if self.quiet { return; }
841        let mut rule = crate::rule::Rule::new().title(title);
842        if let Some(chars) = characters { rule = rule.characters(chars); }
843        if let Some(st) = style { rule = rule.style(st); }
844        if let Some(a) = align { rule = rule.align(a); }
845        let result = rule.render(&self.options);
846        let ansi = result.to_ansi();
847        let _ = write!(self.file, "{ansi}");
848        let _ = self.file.flush();
849    }
850
851    /// Output a bell character.
852    pub fn bell(&mut self) {
853        if self.quiet { return; }
854        let _ = write!(self.file, "\x07");
855        let _ = self.file.flush();
856    }
857
858    /// Output blank lines.
859    pub fn line(&mut self, count: usize) {
860        if self.quiet { return; }
861        for _ in 0..count {
862            let _ = writeln!(self.file);
863        }
864        let _ = self.file.flush();
865    }
866
867    /// Output a log entry with timestamp, caller info.
868    pub fn log(&mut self, objects: &[&dyn Renderable]) {
869        if self.quiet { return; }
870        let now = chrono::Local::now();
871        let time_str = format!("[{}]", now.format("%H:%M:%S"));
872        let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
873        let _ = write!(self.file, "{time_str} ");
874        let _ = write!(self.file, "{}", Style::new().reset_ansi());
875        self.print(objects, " ", "\n");
876    }
877
878    // -- Theme stack --------------------------------------------------------
879
880    /// Push a theme onto the stack.
881    pub fn push_theme(&mut self, theme: Theme) {
882        let mut new_theme = theme.clone();
883        new_theme.inherit = Some(Box::new(self.theme.clone()));
884        self.theme = new_theme;
885    }
886
887    /// Pop the current theme, restoring the previous one.
888    pub fn pop_theme(&mut self) {
889        if let Some(ref inherit) = self.theme.inherit {
890            self.theme = *inherit.clone();
891        }
892    }
893
894    // -- Export methods ------------------------------------------------------
895
896    /// Export the current console output as an HTML document.
897    ///
898    /// Renders the given renderable and wraps it in a styled HTML page.
899    pub fn export_html(&self, renderable: &dyn Renderable) -> String {
900        let result = renderable.render(&self.options);
901        let ansi = result.to_ansi();
902        crate::export::export_html(&crate::export::ExportHtmlOptions {
903            code: crate::export::strip_ansi_escapes(&ansi),
904            ..Default::default()
905        })
906    }
907
908    /// Save rendered output as an HTML file.
909    pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
910        let html = self.export_html(renderable);
911        crate::export::save_html(path, &crate::export::ExportHtmlOptions {
912            code: html,
913            ..Default::default()
914        })
915    }
916
917    /// Export the current console output as an SVG document.
918    pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
919        let result = renderable.render(&self.options);
920        let ansi = result.to_ansi();
921        crate::export::export_svg(&crate::export::ExportSvgOptions {
922            code: crate::export::strip_ansi_escapes(&ansi),
923            ..Default::default()
924        })
925    }
926
927    /// Save rendered output as an SVG file.
928    pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
929        let svg = self.export_svg(renderable);
930        crate::export::save_svg(path, &crate::export::ExportSvgOptions {
931            code: svg,
932            ..Default::default()
933        })
934    }
935
936    /// Export the current console output as plain text (strips ANSI).
937    pub fn export_text(&self, renderable: &dyn Renderable) -> String {
938        let result = renderable.render(&self.options);
939        let ansi = result.to_ansi();
940        crate::export::export_text(&crate::export::ExportTextOptions {
941            text: ansi,
942            strip_ansi: true,
943        })
944    }
945
946    /// Save rendered output as a plain text file.
947    pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
948        let text = self.export_text(renderable);
949        crate::export::save_text(path, &crate::export::ExportTextOptions {
950            text,
951            strip_ansi: false,
952        })
953    }
954
955    // -- Quiet / Soft-wrap setters ------------------------------------------
956
957    /// Set the quiet flag (suppress all output when true).
958    pub fn set_quiet(&mut self, quiet: bool) {
959        self.quiet = quiet;
960    }
961
962    /// Builder-style setter for quiet.
963    pub fn quiet(mut self, quiet: bool) -> Self {
964        self.quiet = quiet;
965        self
966    }
967
968    /// Set the soft-wrap flag (wrap text at word boundaries when true).
969    pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
970        self.soft_wrap = soft_wrap;
971    }
972
973    /// Builder-style setter for soft_wrap.
974    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
975        self.soft_wrap = soft_wrap;
976        self
977    }
978
979    // -- Input --------------------------------------------------------------
980
981    /// Read a line of input from the user.
982    ///
983    /// Writes `prompt` to the console, then reads a line from stdin.
984    /// When `password` is true, the input is masked with `*` characters
985    /// (using raw terminal mode via crossterm).
986    pub fn input(&mut self, prompt: &str, password: bool) -> String {
987        let _ = write!(self.file, "{prompt}");
988        let _ = self.file.flush();
989
990        if password {
991            self.read_password()
992        } else {
993            let mut input = String::new();
994            let _ = io::stdin().read_line(&mut input);
995            input.trim().to_string()
996        }
997    }
998
999    /// Read a password from stdin with character masking.
1000    fn read_password(&mut self) -> String {
1001        use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
1002        use std::io::Read;
1003
1004        match enable_raw_mode() {
1005            Ok(()) => {
1006                let stdin = io::stdin();
1007                let mut handle = stdin.lock();
1008                let mut buf = [0u8; 1];
1009                let mut password = String::new();
1010
1011                loop {
1012                    match handle.read_exact(&mut buf) {
1013                        Ok(()) => match buf[0] {
1014                            b'\r' | b'\n' => {
1015                                let _ = writeln!(self.file);
1016                                let _ = self.file.flush();
1017                                break;
1018                            }
1019                            b'\x03' => {
1020                                // Ctrl+C — break and return what we have
1021                                let _ = writeln!(self.file);
1022                                let _ = self.file.flush();
1023                                break;
1024                            }
1025                            b'\x7f' | b'\x08' => {
1026                                // Backspace
1027                                password.pop();
1028                            }
1029                            c => {
1030                                password.push(c as char);
1031                                let _ = write!(self.file, "*");
1032                                let _ = self.file.flush();
1033                            }
1034                        },
1035                        Err(_) => break,
1036                    }
1037                }
1038                let _ = disable_raw_mode();
1039                password
1040            }
1041            Err(_) => {
1042                // Fallback: read without masking
1043                let mut input = String::new();
1044                let _ = io::stdin().read_line(&mut input);
1045                input.trim().to_string()
1046            }
1047        }
1048    }
1049
1050    // -- Screen / alternate screen ------------------------------------------
1051
1052    /// Create a [`ScreenContext`](crate::screen::ScreenContext) that enters the
1053    /// alternate screen buffer. The context automatically exits the alternate
1054    /// screen when dropped.
1055    pub fn screen(&mut self) -> crate::screen::ScreenContext {
1056        let mut ctx = crate::screen::ScreenContext::new();
1057        ctx.enter();
1058        ctx
1059    }
1060
1061    /// Enter or exit the alternate screen buffer by writing the corresponding
1062    /// escape sequences (`\x1b[?1049h` / `\x1b[?1049l`).
1063    pub fn set_alt_screen(&mut self, enable: bool) {
1064        self.alt_screen = enable;
1065        if enable {
1066            let _ = write!(self.file, "\x1b[?1049h");
1067        } else {
1068            let _ = write!(self.file, "\x1b[?1049l");
1069        }
1070        let _ = self.file.flush();
1071    }
1072
1073    /// Get whether the output is a terminal.
1074    pub fn is_terminal(&self) -> bool {
1075        self.is_terminal
1076    }
1077
1078    /// Set the terminal size (overrides auto-detected dimensions).
1079    pub fn set_size(&mut self, width: usize, height: usize) {
1080        self.width = Some(width);
1081        self.height = Some(height);
1082        self.options.max_width = width;
1083        self.options.max_height = height;
1084        self.options.size = crate::console::ConsoleDimensions { width, height };
1085    }
1086
1087    /// Handle broken pipe errors gracefully.
1088    ///
1089    /// In Rust, `write()` returns `ErrorKind::BrokenPipe` instead of raising
1090    /// `SIGPIPE`, so broken pipes are not fatal. The Console already uses
1091    /// `let _ = write!(...)` throughout, which silently discards all write
1092    /// errors including EPIPE. This method is provided for API compatibility
1093    /// with Python Rich and as a documentation point.
1094    pub fn on_broken_pipe(&self) {
1095        // No-op: Rust handles EPIPE via ErrorKind, not signals.
1096        // All Console write operations use `let _ = write!()` which
1097        // already discards BrokenPipe errors without panicking.
1098    }
1099}
1100
1101impl Default for Console {
1102    fn default() -> Self {
1103        Self::new()
1104    }
1105}
1106
1107impl fmt::Debug for Console {
1108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1109        f.debug_struct("Console")
1110            .field("color_system", &self.color_system)
1111            .field("width", &self.width())
1112            .field("height", &self.height())
1113            .field("is_terminal", &self.is_terminal)
1114            .field("alt_screen", &self.alt_screen)
1115            .field("cursor_visible", &self.cursor_visible)
1116            .field("quiet", &self.quiet)
1117            .field("soft_wrap", &self.soft_wrap)
1118            .finish()
1119    }
1120}
1121
1122// ===========================================================================
1123// New feature methods (Capture, Pager, Terminal Control, Hooks, etc.)
1124// ===========================================================================
1125
1126impl Console {
1127    // -- Capture System ------------------------------------------------------
1128
1129    /// Start capturing all output. All subsequent writes to this console are
1130    /// redirected to an internal buffer. Call [`end_capture`](Self::end_capture)
1131    /// to stop capturing and retrieve the captured content.
1132    pub fn begin_capture(&mut self) {
1133        let buf = Arc::new(Mutex::new(Vec::new()));
1134        let writer = Box::new(CaptureWriter { buf: buf.clone() });
1135        self.saved_file = Some(std::mem::replace(&mut self.file, writer));
1136        self.capture_buf = Some(buf);
1137    }
1138
1139    /// End capture mode and return the [`Capture`] containing all output written
1140    /// while capturing was active. The console's output is restored to its
1141    /// original destination.
1142    pub fn end_capture(&mut self) -> Capture {
1143        let buf = self.capture_buf.take().expect("not currently capturing");
1144        if let Some(saved) = self.saved_file.take() {
1145            self.file = saved;
1146        }
1147        Capture { buf }
1148    }
1149
1150    /// Run the given closure with output captured, returning the captured text.
1151    ///
1152    /// This is the most ergonomic way to capture output in Rust:
1153    ///
1154    /// ```rust,no_run
1155    /// # use rusty_rich::Console;
1156    /// let mut console = Console::new();
1157    /// let output = console.capture(|c| {
1158    ///     c.print_str("Hello, world!");
1159    /// });
1160    /// assert_eq!(output, "Hello, world!");
1161    /// ```
1162    pub fn capture<F: FnOnce(&mut Self)>(&mut self, f: F) -> String {
1163        self.begin_capture();
1164        f(self);
1165        let cap = self.end_capture();
1166        cap.get()
1167    }
1168
1169    // -- Pager System --------------------------------------------------------
1170
1171    /// Get a [`PagerContext`]. Content rendered while the context is alive is
1172    /// collected and displayed through the system pager (`$PAGER` or `less`)
1173    /// when the context is dropped.
1174    ///
1175    /// `styles` controls whether ANSI styles are preserved when paging.
1176    pub fn pager(&mut self, styles: bool) -> PagerContext {
1177        PagerContext::new(Pager::new().color(styles))
1178    }
1179
1180    // -- Input with Renderable prompt ----------------------------------------
1181
1182    /// Display a [`Renderable`] prompt and read a line of input from stdin.
1183    ///
1184    /// The prompt is rendered through the console's current options and theme.
1185    pub fn input_renderable(&mut self, prompt: &dyn Renderable) -> String {
1186        if !self.quiet {
1187            let result = prompt.render(&self.options);
1188            let ansi = result.to_ansi();
1189            let _ = write!(self.file, "{ansi}");
1190            let _ = self.file.flush();
1191        }
1192        let mut input = String::new();
1193        let _ = io::stdin().read_line(&mut input);
1194        input.trim().to_string()
1195    }
1196
1197    // -- Exception / Traceback -----------------------------------------------
1198
1199    /// Print the current exception as a rich traceback.
1200    ///
1201    /// In Rust, this is a best-effort rendering; it captures the current
1202    /// thread's panic info if available. `width` overrides the output width,
1203    /// and `extra_lines` controls how many lines of source context to show
1204    /// around each frame.
1205    pub fn print_exception(&mut self, _width: Option<usize>, _extra_lines: usize) {
1206        if self.quiet { return; }
1207        // Note: Rust does not have Python's sys.exc_info(). A full traceback
1208        // renderer would need std::panic::catch_unwind or custom error capture.
1209        // This method provides the API surface; for actual panic tracebacks
1210        // see crate::traceback::install().
1211        let msg = format!(
1212            "[bold red]Exception[/bold red]: No current exception info. "
1213        );
1214        let msg_text = crate::text::Text::from_markup(&msg);
1215        let result = msg_text.render();
1216        let _ = writeln!(self.file, "{result}");
1217        let _ = self.file.flush();
1218    }
1219
1220    // -- JSON pretty-print (string overload) -----------------------------------
1221
1222    /// Pretty-print a JSON string. Parses the string and renders it with
1223    /// syntax highlighting.
1224    pub fn print_json_str(&mut self, json: &str) {
1225        if self.quiet { return; }
1226        if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
1227            self.print_json(&value);
1228        } else {
1229            let _ = writeln!(self.file, "[invalid JSON]");
1230            let _ = self.file.flush();
1231        }
1232    }
1233
1234    // -- Render lines (simple version) ----------------------------------------
1235
1236    /// Render a renderable to a vector of segment lines.
1237    ///
1238    /// This is the lower-level render entry point, returning raw lines instead
1239    /// of an ANSI string. Compare with [`render`](Self::render) which returns
1240    /// flat segments.
1241    pub fn render_to_lines(
1242        &self,
1243        renderable: &dyn Renderable,
1244        options: &ConsoleOptions,
1245    ) -> Vec<Vec<Segment>> {
1246        let result = renderable.render(options);
1247        let has_items = !result.items.is_empty();
1248        let mut lines = if result.lines.is_empty() && has_items {
1249            let flat = result.flatten(options);
1250            if flat.is_empty() {
1251                Vec::new() // also empty after flatten — keep empty
1252            } else {
1253                vec![flat]
1254            }
1255        } else {
1256            result.lines
1257        };
1258        // Apply any render hooks
1259        if !self.render_hooks.is_empty() {
1260            for hook in &self.render_hooks {
1261                lines = hook.apply(&lines);
1262            }
1263        }
1264        lines
1265    }
1266
1267    // -- Render ANSI string ---------------------------------------------------
1268
1269    /// Render a plain string to ANSI text, applying the current theme and
1270    /// styles. Returns the ANSI-formatted string.
1271    pub fn render_ansi(&self, text: &str) -> String {
1272        let t = self.render_str(text, "");
1273        t.render()
1274    }
1275
1276    // -- Export SVG with options ----------------------------------------------
1277
1278    /// Export the console output as an SVG document with explicit options.
1279    ///
1280    /// This delegates to [`crate::export::export_svg`] with the given
1281    /// [`ExportSvgOptions`](crate::export::ExportSvgOptions).
1282    pub fn export_svg_opts(&self, options: &crate::export::ExportSvgOptions) -> String {
1283        crate::export::export_svg(options)
1284    }
1285
1286    // -- Console Properties ---------------------------------------------------
1287
1288    /// Get the terminal size as [`ConsoleDimensions`].
1289    pub fn size(&self) -> ConsoleDimensions {
1290        ConsoleDimensions {
1291            width: self.width(),
1292            height: self.height(),
1293        }
1294    }
1295
1296    /// Check if the terminal is a "dumb" terminal (no color support).
1297    pub fn is_dumb_terminal(&self) -> bool {
1298        std::env::var("TERM").map_or(false, |t| t == "dumb")
1299    }
1300
1301    /// Check if the console is currently in the alternate screen buffer.
1302    pub fn is_alt_screen(&self) -> bool {
1303        self.alt_screen
1304    }
1305
1306    // -- Terminal Control ----------------------------------------------------
1307
1308    /// Show or hide the cursor based on the boolean parameter.
1309    ///
1310    /// `true` shows the cursor, `false` hides it. Tracks the current state
1311    /// so it can be queried via internal fields.
1312    pub fn set_cursor_visible(&mut self, visible: bool) {
1313        self.cursor_visible = visible;
1314        if visible {
1315            let _ = write!(self.file, "\x1b[?25h");
1316        } else {
1317            let _ = write!(self.file, "\x1b[?25l");
1318        }
1319        let _ = self.file.flush();
1320    }
1321
1322    /// Temporarily switch to a different theme. Returns a [`ThemeContext`]
1323    /// that restores the original theme when dropped.
1324    ///
1325    /// # Example
1326    ///
1327    /// ```rust,no_run
1328    /// # use rusty_rich::{Console, Theme};
1329    /// let mut console = Console::new();
1330    /// let custom = Theme::new();
1331    /// {
1332    ///     let _ctx = console.use_theme(custom);
1333    ///     // console uses custom theme here
1334    /// }
1335    /// // original theme restored here
1336    /// ```
1337    pub fn use_theme(&mut self, theme: Theme) -> ThemeContext {
1338        let prev = std::mem::replace(&mut self.theme, theme);
1339        ThemeContext::new(self, prev)
1340    }
1341
1342    /// Clear the live display region. When in alt-screen mode, this clears
1343    /// the entire alternate screen. Otherwise, it's equivalent to
1344    /// [`clear`](Self::clear).
1345    pub fn clear_live(&mut self) {
1346        if self.alt_screen {
1347            let _ = write!(self.file, "\x1b[2J\x1b[H");
1348        } else {
1349            let _ = write!(self.file, "\x1b[2J\x1b[H");
1350        }
1351        let _ = self.file.flush();
1352    }
1353
1354    /// Set the active live display. Stores a reference to the [`Live`]
1355    /// renderer for integration with the console's rendering pipeline.
1356    ///
1357    /// Note: [`Live`](crate::live::Live) manages its own refresh cycle;
1358    /// this method is primarily for API compatibility with Python Rich.
1359    pub fn set_live(&mut self, _live: &crate::live::Live) {
1360        // Live manages its own refresh cycle; this method provides the
1361        // API surface for attaching a live display to the console.
1362    }
1363
1364    /// Update the full screen (enter alt-screen, render content, exit).
1365    ///
1366    /// Clears the screen and renders the given renderable. If `options` is
1367    /// `None`, the console's current options are used.
1368    pub fn update_screen(&mut self, renderable: &dyn Renderable, options: Option<&ConsoleOptions>) {
1369        let opts = options.unwrap_or(&self.options);
1370        let segments = self.render(renderable, opts);
1371        let mut output = String::new();
1372        for seg in &segments {
1373            output.push_str(&seg.to_ansi());
1374        }
1375        let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1376        let _ = self.file.flush();
1377    }
1378
1379    /// Update the screen from pre-rendered lines of segments.
1380    ///
1381    /// Takes already-rendered lines and displays them as the full screen
1382    /// content, clearing existing content first.
1383    pub fn update_screen_lines(&mut self, lines: &[Vec<Segment>], options: Option<&ConsoleOptions>) {
1384        let _ = options;
1385        let mut output = String::new();
1386        for line in lines {
1387            for seg in line {
1388                output.push_str(&seg.to_ansi());
1389            }
1390            output.push('\n');
1391        }
1392        let _ = write!(self.file, "\x1b[2J\x1b[H{output}");
1393        let _ = self.file.flush();
1394    }
1395
1396    // -- Render Hooks --------------------------------------------------------
1397
1398    /// Add a [`RenderHook`] to the console. Hooks are applied in order and
1399    /// can modify the rendered lines before they are displayed.
1400    pub fn push_render_hook(&mut self, hook: RenderHook) {
1401        self.render_hooks.push(hook);
1402    }
1403
1404    /// Remove and return the most recently added [`RenderHook`], if any.
1405    pub fn pop_render_hook(&mut self) -> Option<RenderHook> {
1406        self.render_hooks.pop()
1407    }
1408}
1409
1410// ---------------------------------------------------------------------------
1411// Color system detection
1412// ---------------------------------------------------------------------------
1413
1414/// Detect the terminal color system from environment variables.
1415///
1416/// Checks `COLORTERM`, `TERM`, `NO_COLOR`, and `CLICOLOR` to determine
1417/// whether the terminal supports true color, 8-bit, or standard 16 colors.
1418fn detect_color_system() -> ColorSystem {
1419    // Check common env vars
1420    if let Ok(val) = std::env::var("COLORTERM") {
1421        if val == "truecolor" || val == "24bit" {
1422            return ColorSystem::TrueColor;
1423        }
1424    }
1425    if let Ok(term) = std::env::var("TERM") {
1426        if term.contains("256color") {
1427            return ColorSystem::EightBit;
1428        }
1429        if term == "xterm-kitty" {
1430            return ColorSystem::TrueColor;
1431        }
1432    }
1433    // Check NO_COLOR / CLICOLOR
1434    if std::env::var("NO_COLOR").is_ok() {
1435        return ColorSystem::Standard;
1436    }
1437    // Default to true color on modern terminals
1438    if atty::is(atty::Stream::Stdout) {
1439        ColorSystem::TrueColor
1440    } else {
1441        ColorSystem::Standard
1442    }
1443}
1444
1445// ---------------------------------------------------------------------------
1446// Global console instance (like Rich's `get_console()`)
1447// ---------------------------------------------------------------------------
1448
1449use once_cell::sync::Lazy;
1450
1451static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
1452    Mutex::new(Console::new())
1453});
1454
1455/// Get a reference to the global Console.
1456pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1457    GLOBAL_CONSOLE.lock().unwrap()
1458}
1459
1460// ---------------------------------------------------------------------------
1461// Convenience functions (like Rich's `print()`)
1462// ---------------------------------------------------------------------------
1463
1464/// Print objects using the global console.
1465pub fn print_objects(objects: &[&dyn Renderable]) {
1466    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1467    console.print(objects, " ", "\n");
1468}
1469
1470/// Print a string with markup support.
1471pub fn print_str(text: &str) {
1472    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1473    console.print_str(text);
1474}
1475
1476/// Print formatted JSON.
1477pub fn print_json_val(data: &serde_json::Value) {
1478    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1479    console.print_json(data);
1480}
1481
1482// ---------------------------------------------------------------------------
1483// Reconfigure global console
1484// ---------------------------------------------------------------------------
1485
1486/// Reconfigure the global Console singleton with new dimensions and/or
1487/// color system. This updates the shared global console instance used by
1488/// [`print_objects`], [`print_str`], and [`print_json_val`].
1489///
1490/// # Parameters
1491///
1492/// * `width` — New terminal width (None to keep current).
1493/// * `height` — New terminal height (None to keep current).
1494/// * `color_system` — New color system level (None to keep current).
1495pub fn reconfigure(
1496    width: Option<usize>,
1497    height: Option<usize>,
1498    color_system: Option<ColorSystem>,
1499) {
1500    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1501    if let Some(w) = width {
1502        console.set_width(w);
1503    }
1504    if let Some(h) = height {
1505        console.set_height(h);
1506    }
1507    if let Some(cs) = color_system {
1508        console.color_system = cs;
1509    }
1510}
1511
1512#[cfg(test)]
1513mod tests {
1514    use super::*;
1515
1516    #[test]
1517    fn test_render_result_from_text() {
1518        let r = RenderResult::from_text("hello");
1519        assert_eq!(r.lines.len(), 1);
1520        assert_eq!(r.lines[0][0].text, "hello");
1521    }
1522
1523    #[test]
1524    fn test_console_options_default() {
1525        let opts = ConsoleOptions::default();
1526        assert!(opts.markup);
1527    }
1528
1529    #[test]
1530    fn test_console_quiet_default() {
1531        let console = Console::new();
1532        assert!(!console.quiet);
1533    }
1534
1535    #[test]
1536    fn test_console_quiet_setter() {
1537        let mut console = Console::new();
1538        console.set_quiet(true);
1539        assert!(console.quiet);
1540    }
1541
1542    #[test]
1543    fn test_console_quiet_builder() {
1544        let console = Console::new().quiet(true);
1545        assert!(console.quiet);
1546    }
1547
1548    #[test]
1549    fn test_console_quiet_suppresses_print() {
1550        let mut console = Console::new();
1551        console.quiet = true;
1552        // Should not panic
1553        console.print(&[], " ", "\n");
1554        console.println(&"test");
1555        console.print_str("test");
1556    }
1557
1558    #[test]
1559    fn test_console_soft_wrap_default() {
1560        let console = Console::new();
1561        assert!(!console.soft_wrap);
1562    }
1563
1564    #[test]
1565    fn test_console_soft_wrap_setter() {
1566        let mut console = Console::new();
1567        console.set_soft_wrap(true);
1568        assert!(console.soft_wrap);
1569    }
1570
1571    #[test]
1572    fn test_console_soft_wrap_builder() {
1573        let console = Console::new().soft_wrap(true);
1574        assert!(console.soft_wrap);
1575    }
1576
1577    #[test]
1578    fn test_console_is_terminal() {
1579        let console = Console::new();
1580        // is_terminal depends on whether stdout is a terminal
1581        let detected = console.is_terminal();
1582        assert_eq!(detected, atty::is(atty::Stream::Stdout));
1583    }
1584
1585    #[test]
1586    fn test_console_set_size() {
1587        let mut console = Console::new();
1588        console.set_size(120, 30);
1589        assert_eq!(console.width(), 120);
1590        assert_eq!(console.height(), 30);
1591        assert_eq!(console.options.max_width, 120);
1592        assert_eq!(console.options.max_height, 30);
1593    }
1594
1595    #[test]
1596    fn test_console_set_alt_screen() {
1597        let mut console = Console::new();
1598        // Just ensure it doesn't panic
1599        console.set_alt_screen(true);
1600        console.set_alt_screen(false);
1601    }
1602
1603    #[test]
1604    fn test_console_on_broken_pipe() {
1605        let console = Console::new();
1606        console.on_broken_pipe(); // no-op
1607    }
1608
1609    #[test]
1610    fn test_console_input_normal() {
1611        // We can't easily test stdin in unit tests, but we can verify
1612        // the method signature compiles and matches.
1613        let _console = Console::new();
1614        // input() cannot be meaningfully tested without actual stdin.
1615    }
1616
1617    #[test]
1618    fn test_console_debug() {
1619        let console = Console::new();
1620        let debug = format!("{:?}", console);
1621        assert!(debug.contains("Console"));
1622    }
1623
1624    #[test]
1625    fn test_console_with_file_has_no_terminal() {
1626        let console = Console::with_file(Box::new(std::io::sink()));
1627        assert!(!console.is_terminal());
1628    }
1629
1630    // -- New feature tests ---------------------------------------------------
1631
1632    #[test]
1633    fn test_newline_renderable() {
1634        let nl = NewLine;
1635        let result = nl.render(&ConsoleOptions::default());
1636        let ansi = result.to_ansi();
1637        assert_eq!(ansi, "\n");
1638    }
1639
1640    #[test]
1641    fn test_nochange_renderable() {
1642        let nc = NoChange;
1643        let result = nc.render(&ConsoleOptions::default());
1644        assert!(result.lines.is_empty());
1645        assert!(result.items.is_empty());
1646    }
1647
1648    #[test]
1649    fn test_capture_begin_end() {
1650        let mut console = Console::with_file(Box::new(std::io::sink()));
1651        console.begin_capture();
1652        let _ = write!(console.file, "captured text");
1653        let cap = console.end_capture();
1654        assert_eq!(cap.get(), "captured text");
1655    }
1656
1657    #[test]
1658    fn test_capture_with_closure() {
1659        let mut console = Console::with_file(Box::new(std::io::sink()));
1660        let output = console.capture(|c| {
1661            let _ = write!(c.file, "hello from capture");
1662        });
1663        assert_eq!(output, "hello from capture");
1664    }
1665
1666    #[test]
1667    fn test_capture_new_empty() {
1668        let console = Console::new();
1669        let cap = Capture::new(&console);
1670        assert_eq!(cap.get(), "");
1671    }
1672
1673    #[test]
1674    fn test_system_pager_default() {
1675        let pager = SystemPager::new();
1676        // SystemPager should be constructable and show() should not panic
1677        // when called with empty content (even if pager command doesn't exist)
1678        let _ = pager.show("");
1679    }
1680
1681    #[test]
1682    fn test_pager_enabled() {
1683        let pager = Pager::new();
1684        assert!(pager.is_enabled());
1685        let disabled = pager.enabled(false);
1686        assert!(!disabled.is_enabled());
1687    }
1688
1689    #[test]
1690    fn test_render_hook() {
1691        let hook = RenderHook::new(|lines| {
1692            // Add a bold "HOOKED" segment to every line
1693            let hooked: Vec<Vec<Segment>> = lines.iter().map(|line| {
1694                let mut new_line = line.clone();
1695                new_line.push(Segment::styled("HOOKED", Style::new().bold(true)));
1696                new_line
1697            }).collect();
1698            hooked
1699        });
1700        let lines = vec![vec![Segment::new("test")]];
1701        let result = hook.apply(&lines);
1702        assert_eq!(result.len(), 1);
1703        assert_eq!(result[0].len(), 2);
1704        assert_eq!(result[0][1].text, "HOOKED");
1705    }
1706
1707    #[test]
1708    fn test_console_size() {
1709        let mut console = Console::new();
1710        console.set_size(100, 40);
1711        let dims = console.size();
1712        assert_eq!(dims.width, 100);
1713        assert_eq!(dims.height, 40);
1714    }
1715
1716    #[test]
1717    fn test_console_is_dumb_terminal() {
1718        let console = Console::new();
1719        // In test environment, TERM is typically not "dumb"
1720        // Just verify it doesn't panic and returns a bool
1721        let _ = console.is_dumb_terminal();
1722    }
1723
1724    #[test]
1725    fn test_console_is_alt_screen() {
1726        let mut console = Console::new();
1727        assert!(!console.is_alt_screen());
1728        console.alt_screen = true;
1729        assert!(console.is_alt_screen());
1730    }
1731
1732    #[test]
1733    fn test_console_render_ansi() {
1734        let console = Console::new();
1735        let ansi = console.render_ansi("test");
1736        // Should return plain text if no style applied
1737        assert!(ansi.contains("test") || ansi.contains("\x1b["));
1738    }
1739
1740    #[test]
1741    fn test_console_render_to_lines() {
1742        let console = Console::new();
1743        let opts = ConsoleOptions::default();
1744        let lines = console.render_to_lines(&"hello", &opts);
1745        assert_eq!(lines.len(), 1);
1746        assert_eq!(lines[0][0].text, "hello");
1747    }
1748
1749    #[test]
1750    fn test_console_input_renderable() {
1751        // input_renderable reads from stdin, which is hard to test
1752        // Verify the method signature compiles
1753        let _console = Console::new();
1754    }
1755
1756    #[test]
1757    fn test_console_print_exception_noop() {
1758        let mut console = Console::new();
1759        // Should not panic
1760        console.print_exception(None, 3);
1761    }
1762
1763    #[test]
1764    fn test_console_render_hooks_push_pop() {
1765        let mut console = Console::new();
1766        let hook = RenderHook::new(|lines| lines.to_vec());
1767        console.push_render_hook(hook);
1768        assert_eq!(console.render_hooks.len(), 1);
1769        let popped = console.pop_render_hook();
1770        assert!(popped.is_some());
1771        assert!(console.render_hooks.is_empty());
1772    }
1773
1774    #[test]
1775    fn test_console_reconfigure() {
1776        // Test that reconfigure doesn't panic
1777        reconfigure(Some(120), Some(40), None);
1778        reconfigure(None, None, Some(ColorSystem::Standard));
1779        // Reset
1780        reconfigure(None, None, None);
1781    }
1782
1783    #[test]
1784    fn test_pager_context_write() {
1785        let pager = Pager::new().enabled(false);
1786        let mut ctx = PagerContext::new(pager);
1787        ctx.feed("test content");
1788        // Drop should not panic since pager is disabled
1789    }
1790
1791    #[test]
1792    fn test_theme_context() {
1793        let mut console = Console::new();
1794        let custom_theme = Theme::new();
1795        let original = console.theme.clone();
1796        {
1797            let _ctx = console.use_theme(custom_theme);
1798            // Theme should be the custom one now
1799        }
1800        // After ctx drops, original theme should be restored
1801        assert_eq!(console.theme.styles.len(), original.styles.len());
1802    }
1803}