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    /// 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
354/// A renderable that renders multiple children one after another (vertically).
355///
356/// Equivalent to Python Rich's `Group`.
357#[derive(Debug, Clone)]
358pub struct Group {
359    /// The child renderables to render in sequence.
360    pub children: Vec<DynRenderable>,
361}
362
363impl Group {
364    /// Create an empty [`Group`].
365    pub fn new() -> Self {
366        Self { children: Vec::new() }
367    }
368
369    /// Add a renderable child to the group.
370    pub fn add(&mut self, renderable: impl Renderable + Send + Sync + 'static) {
371        self.children.push(DynRenderable::new(renderable));
372    }
373}
374
375/// Renders each child sequentially and concatenates their output lines.
376impl Renderable for Group {
377    fn render(&self, options: &ConsoleOptions) -> RenderResult {
378        let mut all_lines: Vec<Vec<Segment>> = Vec::new();
379        for child in &self.children {
380            let result = child.render(options);
381            all_lines.extend(result.lines);
382        }
383        RenderResult { lines: all_lines, items: Vec::new() }
384    }
385}
386
387// ---------------------------------------------------------------------------
388// Console
389// ---------------------------------------------------------------------------
390
391/// The main console for rendering rich output.
392pub struct Console {
393    /// The output writer.
394    pub file: Box<dyn Write + Send>,
395    /// Detected color system.
396    pub color_system: ColorSystem,
397    /// Current theme.
398    pub theme: Theme,
399    /// Default options.
400    pub options: ConsoleOptions,
401    /// Current width (may be overridden).
402    width: Option<usize>,
403    /// Current height (may be overridden).
404    height: Option<usize>,
405    /// Is this output a terminal?
406    is_terminal: bool,
407    /// If true, suppress all output.
408    pub quiet: bool,
409    /// If true, text wraps at word boundaries.
410    pub soft_wrap: bool,
411}
412
413impl Console {
414    /// Create a new Console writing to stdout.
415    pub fn new() -> Self {
416        let is_terminal = atty::is(atty::Stream::Stdout);
417        let color_system = detect_color_system();
418
419        let size = ConsoleDimensions::detect();
420
421        Self {
422            file: Box::new(io::stdout()) as Box<dyn Write + Send>,
423            color_system,
424            theme: crate::theme::default_theme(),
425            options: ConsoleOptions {
426                size,
427                is_terminal,
428                max_width: size.width,
429                max_height: size.height,
430                ..Default::default()
431            },
432            width: None,
433            height: None,
434            is_terminal,
435            quiet: false,
436            soft_wrap: false,
437        }
438    }
439
440    /// Create a Console that writes to a file.
441    pub fn with_file(file: Box<dyn Write + Send>) -> Self {
442        let _is_terminal = false;
443        Self {
444            file,
445            color_system: ColorSystem::Standard,
446            theme: crate::theme::default_theme(),
447            options: ConsoleOptions {
448                size: ConsoleDimensions { width: 80, height: 25 },
449                is_terminal: false,
450                max_width: 80,
451                max_height: 25,
452                ..Default::default()
453            },
454            width: None,
455            height: None,
456            is_terminal: false,
457            quiet: false,
458            soft_wrap: false,
459        }
460    }
461
462    /// Set the console width (overrides auto-detected terminal width).
463    pub fn set_width(&mut self, width: usize) {
464        self.width = Some(width);
465        self.options.max_width = width;
466    }
467
468    /// Set the console height.
469    pub fn set_height(&mut self, height: usize) {
470        self.height = Some(height);
471        self.options.max_height = height;
472    }
473
474    /// Get the effective width.
475    pub fn width(&self) -> usize {
476        self.width.unwrap_or(self.options.size.width)
477    }
478
479    /// Get the effective height.
480    pub fn height(&self) -> usize {
481        self.height.unwrap_or(self.options.size.height)
482    }
483
484    /// Render a renderable and return the segment lines.
485    pub fn render_lines(
486        &self,
487        renderable: &dyn Renderable,
488        options: &ConsoleOptions,
489        style: Option<&Style>,
490        _pad: bool,
491    ) -> Vec<Vec<Segment>> {
492        let result = renderable.render(options);
493
494        if let Some(st) = style {
495            result
496                .lines
497                .into_iter()
498                .map(|line| {
499                    line.into_iter()
500                        .map(|seg| {
501                            let new_style = if let Some(ref s) = seg.style {
502                                s.combine(st)
503                            } else {
504                                st.clone()
505                            };
506                            Segment::styled(seg.text, new_style)
507                        })
508                        .collect()
509                })
510                .collect()
511        } else {
512            result.lines
513        }
514    }
515
516    /// Look up a style by name from the theme.
517    pub fn get_style(&self, name: &str, default: &str) -> Option<Style> {
518        self.theme
519            .get(name)
520            .cloned()
521            .or_else(|| {
522                if !default.is_empty() {
523                    Some(Style::from_str(default))
524                } else {
525                    None
526                }
527            })
528    }
529
530    /// Render a string (with optional style).
531    pub fn render_str(&self, text: &str, style: &str) -> Text {
532        let st = self.get_style(style, "");
533        let mut t = Text::new(text);
534        if let Some(s) = st {
535            t = t.style(s);
536        }
537        t
538    }
539
540    // -----------------------------------------------------------------------
541    // print / log methods
542    // -----------------------------------------------------------------------
543
544    /// Print one or more renderable objects, separated by `sep`, ending with
545    /// `end`.
546    pub fn print(&mut self, objects: &[&dyn Renderable], sep: &str, end: &str) {
547        if self.quiet { return; }
548        let mut first = true;
549        for obj in objects {
550            if !first {
551                let _ = write!(self.file, "{sep}");
552            }
553            first = false;
554            let result = obj.render(&self.options);
555            let ansi = result.to_ansi();
556            let _ = write!(self.file, "{ansi}");
557        }
558        let _ = write!(self.file, "{end}");
559        let _ = self.file.flush();
560    }
561
562    /// Print a single renderable followed by a newline.
563    pub fn println(&mut self, renderable: &dyn Renderable) {
564        if self.quiet { return; }
565        let result = renderable.render(&self.options);
566        let ansi = result.to_ansi();
567        let _ = writeln!(self.file, "{ansi}");
568        let _ = self.file.flush();
569    }
570
571    /// Print a plain string (supports markup by default when `markup` is
572    /// enabled).
573    pub fn print_str(&mut self, text: &str) {
574        if self.quiet { return; }
575        let ansi = if self.options.markup {
576            let parsed = crate::markup::render(text);
577            parsed.render()
578        } else {
579            text.to_string()
580        };
581        let _ = write!(self.file, "{ansi}");
582        let _ = self.file.flush();
583    }
584
585    /// Print formatted JSON.
586    pub fn print_json(&mut self, data: &serde_json::Value) {
587        if self.quiet { return; }
588        let formatted = crate::json::render_json(data);
589        let result = formatted.render(&self.options);
590        let ansi = result.to_ansi();
591        let _ = writeln!(self.file, "{ansi}");
592        let _ = self.file.flush();
593    }
594
595    /// Clear the screen.
596    pub fn clear(&mut self) {
597        if self.quiet { return; }
598        let _ = write!(self.file, "\x1b[2J\x1b[H");
599        let _ = self.file.flush();
600    }
601
602    /// Show the cursor.
603    pub fn show_cursor(&mut self) {
604        let _ = write!(self.file, "\x1b[?25h");
605        let _ = self.file.flush();
606    }
607
608    /// Hide the cursor.
609    pub fn hide_cursor(&mut self) {
610        let _ = write!(self.file, "\x1b[?25l");
611        let _ = self.file.flush();
612    }
613
614    /// Set the terminal window title.
615    pub fn set_window_title(&mut self, title: &str) {
616        let _ = write!(self.file, "\x1b]0;{title}\x07");
617        let _ = self.file.flush();
618    }
619
620    /// Get the ANSI escape string for a given color as this console supports.
621    pub fn color_ansi(&self, color: &Color) -> String {
622        let downgraded = color.downgrade(self.color_system);
623        downgraded.to_string()
624    }
625
626    // -- Recursive rendering ------------------------------------------------
627
628    /// Render a renderable by recursively flattening nested items into
629    /// segments.  This is equivalent to Python Rich's `Console.render()`.
630    /// It handles `Group` composition and any renderable that yields other
631    /// renderables.
632    pub fn render(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> Vec<Segment> {
633        let result = renderable.render(options);
634        result.flatten(options)
635    }
636
637    /// Measure a renderable's width constraints.
638    /// Equivalent to Python Rich's `Measurement.get(console, options, renderable)`.
639    pub fn measure(&self, renderable: &dyn Renderable, options: &ConsoleOptions) -> crate::measure::Measurement {
640        if let Some(m) = renderable.measure(options) {
641            return m;
642        }
643        let segments = self.render(renderable, options);
644        let max_w = segments.iter()
645            .map(|s| s.cell_length())
646            .max()
647            .unwrap_or(0);
648        crate::measure::Measurement::new(max_w, options.max_width)
649    }
650
651    // -- Convenience render methods -----------------------------------------
652
653    /// Render a rule with the given title.
654    /// Equivalent to `Console.rule()`.
655    pub fn rule(
656        &mut self,
657        title: impl Into<String>,
658        characters: Option<&str>,
659        style: Option<Style>,
660        align: Option<AlignMethod>,
661    ) {
662        if self.quiet { return; }
663        let mut rule = crate::rule::Rule::new().title(title);
664        if let Some(chars) = characters { rule = rule.characters(chars); }
665        if let Some(st) = style { rule = rule.style(st); }
666        if let Some(a) = align { rule = rule.align(a); }
667        let result = rule.render(&self.options);
668        let ansi = result.to_ansi();
669        let _ = write!(self.file, "{ansi}");
670        let _ = self.file.flush();
671    }
672
673    /// Output a bell character.
674    pub fn bell(&mut self) {
675        if self.quiet { return; }
676        let _ = write!(self.file, "\x07");
677        let _ = self.file.flush();
678    }
679
680    /// Output blank lines.
681    pub fn line(&mut self, count: usize) {
682        if self.quiet { return; }
683        for _ in 0..count {
684            let _ = writeln!(self.file);
685        }
686        let _ = self.file.flush();
687    }
688
689    /// Output a log entry with timestamp, caller info.
690    pub fn log(&mut self, objects: &[&dyn Renderable]) {
691        if self.quiet { return; }
692        let now = chrono::Local::now();
693        let time_str = format!("[{}]", now.format("%H:%M:%S"));
694        let _ = write!(self.file, "{} ", Style::new().dim(true).to_ansi());
695        let _ = write!(self.file, "{time_str} ");
696        let _ = write!(self.file, "{}", Style::new().reset_ansi());
697        self.print(objects, " ", "\n");
698    }
699
700    // -- Theme stack --------------------------------------------------------
701
702    /// Push a theme onto the stack.
703    pub fn push_theme(&mut self, theme: Theme) {
704        let mut new_theme = theme.clone();
705        new_theme.inherit = Some(Box::new(self.theme.clone()));
706        self.theme = new_theme;
707    }
708
709    /// Pop the current theme, restoring the previous one.
710    pub fn pop_theme(&mut self) {
711        if let Some(ref inherit) = self.theme.inherit {
712            self.theme = *inherit.clone();
713        }
714    }
715
716    // -- Export methods ------------------------------------------------------
717
718    /// Export the current console output as an HTML document.
719    ///
720    /// Renders the given renderable and wraps it in a styled HTML page.
721    pub fn export_html(&self, renderable: &dyn Renderable) -> String {
722        let result = renderable.render(&self.options);
723        let ansi = result.to_ansi();
724        crate::export::export_html(&crate::export::ExportHtmlOptions {
725            code: crate::export::strip_ansi_escapes(&ansi),
726            ..Default::default()
727        })
728    }
729
730    /// Save rendered output as an HTML file.
731    pub fn save_html(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
732        let html = self.export_html(renderable);
733        crate::export::save_html(path, &crate::export::ExportHtmlOptions {
734            code: html,
735            ..Default::default()
736        })
737    }
738
739    /// Export the current console output as an SVG document.
740    pub fn export_svg(&self, renderable: &dyn Renderable) -> String {
741        let result = renderable.render(&self.options);
742        let ansi = result.to_ansi();
743        crate::export::export_svg(&crate::export::ExportSvgOptions {
744            code: crate::export::strip_ansi_escapes(&ansi),
745            ..Default::default()
746        })
747    }
748
749    /// Save rendered output as an SVG file.
750    pub fn save_svg(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
751        let svg = self.export_svg(renderable);
752        crate::export::save_svg(path, &crate::export::ExportSvgOptions {
753            code: svg,
754            ..Default::default()
755        })
756    }
757
758    /// Export the current console output as plain text (strips ANSI).
759    pub fn export_text(&self, renderable: &dyn Renderable) -> String {
760        let result = renderable.render(&self.options);
761        let ansi = result.to_ansi();
762        crate::export::export_text(&crate::export::ExportTextOptions {
763            text: ansi,
764            strip_ansi: true,
765        })
766    }
767
768    /// Save rendered output as a plain text file.
769    pub fn save_text(&self, path: impl AsRef<std::path::Path>, renderable: &dyn Renderable) -> std::io::Result<()> {
770        let text = self.export_text(renderable);
771        crate::export::save_text(path, &crate::export::ExportTextOptions {
772            text,
773            strip_ansi: false,
774        })
775    }
776
777    // -- Context manager equivalent -----------------------------------------
778
779    /// Enter a capture context. Returns the Console back so it can be
780    /// used inside a block. Call `end_capture()` to get the captured text.
781    pub fn begin_capture(&mut self) {
782        // In Rust, capture would need to swap the file with a buffer.
783        // For now, this is a no-op placeholder.
784    }
785
786    /// End capture and return captured text.
787    pub fn end_capture(&mut self) -> String {
788        String::new() // placeholder
789    }
790
791    // -- Quiet / Soft-wrap setters ------------------------------------------
792
793    /// Set the quiet flag (suppress all output when true).
794    pub fn set_quiet(&mut self, quiet: bool) {
795        self.quiet = quiet;
796    }
797
798    /// Builder-style setter for quiet.
799    pub fn quiet(mut self, quiet: bool) -> Self {
800        self.quiet = quiet;
801        self
802    }
803
804    /// Set the soft-wrap flag (wrap text at word boundaries when true).
805    pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
806        self.soft_wrap = soft_wrap;
807    }
808
809    /// Builder-style setter for soft_wrap.
810    pub fn soft_wrap(mut self, soft_wrap: bool) -> Self {
811        self.soft_wrap = soft_wrap;
812        self
813    }
814
815    // -- Input --------------------------------------------------------------
816
817    /// Read a line of input from the user.
818    ///
819    /// Writes `prompt` to the console, then reads a line from stdin.
820    /// When `password` is true, the input is masked with `*` characters
821    /// (using raw terminal mode via crossterm).
822    pub fn input(&mut self, prompt: &str, password: bool) -> String {
823        let _ = write!(self.file, "{prompt}");
824        let _ = self.file.flush();
825
826        if password {
827            self.read_password()
828        } else {
829            let mut input = String::new();
830            let _ = io::stdin().read_line(&mut input);
831            input.trim().to_string()
832        }
833    }
834
835    /// Read a password from stdin with character masking.
836    fn read_password(&mut self) -> String {
837        use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
838        use std::io::Read;
839
840        match enable_raw_mode() {
841            Ok(()) => {
842                let stdin = io::stdin();
843                let mut handle = stdin.lock();
844                let mut buf = [0u8; 1];
845                let mut password = String::new();
846
847                loop {
848                    match handle.read_exact(&mut buf) {
849                        Ok(()) => match buf[0] {
850                            b'\r' | b'\n' => {
851                                let _ = writeln!(self.file);
852                                let _ = self.file.flush();
853                                break;
854                            }
855                            b'\x03' => {
856                                // Ctrl+C — break and return what we have
857                                let _ = writeln!(self.file);
858                                let _ = self.file.flush();
859                                break;
860                            }
861                            b'\x7f' | b'\x08' => {
862                                // Backspace
863                                password.pop();
864                            }
865                            c => {
866                                password.push(c as char);
867                                let _ = write!(self.file, "*");
868                                let _ = self.file.flush();
869                            }
870                        },
871                        Err(_) => break,
872                    }
873                }
874                let _ = disable_raw_mode();
875                password
876            }
877            Err(_) => {
878                // Fallback: read without masking
879                let mut input = String::new();
880                let _ = io::stdin().read_line(&mut input);
881                input.trim().to_string()
882            }
883        }
884    }
885
886    // -- Screen / alternate screen ------------------------------------------
887
888    /// Create a [`ScreenContext`](crate::screen::ScreenContext) that enters the
889    /// alternate screen buffer. The context automatically exits the alternate
890    /// screen when dropped.
891    pub fn screen(&mut self) -> crate::screen::ScreenContext {
892        let mut ctx = crate::screen::ScreenContext::new();
893        ctx.enter();
894        ctx
895    }
896
897    /// Enter or exit the alternate screen buffer by writing the corresponding
898    /// escape sequences (`\x1b[?1049h` / `\x1b[?1049l`).
899    pub fn set_alt_screen(&mut self, enable: bool) {
900        if enable {
901            let _ = write!(self.file, "\x1b[?1049h");
902        } else {
903            let _ = write!(self.file, "\x1b[?1049l");
904        }
905        let _ = self.file.flush();
906    }
907
908    /// Get whether the output is a terminal.
909    pub fn is_terminal(&self) -> bool {
910        self.is_terminal
911    }
912
913    /// Set the terminal size (overrides auto-detected dimensions).
914    pub fn set_size(&mut self, width: usize, height: usize) {
915        self.width = Some(width);
916        self.height = Some(height);
917        self.options.max_width = width;
918        self.options.max_height = height;
919        self.options.size = crate::console::ConsoleDimensions { width, height };
920    }
921
922    /// Handle broken pipe errors gracefully.
923    ///
924    /// In Rust, `write()` returns `ErrorKind::BrokenPipe` instead of raising
925    /// `SIGPIPE`, so broken pipes are not fatal. The Console already uses
926    /// `let _ = write!(...)` throughout, which silently discards all write
927    /// errors including EPIPE. This method is provided for API compatibility
928    /// with Python Rich and as a documentation point.
929    pub fn on_broken_pipe(&self) {
930        // No-op: Rust handles EPIPE via ErrorKind, not signals.
931        // All Console write operations use `let _ = write!()` which
932        // already discards BrokenPipe errors without panicking.
933    }
934}
935
936impl Default for Console {
937    fn default() -> Self {
938        Self::new()
939    }
940}
941
942impl fmt::Debug for Console {
943    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944        f.debug_struct("Console")
945            .field("color_system", &self.color_system)
946            .field("width", &self.width())
947            .field("height", &self.height())
948            .field("is_terminal", &self.is_terminal)
949            .finish()
950    }
951}
952
953// ---------------------------------------------------------------------------
954// Color system detection
955// ---------------------------------------------------------------------------
956
957/// Detect the terminal color system from environment variables.
958///
959/// Checks `COLORTERM`, `TERM`, `NO_COLOR`, and `CLICOLOR` to determine
960/// whether the terminal supports true color, 8-bit, or standard 16 colors.
961fn detect_color_system() -> ColorSystem {
962    // Check common env vars
963    if let Ok(val) = std::env::var("COLORTERM") {
964        if val == "truecolor" || val == "24bit" {
965            return ColorSystem::TrueColor;
966        }
967    }
968    if let Ok(term) = std::env::var("TERM") {
969        if term.contains("256color") {
970            return ColorSystem::EightBit;
971        }
972        if term == "xterm-kitty" {
973            return ColorSystem::TrueColor;
974        }
975    }
976    // Check NO_COLOR / CLICOLOR
977    if std::env::var("NO_COLOR").is_ok() {
978        return ColorSystem::Standard;
979    }
980    // Default to true color on modern terminals
981    if atty::is(atty::Stream::Stdout) {
982        ColorSystem::TrueColor
983    } else {
984        ColorSystem::Standard
985    }
986}
987
988// ---------------------------------------------------------------------------
989// Global console instance (like Rich's `get_console()`)
990// ---------------------------------------------------------------------------
991
992use std::sync::Mutex;
993use once_cell::sync::Lazy;
994
995static GLOBAL_CONSOLE: Lazy<Mutex<Console>> = Lazy::new(|| {
996    Mutex::new(Console::new())
997});
998
999/// Get a reference to the global Console.
1000pub fn get_console() -> std::sync::MutexGuard<'static, Console> {
1001    GLOBAL_CONSOLE.lock().unwrap()
1002}
1003
1004// ---------------------------------------------------------------------------
1005// Convenience functions (like Rich's `print()`)
1006// ---------------------------------------------------------------------------
1007
1008/// Print objects using the global console.
1009pub fn print_objects(objects: &[&dyn Renderable]) {
1010    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1011    console.print(objects, " ", "\n");
1012}
1013
1014/// Print a string with markup support.
1015pub fn print_str(text: &str) {
1016    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1017    console.print_str(text);
1018}
1019
1020/// Print formatted JSON.
1021pub fn print_json_val(data: &serde_json::Value) {
1022    let mut console = GLOBAL_CONSOLE.lock().unwrap();
1023    console.print_json(data);
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::*;
1029
1030    #[test]
1031    fn test_render_result_from_text() {
1032        let r = RenderResult::from_text("hello");
1033        assert_eq!(r.lines.len(), 1);
1034        assert_eq!(r.lines[0][0].text, "hello");
1035    }
1036
1037    #[test]
1038    fn test_console_options_default() {
1039        let opts = ConsoleOptions::default();
1040        assert!(opts.markup);
1041    }
1042
1043    #[test]
1044    fn test_console_quiet_default() {
1045        let console = Console::new();
1046        assert!(!console.quiet);
1047    }
1048
1049    #[test]
1050    fn test_console_quiet_setter() {
1051        let mut console = Console::new();
1052        console.set_quiet(true);
1053        assert!(console.quiet);
1054    }
1055
1056    #[test]
1057    fn test_console_quiet_builder() {
1058        let console = Console::new().quiet(true);
1059        assert!(console.quiet);
1060    }
1061
1062    #[test]
1063    fn test_console_quiet_suppresses_print() {
1064        let mut console = Console::new();
1065        console.quiet = true;
1066        // Should not panic
1067        console.print(&[], " ", "\n");
1068        console.println(&"test");
1069        console.print_str("test");
1070    }
1071
1072    #[test]
1073    fn test_console_soft_wrap_default() {
1074        let console = Console::new();
1075        assert!(!console.soft_wrap);
1076    }
1077
1078    #[test]
1079    fn test_console_soft_wrap_setter() {
1080        let mut console = Console::new();
1081        console.set_soft_wrap(true);
1082        assert!(console.soft_wrap);
1083    }
1084
1085    #[test]
1086    fn test_console_soft_wrap_builder() {
1087        let console = Console::new().soft_wrap(true);
1088        assert!(console.soft_wrap);
1089    }
1090
1091    #[test]
1092    fn test_console_is_terminal() {
1093        let console = Console::new();
1094        // is_terminal depends on whether stdout is a terminal
1095        let detected = console.is_terminal();
1096        assert_eq!(detected, atty::is(atty::Stream::Stdout));
1097    }
1098
1099    #[test]
1100    fn test_console_set_size() {
1101        let mut console = Console::new();
1102        console.set_size(120, 30);
1103        assert_eq!(console.width(), 120);
1104        assert_eq!(console.height(), 30);
1105        assert_eq!(console.options.max_width, 120);
1106        assert_eq!(console.options.max_height, 30);
1107    }
1108
1109    #[test]
1110    fn test_console_set_alt_screen() {
1111        let mut console = Console::new();
1112        // Just ensure it doesn't panic
1113        console.set_alt_screen(true);
1114        console.set_alt_screen(false);
1115    }
1116
1117    #[test]
1118    fn test_console_on_broken_pipe() {
1119        let console = Console::new();
1120        console.on_broken_pipe(); // no-op
1121    }
1122
1123    #[test]
1124    fn test_console_input_normal() {
1125        // We can't easily test stdin in unit tests, but we can verify
1126        // the method signature compiles and matches.
1127        let _console = Console::new();
1128        // input() cannot be meaningfully tested without actual stdin.
1129    }
1130
1131    #[test]
1132    fn test_console_debug() {
1133        let console = Console::new();
1134        let debug = format!("{:?}", console);
1135        assert!(debug.contains("Console"));
1136    }
1137
1138    #[test]
1139    fn test_console_with_file_has_no_terminal() {
1140        let console = Console::with_file(Box::new(std::io::sink()));
1141        assert!(!console.is_terminal());
1142    }
1143}