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;
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    pub fn detect() -> Self {
31        if let Some((w, h)) = terminal_size::terminal_size() {
32            Self {
33                width: w.0 as usize,
34                height: h.0 as usize,
35            }
36        } else {
37            Self {
38                width: 80,
39                height: 25,
40            }
41        }
42    }
43}
44
45// ---------------------------------------------------------------------------
46// OverflowMethod
47// ---------------------------------------------------------------------------
48
49/// How to handle text that overflows the available width.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum OverflowMethod {
52    /// Wrap text onto the next line.
53    Fold,
54    /// Crop text at the boundary.
55    Crop,
56    /// Crop and append "…".
57    Ellipsis,
58    /// Let text overflow (don't clip).
59    Ignore,
60}
61
62// ---------------------------------------------------------------------------
63// ConsoleOptions
64// ---------------------------------------------------------------------------
65
66/// Options passed to renderables during rendering.
67#[derive(Debug, Clone)]
68pub struct ConsoleOptions {
69    /// Terminal size.
70    pub size: ConsoleDimensions,
71    /// True if output is a terminal.
72    pub is_terminal: bool,
73    /// The encoding (almost always UTF-8).
74    pub encoding: String,
75    /// Minimum render width.
76    pub min_width: usize,
77    /// Maximum render width.
78    pub max_width: usize,
79    /// Maximum height.
80    pub max_height: usize,
81    /// Override for text justification.
82    pub justify: Option<AlignMethod>,
83    /// Override for overflow handling.
84    pub overflow: Option<OverflowMethod>,
85    /// Disable text wrapping.
86    pub no_wrap: bool,
87    /// If true, use ASCII-only box characters.
88    pub ascii_only: bool,
89    /// If true, enable markup interpretation.
90    pub markup: bool,
91    /// If true, enable syntax highlighting of strings.
92    pub highlight: bool,
93    /// Optional fixed height for the renderable.
94    pub height: Option<usize>,
95    /// For legacy Windows console.
96    pub legacy_windows: bool,
97}
98
99impl Default for ConsoleOptions {
100    fn default() -> Self {
101        Self {
102            size: ConsoleDimensions::detect(),
103            is_terminal: true,
104            encoding: "utf-8".into(),
105            min_width: 1,
106            max_width: 80,
107            max_height: 25,
108            justify: None,
109            overflow: None,
110            no_wrap: false,
111            ascii_only: false,
112            markup: true,
113            highlight: true,
114            height: None,
115            legacy_windows: false,
116        }
117    }
118}
119
120impl ConsoleOptions {
121    /// Update the max width.
122    pub fn update_width(&self, max_width: usize) -> Self {
123        let mut opts = self.clone();
124        opts.max_width = max_width;
125        opts
126    }
127
128    /// Update the height.
129    pub fn update_height(&self, height: usize) -> Self {
130        let mut opts = self.clone();
131        opts.height = Some(height);
132        opts
133    }
134
135    /// Shrink the max width by an amount (for padding).
136    pub fn shrink_width(&self, amount: usize) -> Self {
137        let mut opts = self.clone();
138        opts.max_width = opts.max_width.saturating_sub(amount);
139        opts
140    }
141}
142
143// ---------------------------------------------------------------------------
144// Renderable trait
145// ---------------------------------------------------------------------------
146
147/// A single item in a render result — either a final `Segment` or a nested
148/// renderable that will be recursively flattened by `Console::render()`.
149///
150/// Equivalent to Python Rich's `RenderResult = Iterable[Union[Segment, RenderableType]]`.
151#[derive(Clone)]
152pub enum RenderItem {
153    Segment(Segment),
154    Nested(DynRenderable),
155}
156
157impl fmt::Debug for RenderItem {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::Segment(s) => write!(f, "Segment({})", &s.text),
161            Self::Nested(_) => write!(f, "Nested(...)"),
162        }
163    }
164}
165
166impl From<Segment> for RenderItem {
167    fn from(s: Segment) -> Self { Self::Segment(s) }
168}
169
170impl From<DynRenderable> for RenderItem {
171    fn from(r: DynRenderable) -> Self { Self::Nested(r) }
172}
173
174/// The result of rendering: a list of lines, each line being a list of
175/// segments.  Also carries an optional `items` list for recursive rendering.
176#[derive(Debug, Clone)]
177pub struct RenderResult {
178    /// Flat line-oriented segments (backward-compatible).
179    pub lines: Vec<Vec<Segment>>,
180    /// Optional render items for recursive flattening. When present,
181    /// `Console::render()` recurses into nested renderables.
182    pub items: Vec<RenderItem>,
183}
184
185impl RenderResult {
186    pub fn new() -> Self {
187        Self { lines: Vec::new(), items: Vec::new() }
188    }
189
190    pub fn from_text(text: &str) -> Self {
191        Self {
192            lines: vec![vec![Segment::new(text)]],
193            items: vec![RenderItem::Segment(Segment::new(text))],
194        }
195    }
196
197    pub fn from_segments(segments: Vec<Segment>) -> Self {
198        let items: Vec<RenderItem> = segments.iter().map(|s| RenderItem::Segment(s.clone())).collect();
199        Self { lines: vec![segments], items }
200    }
201
202    pub fn from_lines(lines: Vec<Vec<Segment>>) -> Self {
203        Self { lines, items: Vec::new() }
204    }
205
206    pub fn from_items(items: Vec<RenderItem>) -> Self {
207        Self { lines: Vec::new(), items }
208    }
209
210    /// Push a segment item.
211    pub fn push_item(&mut self, item: impl Into<RenderItem>) {
212        self.items.push(item.into());
213    }
214
215    /// Push a nested renderable for recursive flattening.
216    pub fn push_renderable(&mut self, r: impl Renderable + Send + Sync + 'static) {
217        self.items.push(RenderItem::Nested(DynRenderable::new(r)));
218    }
219
220    /// Recursively flatten items into segments using the given options.
221    /// This is called by `Console::render()` to resolve nested renderables.
222    pub fn flatten(&self, options: &ConsoleOptions) -> Vec<Segment> {
223        let mut out: Vec<Segment> = Vec::new();
224        flatten_items(&self.items, options, &mut out);
225        // Also flatten lines for backward compat
226        if out.is_empty() {
227            for line in &self.lines {
228                for seg in line {
229                    out.push(seg.clone());
230                }
231            }
232        }
233        out
234    }
235
236    /// Flatten all segments into a single ANSI string.
237    pub fn to_ansi(&self) -> String {
238        let mut out = String::new();
239        // Use items if present, otherwise fall back to lines
240        if !self.items.is_empty() {
241            let flat = self.flatten(&ConsoleOptions::default());
242            for seg in &flat {
243                out.push_str(&seg.to_ansi());
244            }
245        } else {
246            for line in &self.lines {
247                for seg in line {
248                    out.push_str(&seg.to_ansi());
249                }
250            }
251        }
252        out
253    }
254}
255
256/// Recursively flatten `RenderItem`s into a `Vec<Segment>`.
257fn flatten_items(items: &[RenderItem], options: &ConsoleOptions, out: &mut Vec<Segment>) {
258    for item in items {
259        match item {
260            RenderItem::Segment(seg) => out.push(seg.clone()),
261            RenderItem::Nested(renderable) => {
262                let nested = renderable.render(options);
263                flatten_items(&nested.items, options, out);
264            }
265        }
266    }
267}
268
269/// Trait for anything that can be rendered to the console.
270///
271/// Equivalent to `__rich_console__` in Python Rich.
272pub trait Renderable {
273    fn render(&self, options: &ConsoleOptions) -> RenderResult;
274
275    /// Optional width-measurement hook (equivalent to `__rich_measure__`).
276    /// Override to provide min/max width constraints for layout.
277    fn measure(&self, _options: &ConsoleOptions) -> Option<crate::measure::Measurement> {
278        None
279    }
280}
281
282// -- Implementations for common types ---------------------------------------
283
284impl Renderable for String {
285    fn render(&self, options: &ConsoleOptions) -> RenderResult {
286        self.as_str().render(options)
287    }
288}
289
290impl Renderable for &str {
291    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
292        RenderResult::from_text(self)
293    }
294}
295
296impl Renderable for Text {
297    fn render(&self, _options: &ConsoleOptions) -> RenderResult {
298        let rendered = self.render();
299        // Simple: just treat the rendered ANSI string as one segment per line
300        let lines: Vec<Vec<Segment>> = rendered
301            .lines()
302            .map(|l| vec![Segment::new(l)])
303            .collect();
304        RenderResult { lines, items: Vec::new() }
305    }
306}
307
308/// A wrapper that provides Clone + Debug for trait-object renderables.
309#[derive(Clone)]
310pub struct DynRenderable {
311    inner: Arc<dyn Renderable + Send + Sync>,
312}
313
314impl DynRenderable {
315    pub fn new(r: impl Renderable + Send + Sync + 'static) -> Self {
316        Self { inner: Arc::new(r) }
317    }
318}
319
320impl fmt::Debug for DynRenderable {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        f.debug_struct("DynRenderable").finish()
323    }
324}
325
326impl Renderable for DynRenderable {
327    fn render(&self, options: &ConsoleOptions) -> RenderResult {
328        self.inner.render(options)
329    }
330}
331
332/// A group of renderables rendered one after another.
333#[derive(Debug, Clone)]
334pub struct Group {
335    pub children: Vec<DynRenderable>,
336}
337
338impl Group {
339    pub fn new() -> Self {
340        Self { children: Vec::new() }
341    }
342
343    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
344        self.children.push(DynRenderable::new(renderable));
345    }
346}
347
348impl Renderable for Group {
349    fn render(&self, options: &ConsoleOptions) -> RenderResult {
350        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
351        for child in &self.children {
352            let result = child.render(options);
353            all_lines.extend(result.lines);
354        }
355        RenderResult { lines: all_lines, items: Vec::new() }
356    }
357}
358
359// ---------------------------------------------------------------------------
360// Console
361// ---------------------------------------------------------------------------
362
363/// The main console for rendering rich output.
364pub struct Console {
365    /// The output writer.
366    pub file: Box<dyn Write + Send>,
367    /// Detected color system.
368    pub color_system: ColorSystem,
369    /// Current theme.
370    pub theme: Theme,
371    /// Default options.
372    pub options: ConsoleOptions,
373    /// Current width (may be overridden).
374    width: Option<usize>,
375    /// Current height (may be overridden).
376    height: Option<usize>,
377    /// Is this output a terminal?
378    is_terminal: bool,
379    /// If true, suppress all output.
380    pub quiet: bool,
381    /// If true, text wraps at word boundaries.
382    pub soft_wrap: bool,
383}
384
385impl Console {
386    /// Create a new Console writing to stdout.
387    pub fn new() -> Self {
388        let is_terminal = atty::is(atty::Stream::Stdout);
389        let color_system = detect_color_system();
390
391        let size = ConsoleDimensions::detect();
392
393        Self {
394            file: Box::new(io::stdout()) as Box<dyn Write + Send>,
395            color_system,
396            theme: crate::theme::default_theme(),
397            options: ConsoleOptions {
398                size,
399                is_terminal,
400                max_width: size.width,
401                max_height: size.height,
402                ..Default::default()
403            },
404            width: None,
405            height: None,
406            is_terminal,
407            quiet: false,
408            soft_wrap: false,
409        }
410    }
411
412    /// Create a Console that writes to a file.
413    pub fn with_file(file: Box<dyn Write + Send>) -> Self {
414        let _is_terminal = false;
415        Self {
416            file,
417            color_system: ColorSystem::Standard,
418            theme: crate::theme::default_theme(),
419            options: ConsoleOptions {
420                size: ConsoleDimensions { width: 80, height: 25 },
421                is_terminal: false,
422                max_width: 80,
423                max_height: 25,
424                ..Default::default()
425            },
426            width: None,
427            height: None,
428            is_terminal: false,
429            quiet: false,
430            soft_wrap: false,
431        }
432    }
433
434    /// Set whether to suppress all output.
435    pub fn set_width(&mut self, width: usize) {
436        self.width = Some(width);
437        self.options.max_width = width;
438    }
439
440    /// Set the console height.
441    pub fn set_height(&mut self, height: usize) {
442        self.height = Some(height);
443        self.options.max_height = height;
444    }
445
446    /// Get the effective width.
447    pub fn width(&self) -> usize {
448        self.width.unwrap_or(self.options.size.width)
449    }
450
451    /// Get the effective height.
452    pub fn height(&self) -> usize {
453        self.height.unwrap_or(self.options.size.height)
454    }
455
456    /// Render a renderable and return the segment lines.
457    pub fn render_lines(
458        &self,
459        renderable: &dyn Renderable,
460        options: &ConsoleOptions,
461        style: Option<&Style>,
462        _pad: bool,
463    ) -> Vec<Vec<Segment>> {
464        let result = renderable.render(options);
465
466        if let Some(st) = style {
467            result
468                .lines
469                .into_iter()
470                .map(|line| {
471                    line.into_iter()
472                        .map(|seg| {
473                            let new_style = if let Some(ref s) = seg.style {
474                                s.combine(st)
475                            } else {
476                                st.clone()
477                            };
478                            Segment::styled(seg.text, new_style)
479                        })
480                        .collect()
481                })
482                .collect()
483        } else {
484            result.lines
485        }
486    }
487
488    /// Look up a style by name from the theme.
489    pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
490        self.theme
491            .get(name)
492            .cloned()
493            .or_else(|| {
494                if !default.is_empty() {
495                    Some(Style::from_str(default))
496                } else {
497                    None
498                }
499            })
500    }
501
502    /// Render a string (with optional style).
503    pub fn render_str(&self, text: &str, style: &str) -> Text {
504        let st = self.get_style(style, "");
505        let mut t = Text::new(text);
506        if let Some(s) = st {
507            t = t.style(s);
508        }
509        t
510    }
511
512    // -----------------------------------------------------------------------
513    // print / log methods
514    // -----------------------------------------------------------------------
515
516    /// Print one or more renderable objects, separated by `sep`, ending with
517    /// `end`.
518    pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
519        if self.quiet { return; }
520        let mut first = true;
521        for obj in objects {
522            if !first {
523                let _ = write!(self.file, "{sep}");
524            }
525            first = false;
526            let result = obj.render(&self.options);
527            let ansi = result.to_ansi();
528            let _ = write!(self.file, "{ansi}");
529        }
530        let _ = write!(self.file, "{end}");
531        let _ = self.file.flush();
532    }
533
534    /// Print a single renderable followed by a newline.
535    pub fn println(&mut self, renderable: &dyn Renderable) {
536        if self.quiet { return; }
537        let result = renderable.render(&self.options);
538        let ansi = result.to_ansi();
539        let _ = writeln!(self.file, "{ansi}");
540        let _ = self.file.flush();
541    }
542
543    /// Print a plain string (supports markup by default when `markup` is
544    /// enabled).
545    pub fn print_str(&mut self, text: &str) {
546        if self.quiet { return; }
547        let ansi = if self.options.markup {
548            let parsed = crate::markup::render(text);
549            parsed.render()
550        } else {
551            text.to_string()
552        };
553        let _ = write!(self.file, "{ansi}");
554        let _ = self.file.flush();
555    }
556
557    /// Print formatted JSON.
558    pub fn print_json(&mut self, data: &serde_json::Value) {
559        if self.quiet { return; }
560        let formatted = crate::json::render_json(data);
561        let result = formatted.render(&self.options);
562        let ansi = result.to_ansi();
563        let _ = writeln!(self.file, "{ansi}");
564        let _ = self.file.flush();
565    }
566
567    /// Clear the screen.
568    pub fn clear(&mut self) {
569        if self.quiet { return; }
570        let _ = write!(self.file, "\x1b[2J\x1b[H");
571        let _ = self.file.flush();
572    }
573
574    /// Show the cursor.
575    pub fn show_cursor(&mut self) {
576        let _ = write!(self.file, "\x1b[?25h");
577        let _ = self.file.flush();
578    }
579
580    /// Hide the cursor.
581    pub fn hide_cursor(&mut self) {
582        let _ = write!(self.file, "\x1b[?25l");
583        let _ = self.file.flush();
584    }
585
586    /// Set the terminal window title.
587    pub fn set_window_title(&mut self, title: &str) {
588        let _ = write!(self.file, "\x1b]0;{title}\x07");
589        let _ = self.file.flush();
590    }
591
592    /// Get the ANSI escape string for a given color as this console supports.
593    pub fn color_ansi(&self, color: &Color) -> String {
594        let downgraded = color.downgrade(self.color_system);
595        downgraded.to_string()
596    }
597
598    // -- Recursive rendering ------------------------------------------------
599
600    /// Render a renderable by recursively flattening nested items into
601    /// segments.  This is equivalent to Python Rich's `Console.render()`.
602    /// It handles `Group` composition and any renderable that yields other
603    /// renderables.
604    pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
605        let result = renderable.render(options);
606        result.flatten(options)
607    }
608
609    /// Measure a renderable's width constraints.
610    /// Equivalent to Python Rich's `Measurement.get(console, options, renderable)`.
611    pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
612        if let Some(m) = renderable.measure(options) {
613            return m;
614        }
615        let segments = self.render(renderable, options);
616        let max_w = segments.iter()
617            .map(|s| s.cell_length())
618            .max()
619            .unwrap_or(0);
620        crate::measure::Measurement::new(max_w, options.max_width)
621    }
622
623    // -- Convenience render methods -----------------------------------------
624
625    /// Render a rule with the given title.
626    /// Equivalent to `Console.rule()`.
627    pub fn rule(
628        &mut self,
629        title: impl Into<String>,
630        characters: Option<&str>,
631        style: Option<Style>,
632        align: Option<AlignMethod>,
633    ) {
634        if self.quiet { return; }
635        let mut rule = crate::rule::Rule::new().title(title);
636        if let Some(chars) = characters { rule = rule.characters(chars); }
637        if let Some(st) = style { rule = rule.style(st); }
638        if let Some(a) = align { rule = rule.align(a); }
639        let result = rule.render(&self.options);
640        let ansi = result.to_ansi();
641        let _ = write!(self.file, "{ansi}");
642        let _ = self.file.flush();
643    }
644
645    /// Output a bell character.
646    pub fn bell(&mut self) {
647        if self.quiet { return; }
648        let _ = write!(self.file, "\x07");
649        let _ = self.file.flush();
650    }
651
652    /// Output blank lines.
653    pub fn line(&mut self, count: usize) {
654        if self.quiet { return; }
655        for _ in 0..count {
656            let _ = writeln!(self.file);
657        }
658        let _ = self.file.flush();
659    }
660
661    /// Output a log entry with timestamp, caller info.
662    pub fn log(&mut self, objects: &[&dyn Renderable]) {
663        if self.quiet { return; }
664        let now = chrono::Local::now();
665        let time_str = format!("[{}]", now.format("%H:%M:%S"));
666        let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
667        let _ = write!(self.file, "{time_str} ");
668        let _ = write!(self.file, "{}", Style::new().reset_ansi());
669        self.print(objects, " ", "\n");
670    }
671
672    // -- Theme stack --------------------------------------------------------
673
674    /// Push a theme onto the stack.
675    pub fn push_theme(&mut self, theme: Theme) {
676        let mut new_theme = theme.clone();
677        new_theme.inherit = Some(Box::new(self.theme.clone()));
678        self.theme = new_theme;
679    }
680
681    /// Pop the current theme, restoring the previous one.
682    pub fn pop_theme(&mut self) {
683        if let Some(ref inherit) = self.theme.inherit {
684            self.theme = *inherit.clone();
685        }
686    }
687
688    // -- Export methods ------------------------------------------------------
689
690    /// Export the current console output as an HTML document.
691    ///
692    /// Renders the given renderable and wraps it in a styled HTML page.
693    pub fn export_html(&self, renderable: &dyn Renderable) -> String {
694        let result = renderable.render(&self.options);
695        let ansi = result.to_ansi();
696        crate::export::export_html(&crate::export::ExportHtmlOptions {
697            code: crate::export::strip_ansi_escapes(&ansi),
698            ..Default::default()
699        })
700    }
701
702    /// Save rendered output as an HTML file.
703    pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
704        let html = self.export_html(renderable);
705        crate::export::save_html(path, &crate::export::ExportHtmlOptions {
706            code: html,
707            ..Default::default()
708        })
709    }
710
711    /// Export the current console output as an SVG document.
712    pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
713        let result = renderable.render(&self.options);
714        let ansi = result.to_ansi();
715        crate::export::export_svg(&crate::export::ExportSvgOptions {
716            code: crate::export::strip_ansi_escapes(&ansi),
717            ..Default::default()
718        })
719    }
720
721    /// Save rendered output as an SVG file.
722    pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
723        let svg = self.export_svg(renderable);
724        crate::export::save_svg(path, &crate::export::ExportSvgOptions {
725            code: svg,
726            ..Default::default()
727        })
728    }
729
730    /// Export the current console output as plain text (strips ANSI).
731    pub fn export_text(&self, renderable: &dyn Renderable) -> String {
732        let result = renderable.render(&self.options);
733        let ansi = result.to_ansi();
734        crate::export::export_text(&crate::export::ExportTextOptions {
735            text: ansi,
736            strip_ansi: true,
737        })
738    }
739
740    /// Save rendered output as a plain text file.
741    pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
742        let text = self.export_text(renderable);
743        crate::export::save_text(path, &crate::export::ExportTextOptions {
744            text,
745            strip_ansi: false,
746        })
747    }
748
749    // -- Context manager equivalent -----------------------------------------
750
751    /// Enter a capture context. Returns the Console back so it can be
752    /// used inside a block. Call `end_capture()` to get the captured text.
753    pub fn begin_capture(&mut self) {
754        // In Rust, capture would need to swap the file with a buffer.
755        // For now, this is a no-op placeholder.
756    }
757
758    /// End capture and return captured text.
759    pub fn end_capture(&mut self) -> String {
760        String::new() // placeholder
761    }
762
763    // -- Quiet / Soft-wrap setters ------------------------------------------
764
765    /// Set the quiet flag (suppress all output when true).
766    pub fn set_quiet(&mut self, quiet: bool) {
767        self.quiet = quiet;
768    }
769
770    /// Builder-style setter for quiet.
771    pub fn quiet(mut self, quiet: bool) -> Self {
772        self.quiet = quiet;
773        self
774    }
775
776    /// Set the soft-wrap flag (wrap text at word boundaries when true).
777    pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
778        self.soft_wrap = soft_wrap;
779    }
780
781    /// Builder-style setter for soft_wrap.
782    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
783        self.soft_wrap = soft_wrap;
784        self
785    }
786
787    // -- Input --------------------------------------------------------------
788
789    /// Read a line of input from the user.
790    ///
791    /// Writes `prompt` to the console, then reads a line from stdin.
792    /// When `password` is true, the input is masked with `*` characters
793    /// (using raw terminal mode via crossterm).
794    pub fn input(&mut self, prompt: &str, password: bool) -> String {
795        let _ = write!(self.file, "{prompt}");
796        let _ = self.file.flush();
797
798        if password {
799            self.read_password()
800        } else {
801            let mut input = String::new();
802            let _ = io::stdin().read_line(&mut input);
803            input.trim().to_string()
804        }
805    }
806
807    /// Read a password from stdin with character masking.
808    fn read_password(&mut self) -> String {
809        use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
810        use std::io::Read;
811
812        match enable_raw_mode() {
813            Ok(()) => {
814                let stdin = io::stdin();
815                let mut handle = stdin.lock();
816                let mut buf = [0u8; 1];
817                let mut password = String::new();
818
819                loop {
820                    match handle.read_exact(&mut buf) {
821                        Ok(()) => match buf[0] {
822                            b'\r' | b'\n' => {
823                                let _ = writeln!(self.file);
824                                let _ = self.file.flush();
825                                break;
826                            }
827                            b'\x03' => {
828                                // Ctrl+C — break and return what we have
829                                let _ = writeln!(self.file);
830                                let _ = self.file.flush();
831                                break;
832                            }
833                            b'\x7f' | b'\x08' => {
834                                // Backspace
835                                password.pop();
836                            }
837                            c => {
838                                password.push(c as char);
839                                let _ = write!(self.file, "*");
840                                let _ = self.file.flush();
841                            }
842                        },
843                        Err(_) => break,
844                    }
845                }
846                let _ = disable_raw_mode();
847                password
848            }
849            Err(_) => {
850                // Fallback: read without masking
851                let mut input = String::new();
852                let _ = io::stdin().read_line(&mut input);
853                input.trim().to_string()
854            }
855        }
856    }
857
858    // -- Screen / alternate screen ------------------------------------------
859
860    /// Create a [`ScreenContext`](crate::screen::ScreenContext) that enters the
861    /// alternate screen buffer. The context automatically exits the alternate
862    /// screen when dropped.
863    pub fn screen(&mut self) -> crate::screen::ScreenContext {
864        let mut ctx = crate::screen::ScreenContext::new();
865        ctx.enter();
866        ctx
867    }
868
869    /// Enter or exit the alternate screen buffer by writing the corresponding
870    /// escape sequences (`\x1b[?1049h` / `\x1b[?1049l`).
871    pub fn set_alt_screen(&mut self, enable: bool) {
872        if enable {
873            let _ = write!(self.file, "\x1b[?1049h");
874        } else {
875            let _ = write!(self.file, "\x1b[?1049l");
876        }
877        let _ = self.file.flush();
878    }
879
880    /// Get whether the output is a terminal.
881    pub fn is_terminal(&self) -> bool {
882        self.is_terminal
883    }
884
885    /// Set the terminal size (overrides auto-detected dimensions).
886    pub fn set_size(&mut self, width: usize, height: usize) {
887        self.width = Some(width);
888        self.height = Some(height);
889        self.options.max_width = width;
890        self.options.max_height = height;
891        self.options.size = crate::console::ConsoleDimensions { width, height };
892    }
893
894    /// Handle broken pipe errors gracefully.
895    ///
896    /// In Rust, `write()` returns `ErrorKind::BrokenPipe` instead of raising
897    /// `SIGPIPE`, so broken pipes are not fatal. The Console already uses
898    /// `let _ = write!(...)` throughout, which silently discards all write
899    /// errors including EPIPE. This method is provided for API compatibility
900    /// with Python Rich and as a documentation point.
901    pub fn on_broken_pipe(&self) {
902        // No-op: Rust handles EPIPE via ErrorKind, not signals.
903        // All Console write operations use `let _ = write!()` which
904        // already discards BrokenPipe errors without panicking.
905    }
906}
907
908impl Default for Console {
909    fn default() -> Self {
910        Self::new()
911    }
912}
913
914impl fmt::Debug for Console {
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        f.debug_struct("Console")
917            .field("color_system", &self.color_system)
918            .field("width", &self.width())
919            .field("height", &self.height())
920            .field("is_terminal", &self.is_terminal)
921            .finish()
922    }
923}
924
925// ---------------------------------------------------------------------------
926// Color system detection
927// ---------------------------------------------------------------------------
928
929fn detect_color_system() -> ColorSystem {
930    // Check common env vars
931    if let Ok(val) = std::env::var("COLORTERM") {
932        if val == "truecolor" || val == "24bit" {
933            return ColorSystem::TrueColor;
934        }
935    }
936    if let Ok(term) = std::env::var("TERM") {
937        if term.contains("256color") {
938            return ColorSystem::EightBit;
939        }
940        if term == "xterm-kitty" {
941            return ColorSystem::TrueColor;
942        }
943    }
944    // Check NO_COLOR / CLICOLOR
945    if std::env::var("NO_COLOR").is_ok() {
946        return ColorSystem::Standard;
947    }
948    // Default to true color on modern terminals
949    if atty::is(atty::Stream::Stdout) {
950        ColorSystem::TrueColor
951    } else {
952        ColorSystem::Standard
953    }
954}
955
956// ---------------------------------------------------------------------------
957// Global console instance (like Rich's `get_console()`)
958// ---------------------------------------------------------------------------
959
960use std::sync::Mutex;
961use once_cell::sync::Lazy;
962
963static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
964    Mutex::new(Console::new())
965});
966
967/// Get a reference to the global Console.
968pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
969    GLOBAL_CONSOLE.lock().unwrap()
970}
971
972// ---------------------------------------------------------------------------
973// Convenience functions (like Rich's `print()`)
974// ---------------------------------------------------------------------------
975
976/// Print objects using the global console.
977pub fn print_objects(objects: &[&dyn Renderable]) {
978    let mut console = GLOBAL_CONSOLE.lock().unwrap();
979    console.print(objects, " ", "\n");
980}
981
982/// Print a string with markup support.
983pub fn print_str(text: &str) {
984    let mut console = GLOBAL_CONSOLE.lock().unwrap();
985    console.print_str(text);
986}
987
988/// Print formatted JSON.
989pub fn print_json_val(data: &serde_json::Value) {
990    let mut console = GLOBAL_CONSOLE.lock().unwrap();
991    console.print_json(data);
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997
998    #[test]
999    fn test_render_result_from_text() {
1000        let r = RenderResult::from_text("hello");
1001        assert_eq!(r.lines.len(), 1);
1002        assert_eq!(r.lines[0][0].text, "hello");
1003    }
1004
1005    #[test]
1006    fn test_console_options_default() {
1007        let opts = ConsoleOptions::default();
1008        assert!(opts.markup);
1009    }
1010
1011    #[test]
1012    fn test_console_quiet_default() {
1013        let console = Console::new();
1014        assert!(!console.quiet);
1015    }
1016
1017    #[test]
1018    fn test_console_quiet_setter() {
1019        let mut console = Console::new();
1020        console.set_quiet(true);
1021        assert!(console.quiet);
1022    }
1023
1024    #[test]
1025    fn test_console_quiet_builder() {
1026        let console = Console::new().quiet(true);
1027        assert!(console.quiet);
1028    }
1029
1030    #[test]
1031    fn test_console_quiet_suppresses_print() {
1032        let mut console = Console::new();
1033        console.quiet = true;
1034        // Should not panic
1035        console.print(&[], " ", "\n");
1036        console.println(&"test");
1037        console.print_str("test");
1038    }
1039
1040    #[test]
1041    fn test_console_soft_wrap_default() {
1042        let console = Console::new();
1043        assert!(!console.soft_wrap);
1044    }
1045
1046    #[test]
1047    fn test_console_soft_wrap_setter() {
1048        let mut console = Console::new();
1049        console.set_soft_wrap(true);
1050        assert!(console.soft_wrap);
1051    }
1052
1053    #[test]
1054    fn test_console_soft_wrap_builder() {
1055        let console = Console::new().soft_wrap(true);
1056        assert!(console.soft_wrap);
1057    }
1058
1059    #[test]
1060    fn test_console_is_terminal() {
1061        let console = Console::new();
1062        // is_terminal depends on whether stdout is a terminal
1063        let detected = console.is_terminal();
1064        assert_eq!(detected, atty::is(atty::Stream::Stdout));
1065    }
1066
1067    #[test]
1068    fn test_console_set_size() {
1069        let mut console = Console::new();
1070        console.set_size(120, 30);
1071        assert_eq!(console.width(), 120);
1072        assert_eq!(console.height(), 30);
1073        assert_eq!(console.options.max_width, 120);
1074        assert_eq!(console.options.max_height, 30);
1075    }
1076
1077    #[test]
1078    fn test_console_set_alt_screen() {
1079        let mut console = Console::new();
1080        // Just ensure it doesn't panic
1081        console.set_alt_screen(true);
1082        console.set_alt_screen(false);
1083    }
1084
1085    #[test]
1086    fn test_console_on_broken_pipe() {
1087        let console = Console::new();
1088        console.on_broken_pipe(); // no-op
1089    }
1090
1091    #[test]
1092    fn test_console_input_normal() {
1093        // We can't easily test stdin in unit tests, but we can verify
1094        // the method signature compiles and matches.
1095        let _console = Console::new();
1096        // input() cannot be meaningfully tested without actual stdin.
1097    }
1098
1099    #[test]
1100    fn test_console_debug() {
1101        let console = Console::new();
1102        let debug = format!("{:?}", console);
1103        assert!(debug.contains("Console"));
1104    }
1105
1106    #[test]
1107    fn test_console_with_file_has_no_terminal() {
1108        let console = Console::with_file(Box::new(std::io::sink()));
1109        assert!(!console.is_terminal());
1110    }
1111}