Skip to main content

rich/
console.rs

1//! The Console — the high-level rendering entry point.
2//!
3//! Port of upstream `rich/console.py` (core subset): terminal / color-system /
4//! width detection, markup + highlighter application, and writing styled output.
5//! Layout options, capture, export, and paging land in the Console-completeness
6//! issue.
7
8use std::io::{IsTerminal, Write};
9
10use crate::color::ColorSystem;
11use crate::protocol::{Highlighter, Renderable};
12use crate::segment::Segment;
13use crate::style::Style;
14use crate::text::Text;
15use crate::theme::Theme;
16
17const DEFAULT_WIDTH: usize = 80;
18const DEFAULT_HEIGHT: usize = 25;
19
20/// A source of the current time in seconds. Upstream's `GetTimeCallable`,
21/// shared by [`Console::get_time`] and [`Progress`](crate::progress::Progress).
22pub type GetTime = std::sync::Arc<dyn Fn() -> f64 + Send + Sync>;
23
24/// Seconds on a monotonic clock: upstream's default `time.monotonic`. Every
25/// default clock in the crate reads this one origin, so times taken from a
26/// console and from a progress display are comparable.
27pub(crate) fn monotonic() -> f64 {
28    static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
29    START
30        .get_or_init(std::time::Instant::now)
31        .elapsed()
32        .as_secs_f64()
33}
34
35/// Horizontal justification of a renderable within its width.
36/// Mirrors `rich.console.JustifyMethod`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum Justify {
39    /// Renderable-defined default (usually left, no padding).
40    #[default]
41    Default,
42    Left,
43    Center,
44    Right,
45    Full,
46}
47
48/// What to do with text that is wider than the space available.
49/// Mirrors `rich.console.OverflowMethod`.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum Overflow {
52    /// Break over-long words across lines. Upstream's `DEFAULT_OVERFLOW`.
53    #[default]
54    Fold,
55    /// Cut the line off at the width.
56    Crop,
57    /// Cut the line off one cell early and mark it with `…`.
58    Ellipsis,
59    /// Leave over-long lines intact, and do not wrap.
60    Ignore,
61}
62
63/// The options passed to a [`Renderable`] describing the space it must fit into.
64///
65/// Port of the core of `rich.console.ConsoleOptions`. Only the fields needed by
66/// the currently-ported renderables are present; more are added as widgets land.
67#[derive(Debug, Clone)]
68pub struct ConsoleOptions {
69    pub min_width: usize,
70    pub max_width: usize,
71    pub height: Option<usize>,
72    pub justify: Justify,
73    /// Overflow method to impose on renderables, or `None` to let each pick its
74    /// own. Mirrors `ConsoleOptions.overflow`.
75    pub overflow: Option<Overflow>,
76    /// Disable wrapping, or `None` to let each renderable pick. Mirrors
77    /// `ConsoleOptions.no_wrap`.
78    pub no_wrap: Option<bool>,
79    /// Highlight override for strings rendered under these options, or `None`
80    /// for the console default. Mirrors `ConsoleOptions.highlight`: `Panel`,
81    /// `Table` and `Tree` set it for their children, and upstream's
82    /// `Console.render` passes it to `render_str` for a `str` renderable.
83    pub highlight: Option<bool>,
84    /// Markup override for strings rendered under these options, or `None` for
85    /// the console default. Mirrors `ConsoleOptions.markup`.
86    pub markup: Option<bool>,
87    /// Height of the container (starts as the terminal height). Mirrors
88    /// `ConsoleOptions.max_height`.
89    pub max_height: usize,
90    /// Encoding of the terminal (`"utf-8"`, or `"ascii"` for an ASCII-only
91    /// console). Mirrors `ConsoleOptions.encoding`.
92    pub encoding: String,
93    /// Whether the target is a terminal. Mirrors `ConsoleOptions.is_terminal`.
94    pub is_terminal: bool,
95    /// Whether the target is a legacy Windows console. Mirrors
96    /// `ConsoleOptions.legacy_windows`.
97    pub legacy_windows: bool,
98    /// The size of the console. Mirrors `ConsoleOptions.size`.
99    pub size: ConsoleDimensions,
100}
101
102/// The size of a console in cells. Mirrors `rich.console.ConsoleDimensions`.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
104pub struct ConsoleDimensions {
105    /// Width in cells.
106    pub width: usize,
107    /// Height in rows.
108    pub height: usize,
109}
110
111impl Default for ConsoleOptions {
112    /// The options of a default 80x25 UTF-8 console, as
113    /// [`Console::options`] builds them.
114    fn default() -> Self {
115        ConsoleOptions {
116            min_width: 1,
117            max_width: DEFAULT_WIDTH,
118            height: None,
119            justify: Justify::Default,
120            overflow: None,
121            no_wrap: None,
122            highlight: None,
123            markup: None,
124            max_height: DEFAULT_HEIGHT,
125            encoding: "utf-8".to_string(),
126            is_terminal: false,
127            legacy_windows: false,
128            size: ConsoleDimensions {
129                width: DEFAULT_WIDTH,
130                height: DEFAULT_HEIGHT,
131            },
132        }
133    }
134}
135
136impl ConsoleOptions {
137    /// Return a copy with `max_width` (and a clamped `min_width`) updated.
138    /// Port of `ConsoleOptions.update_width`.
139    pub fn update_width(&self, width: usize) -> ConsoleOptions {
140        // Copy-then-overwrite rather than a fresh literal, so fields added later
141        // are carried through instead of being silently reset to a default.
142        let mut options = self.clone();
143        options.min_width = width;
144        options.max_width = width;
145        options
146    }
147
148    /// Return a copy with both width and height pinned. Port of
149    /// `ConsoleOptions.update_dimensions`.
150    pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
151        let mut options = self.update_width(width);
152        options.height = Some(height);
153        options.max_height = height;
154        options
155    }
156
157    /// Return a copy with the height (and `max_height`) set. Port of
158    /// `ConsoleOptions.update_height`.
159    pub fn update_height(&self, height: usize) -> ConsoleOptions {
160        let mut options = self.clone();
161        options.height = Some(height);
162        options.max_height = height;
163        options
164    }
165
166    /// Return a copy with `height` cleared. Port of
167    /// `ConsoleOptions.reset_height`.
168    pub fn reset_height(&self) -> ConsoleOptions {
169        let mut options = self.clone();
170        options.height = None;
171        options
172    }
173
174    /// Whether renderables should use ASCII only: the encoding is not a UTF
175    /// one. Port of the `ConsoleOptions.ascii_only` property.
176    pub fn ascii_only(&self) -> bool {
177        !self.encoding.starts_with("utf")
178    }
179}
180
181/// Keyword arguments of upstream's `Console.render_str`, for
182/// [`Console::render_str_with`]. `None` takes the console's default.
183#[derive(Clone, Default)]
184pub struct RenderStrOptions<'a> {
185    /// Base style of the result (`style=`, default none).
186    pub style: crate::style::StyleType,
187    /// `justify=`; `None` leaves the text's justify unset.
188    pub justify: Option<Justify>,
189    /// `overflow=`; `None` leaves the text's overflow unset.
190    pub overflow: Option<Overflow>,
191    /// `emoji=`: replace emoji codes, or `None` for the console default.
192    pub emoji: Option<bool>,
193    /// `markup=`: parse console markup, or `None` for the console default.
194    pub markup: Option<bool>,
195    /// `highlight=`: highlight, or `None` for the console default.
196    pub highlight: Option<bool>,
197    /// `highlighter=`: highlight with this instead of the console's
198    /// highlighters (registered ones plus `ReprHighlighter`).
199    pub highlighter: Option<&'a dyn Highlighter>,
200}
201
202/// Per-thread capture buffer stacks (innermost capture last).
203type CaptureStacks = std::collections::HashMap<std::thread::ThreadId, Vec<Vec<Segment>>>;
204
205/// The calling thread's innermost capture buffer, if it is capturing.
206fn current_capture(captures: &mut CaptureStacks) -> Option<&mut Vec<Segment>> {
207    captures
208        .get_mut(&std::thread::current().id())
209        .and_then(|stack| stack.last_mut())
210}
211
212/// The high-level interface for rendering to a terminal. Mirrors
213/// `rich.console.Console`.
214pub struct Console {
215    render_environment: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
216    /// The default code highlighter (see `ConsoleCodeHighlighting`).
217    code_highlighting: Option<crate::protocol::CodeHighlighting>,
218    color_system: Option<ColorSystem>,
219    width: usize,
220    height: usize,
221    is_terminal: bool,
222    no_color: bool,
223    /// Upstream's `Console.get_time`: the clock animations read.
224    get_time: GetTime,
225    emoji: bool,
226    highlight: bool,
227    legacy_windows: bool,
228    safe_box: bool,
229    ascii_only: bool,
230    /// Upstream's `ThemeStack`: the builder's theme at the bottom, pushed
231    /// themes above it. Never empty; styles resolve against the top entry.
232    theme_stack: Vec<Theme>,
233    base_style: Style,
234    /// Registered highlighters. Shared (`Arc`) so a [`Clone`] of the console
235    /// keeps them; each is behind a lock so the console is `Sync`.
236    highlighters: Vec<std::sync::Arc<dyn Highlighter + Send + Sync>>,
237    /// Upstream's `Console(markup=…)`: whether printed strings are parsed as
238    /// console markup.
239    markup: bool,
240    /// Upstream's `Console(emoji_variant=…)`: the variant appended to emoji
241    /// codes that name none.
242    emoji_variant: Option<crate::emoji::EmojiVariant>,
243    /// Upstream's `Console(tab_size=…)`: the tab stop width `Text` expands to.
244    tab_size: usize,
245    /// While a thread is capturing, its print paths append their segments to
246    /// the innermost buffer of its stack here instead of writing to stdout.
247    /// Mirrors upstream's `ConsoleThreadLocals.buffer`: capture state is per
248    /// thread, so a thread printing while another captures is not swallowed
249    /// by that capture, and concurrent captures never mix. Each nested
250    /// capture on a thread pushes a buffer and pops it when it ends.
251    captures: std::sync::Mutex<CaptureStacks>,
252    /// Upstream's `Console._is_alt_screen`: set by
253    /// [`set_alt_screen`](Console::set_alt_screen).
254    is_alt_screen: std::sync::atomic::AtomicBool,
255}
256
257/// A `Send`-only highlighter behind a lock, so it can be shared by a `Sync`
258/// console.
259struct LockedHighlighter(std::sync::Mutex<Box<dyn Highlighter + Send>>);
260
261impl Highlighter for LockedHighlighter {
262    fn highlight(&self, text: &mut Text) {
263        let highlighter = self
264            .0
265            .lock()
266            .unwrap_or_else(std::sync::PoisonError::into_inner);
267        highlighter.highlight(text);
268    }
269}
270
271/// `NoAltScreen("Alt screen must be enabled to call update_screen")`.
272fn no_alt_screen() -> crate::errors::RichError {
273    crate::errors::RichError::NoAltScreen(
274        "Alt screen must be enabled to call update_screen".to_string(),
275    )
276}
277
278/// Rendered lines placed at an offset on the screen: each line is preceded
279/// by a cursor move to its row. Port of `rich.console.ScreenUpdate`.
280pub struct ScreenUpdate {
281    lines: Vec<Vec<Segment>>,
282    x: usize,
283    y: usize,
284}
285
286impl ScreenUpdate {
287    /// `lines` drawn from column `x`, row `y`.
288    pub fn new(lines: Vec<Vec<Segment>>, x: usize, y: usize) -> Self {
289        ScreenUpdate { lines, x, y }
290    }
291}
292
293impl Renderable for ScreenUpdate {
294    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
295        let mut segments = Vec::new();
296        for (offset, line) in self.lines.iter().enumerate() {
297            // `Control.move_to(x, y + offset)`, widened so huge coordinates
298            // are written in full (upstream's ints are unbounded).
299            let to = crate::control::move_to_code(self.x as u128, self.y as u128 + offset as u128);
300            segments.push(Segment::control(to));
301            segments.extend(line.iter().cloned());
302        }
303        segments
304    }
305}
306
307impl Clone for Console {
308    /// An independent console with the same configuration, theme stack and
309    /// highlighters (shared). The clone starts with an empty capture buffer
310    /// and is not capturing, whatever the original is doing.
311    fn clone(&self) -> Self {
312        Console {
313            render_environment: self.render_environment.clone(),
314            code_highlighting: self.code_highlighting.clone(),
315            color_system: self.color_system,
316            width: self.width,
317            height: self.height,
318            is_terminal: self.is_terminal,
319            no_color: self.no_color,
320            get_time: self.get_time.clone(),
321            emoji: self.emoji,
322            highlight: self.highlight,
323            legacy_windows: self.legacy_windows,
324            safe_box: self.safe_box,
325            ascii_only: self.ascii_only,
326            theme_stack: self.theme_stack.clone(),
327            base_style: self.base_style.clone(),
328            highlighters: self.highlighters.clone(),
329            markup: self.markup,
330            emoji_variant: self.emoji_variant,
331            tab_size: self.tab_size,
332            captures: std::sync::Mutex::new(CaptureStacks::new()),
333            is_alt_screen: std::sync::atomic::AtomicBool::new(false),
334        }
335    }
336}
337
338/// A theme in use on a [`Console`] until this guard drops. Returned by
339/// [`Console::use_theme`]; upstream's `ThemeContext`.
340pub struct ThemeContext<'a> {
341    console: &'a mut Console,
342}
343
344impl std::ops::Deref for ThemeContext<'_> {
345    type Target = Console;
346
347    fn deref(&self) -> &Console {
348        self.console
349    }
350}
351
352impl std::ops::DerefMut for ThemeContext<'_> {
353    fn deref_mut(&mut self) -> &mut Console {
354        self.console
355    }
356}
357
358impl Drop for ThemeContext<'_> {
359    fn drop(&mut self) {
360        // Upstream's `__exit__` pops unconditionally. This only fails if the
361        // caller already popped back down to the base through the guard.
362        let _ = self.console.pop_theme();
363    }
364}
365
366impl Default for Console {
367    fn default() -> Self {
368        Console::new()
369    }
370}
371
372impl Console {
373    /// Auto-detect terminal capabilities from the environment.
374    pub fn new() -> Self {
375        ConsoleBuilder::new().build()
376    }
377
378    /// Start configuring a console explicitly (used by tests and `rich-ext`).
379    pub fn builder() -> ConsoleBuilder {
380        ConsoleBuilder::new()
381    }
382
383    /// The active color system, or `None` when styles are not rendered at
384    /// all. Port of `Console.color_system`.
385    ///
386    /// Like upstream, this is independent of [`no_color`](Self::no_color):
387    /// no-colour mode keeps the colour system and strips only the colours at
388    /// output time, so bold, italic, underline and the like still render.
389    /// Callers asking "will colour reach the terminal?" must check both.
390    pub fn color_system(&self) -> Option<ColorSystem> {
391        self.color_system
392    }
393
394    /// Whether colour output is disabled. Port of `Console.no_color`: set by
395    /// the builder, or by a non-empty `NO_COLOR` environment variable.
396    pub fn no_color(&self) -> bool {
397        self.no_color
398    }
399
400    /// The current time in seconds from this console's clock. Port of
401    /// `Console.get_time` (default `time.monotonic`); override it with
402    /// [`ConsoleBuilder::get_time`] for deterministic animation.
403    pub fn get_time(&self) -> f64 {
404        (self.get_time)()
405    }
406
407    /// The detected (or configured) width in cells.
408    pub fn width(&self) -> usize {
409        self.width
410    }
411
412    /// The detected (or configured) height in rows. Used by height-aware
413    /// renderables such as [`Layout`](crate::layout::Layout).
414    pub fn height(&self) -> usize {
415        self.height
416    }
417
418    /// Whether output is going to a real terminal.
419    pub fn is_terminal(&self) -> bool {
420        self.is_terminal
421    }
422
423    /// Whether output targets a legacy Windows console (drives box substitution).
424    pub fn legacy_windows(&self) -> bool {
425        self.legacy_windows
426    }
427
428    /// Whether to substitute box glyphs for terminal-safe variants (default on).
429    pub fn safe_box(&self) -> bool {
430        self.safe_box
431    }
432
433    /// Whether the terminal can only render ASCII (forces the `ASCII` box).
434    pub fn ascii_only(&self) -> bool {
435        self.ascii_only
436    }
437
438    /// The active theme: the top of the theme stack.
439    pub fn theme(&self) -> &Theme {
440        self.theme_stack
441            .last()
442            .expect("the theme stack always holds its base theme")
443    }
444
445    /// Resolve a style name (or pass a style through) against this console's
446    /// theme. Port of `Console.get_style`.
447    pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
448        self.theme().get_style(style)
449    }
450
451    /// Push a theme on to the top of the stack. Port of `Console.push_theme`.
452    ///
453    /// With `inherit` the new top is the current top's styles overridden by
454    /// `theme`'s; without it, the new top is exactly `theme`. Prefer
455    /// [`use_theme`](Self::use_theme), which pops again automatically.
456    pub fn push_theme(&mut self, theme: Theme, inherit: bool) {
457        let top = if inherit {
458            let mut merged = self.theme().clone();
459            merged.extend_from(&theme);
460            merged
461        } else {
462            theme
463        };
464        self.theme_stack.push(top);
465    }
466
467    /// Remove the top theme, restoring the previous one. Port of
468    /// `Console.pop_theme`; popping the base theme is an error
469    /// (upstream's `ThemeStackError("Unable to pop base theme")`).
470    pub fn pop_theme(&mut self) -> crate::errors::Result<()> {
471        if self.theme_stack.len() == 1 {
472            return Err(crate::errors::RichError::ThemeStack(
473                "Unable to pop base theme".to_string(),
474            ));
475        }
476        self.theme_stack.pop();
477        Ok(())
478    }
479
480    /// Use a theme until the returned guard is dropped. Port of
481    /// `Console.use_theme`, Python's context manager as an RAII guard.
482    ///
483    /// The guard dereferences to the console, so print *through the guard*
484    /// while it is alive; dropping it pops the theme, including during a
485    /// panic unwind.
486    ///
487    /// ```
488    /// # use rich::{Console, Theme, Style};
489    /// let mut console = Console::builder().width(20).build();
490    /// let mut theme = Theme::new();
491    /// theme.insert("warning", Style::parse("bold red").unwrap());
492    /// {
493    ///     let themed = console.use_theme(theme);
494    ///     assert!(themed.theme().get("warning").is_some());
495    /// }
496    /// assert!(console.theme().get("warning").is_none());
497    /// ```
498    ///
499    /// Upstream's `use_theme` also takes `inherit`, but its `ThemeContext`
500    /// never passes it on to `push_theme`, so a used theme always inherits
501    /// (verified against rich 15.0.0). This port keeps that behaviour and
502    /// omits the ignored parameter; call [`push_theme`](Self::push_theme) to
503    /// replace the styles outright.
504    pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
505        self.push_theme(theme, true);
506        ThemeContext { console: self }
507    }
508
509    /// The whole-output base style.
510    pub fn base_style(&self) -> &Style {
511        &self.base_style
512    }
513
514    /// Register a highlighter. **The core plugin seam** — see docs/PLUGINS.md.
515    /// The highlighter must be `Send` so a [`Console`](Console) can move to a
516    /// background thread (e.g. an auto-refreshing [`Live`](crate::live::Live)).
517    pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
518        self.highlighters
519            .push(std::sync::Arc::new(LockedHighlighter(
520                std::sync::Mutex::new(highlighter),
521            )));
522    }
523
524    /// The default render options for this console (full width, no height).
525    pub fn options(&self) -> ConsoleOptions {
526        ConsoleOptions {
527            min_width: 1,
528            max_width: self.width,
529            height: None,
530            justify: Justify::Default,
531            overflow: None,
532            no_wrap: None,
533            highlight: None,
534            markup: None,
535            max_height: self.height,
536            encoding: self.encoding().to_string(),
537            is_terminal: self.is_terminal,
538            legacy_windows: self.legacy_windows,
539            size: self.size(),
540        }
541    }
542
543    /// The size of the console. Port of the `Console.size` property.
544    pub fn size(&self) -> ConsoleDimensions {
545        ConsoleDimensions {
546            width: self.width,
547            height: self.height,
548        }
549    }
550
551    /// The output encoding: `"ascii"` for an [ASCII-only](Self::ascii_only)
552    /// console, else `"utf-8"`. Port of the `Console.encoding` property (which
553    /// reads the output file's encoding; this port has no file to ask).
554    pub fn encoding(&self) -> &'static str {
555        if self.ascii_only {
556            "ascii"
557        } else {
558            "utf-8"
559        }
560    }
561
562    /// Render a value to an ANSI string (no trailing newline). Primarily for
563    /// tests and inline rendering.
564    ///
565    /// When no explicit justify is requested, the width is first shrunk to the
566    /// renderable's measured width (matching upstream's measurement-fit for a
567    /// bare top-level renderable).
568    pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
569        let segments = self.render_segments(renderable);
570        self.segments_to_string(&segments)
571    }
572
573    /// Render a renderable to segments as `Console.print` does (shared by the
574    /// string and print paths): a printed `Text` goes through upstream's
575    /// `Text.join`, and an extension that opts into measurement-fit is shrunk
576    /// to its measured width when no explicit justify is set.
577    fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
578        self.render_segments_with(renderable, &self.options())
579    }
580
581    /// Render a renderable to segments with explicit render options, exactly
582    /// as [`print_with`](Self::print_with) does before writing: a printed
583    /// `Text` goes through upstream's `Text.join`, a renderable that opts in
584    /// is fitted to its measurement, and the result is cropped to the console
585    /// width (`print(crop=True)`). No trailing newline is added.
586    ///
587    /// For upstream's lower-level `Console.render`, see [`render`](Self::render).
588    pub fn render_segments_with(
589        &self,
590        renderable: &dyn Renderable,
591        options: &ConsoleOptions,
592    ) -> Vec<Segment> {
593        let mut options = options.clone();
594        let joined;
595        let joined_text = renderable.printed_text();
596        let renderable = match joined_text.clone() {
597            Some(text) => {
598                // `print(justify="left"|"center"|"right")` wraps the joined
599                // text in `Align`, which renders a zero-width text (such as an
600                // empty one) at no width at all: nothing is printed.
601                if matches!(
602                    options.justify,
603                    Justify::Left | Justify::Center | Justify::Right
604                ) && text.measurement().1 == 0
605                {
606                    return Vec::new();
607                }
608                joined = text;
609                &joined as &dyn Renderable
610            }
611            None => renderable,
612        };
613        // `print(justify="left"|"center"|"right")` wraps every other
614        // renderable in `Align(renderable, justify)` (`_collect_renderables`).
615        let align = match options.justify {
616            Justify::Left if joined_text.is_none() => Some(crate::align::HorizontalAlign::Left),
617            Justify::Center if joined_text.is_none() => Some(crate::align::HorizontalAlign::Center),
618            Justify::Right if joined_text.is_none() => Some(crate::align::HorizontalAlign::Right),
619            _ => None,
620        };
621        if options.justify == Justify::Default && renderable.fit_to_measurement() {
622            let measurement = renderable.measure(self, &options);
623            options.max_width = measurement.maximum.min(options.max_width).max(1);
624        }
625        let segments = match align {
626            // `Console.render` yields nothing when there is no width.
627            _ if options.max_width < 1 => Vec::new(),
628            Some(align) => crate::align::Align::render_child(renderable, align, self, &options),
629            None => renderable.rich_render(self, &options),
630        };
631        // `Console.print(crop=True)`: the final backstop against a line running
632        // off the side of the terminal. Renderables that fit are untouched; this
633        // is what gives `Overflow::Ignore` its "wrap nothing, but still don't
634        // corrupt the display" behaviour.
635        Segment::crop_lines(&segments, self.width)
636    }
637
638    /// Render a renderable to segments. Port of `Console.render`: `options`
639    /// defaults to [`options`](Self::options), and nothing is rendered when
640    /// there is no width (`max_width < 1`).
641    ///
642    /// Unlike the print path ([`render_segments_with`](Self::render_segments_with))
643    /// the renderable is rendered as-is: no `Text.join`, no fitting and no
644    /// crop. Container renderables use this for their children.
645    ///
646    /// As everywhere in this port, lines are *separated* by newline segments:
647    /// the final line has no trailing `"\n"` where upstream's has one.
648    pub fn render(
649        &self,
650        renderable: &dyn Renderable,
651        options: Option<&ConsoleOptions>,
652    ) -> Vec<Segment> {
653        let default_options;
654        let options = match options {
655            Some(options) => options,
656            None => {
657                default_options = self.options();
658                &default_options
659            }
660        };
661        if options.max_width < 1 {
662            return Vec::new();
663        }
664        renderable.rich_render(self, options)
665    }
666
667    /// Write (or, while capturing, record) a rendered segment stream, adding a
668    /// trailing newline. The single sink for every `print*` path.
669    fn emit(&self, segments: Vec<Segment>) {
670        self.emit_end(segments, true);
671    }
672
673    /// [`emit`](Self::emit), with the trailing newline optional: upstream's
674    /// `print(…, end="")` when `newline` is false. Output written straight to
675    /// stdout is flushed in that case, so a prompt shows before input is read.
676    fn emit_end(&self, segments: Vec<Segment>, newline: bool) {
677        if segments.is_empty() {
678            return;
679        }
680        let segments = {
681            let mut captures = self.lock_captures();
682            if let Some(buffer) = current_capture(&mut captures) {
683                buffer.extend(segments);
684                if newline {
685                    buffer.push(Segment::line());
686                }
687                return;
688            }
689            segments
690        };
691        let mut output = self.segments_to_string(&segments);
692        if newline {
693            output.push('\n');
694        }
695        let stdout = std::io::stdout();
696        let mut lock = stdout.lock();
697        let _ = write!(lock, "{output}");
698        if !newline {
699            let _ = lock.flush();
700        }
701    }
702
703    /// Display `prompt` and read a line of input from standard input. Port of
704    /// `Console.input`: the prompt is console markup, printed through the
705    /// console with `end=""` so it is captured and exported like any other
706    /// output. `None` means end of input.
707    pub fn input(&self, prompt: &str) -> std::io::Result<Option<String>> {
708        let prompt = self.build_text(prompt);
709        self.input_from(&prompt, &mut crate::prompt::StdinInput)
710    }
711
712    /// [`input`](Self::input) with a renderable prompt (upstream accepts a
713    /// `Text`) and an explicit input source — upstream's `stream=` argument.
714    pub fn input_from(
715        &self,
716        prompt: &dyn Renderable,
717        stream: &mut dyn crate::prompt::InputSource,
718    ) -> std::io::Result<Option<String>> {
719        // `if prompt: self.print(prompt, end="")`.
720        let segments = self.render_segments(prompt);
721        if segments.iter().any(|segment| !segment.text.is_empty()) {
722            self.emit_end(segments, false);
723        }
724        stream.read_line()
725    }
726
727    /// Render a value into a list of lines, each a list of [`Segment`]s.
728    ///
729    /// Port of `Console.render_lines`. When `pad` is true, every line is padded
730    /// (or cropped) to `options.max_width` — this is what container renderables
731    /// such as `Panel`/`Padding` rely on to get uniform-width child rows.
732    pub fn render_lines(
733        &self,
734        renderable: &dyn Renderable,
735        options: &ConsoleOptions,
736        pad: bool,
737    ) -> Vec<Vec<Segment>> {
738        self.render_lines_styled(renderable, options, None, pad)
739    }
740
741    /// [`render_lines`](Self::render_lines) with upstream's `style=` argument:
742    /// the style is applied under every rendered segment and to the padding
743    /// that fills each line, as `Panel` and `Padding` use it.
744    pub fn render_lines_styled(
745        &self,
746        renderable: &dyn Renderable,
747        options: &ConsoleOptions,
748        style: Option<&Style>,
749        pad: bool,
750    ) -> Vec<Vec<Segment>> {
751        // Upstream pads with the `style` argument as given (default `None`),
752        // so the pad segments carry no style unless one was passed.
753        let pad_style = style.cloned();
754        let style = style.filter(|style| !style.is_null());
755        // Upstream `Console.render` yields nothing when `max_width < 1`, so a
756        // renderable squeezed to zero width contributes no lines (#449).
757        let mut segments = if options.max_width < 1 {
758            Vec::new()
759        } else {
760            renderable.rich_render(self, options)
761        };
762        if let Some(style) = style {
763            segments = Segment::apply_style(&segments, style);
764        }
765        let mut lines = Segment::split_lines(&segments);
766        // An empty `Text` renders as a lone empty segment: upstream renders it
767        // as its `end` newline, which `split_and_crop_lines` turns into one
768        // blank line (#442).
769        if lines.is_empty()
770            && !segments.is_empty()
771            && segments
772                .iter()
773                .all(|segment| !segment.control && segment.text.is_empty())
774        {
775            lines.push(Vec::new());
776        }
777        if pad {
778            for line in &mut lines {
779                *line = Segment::adjust_line_length(line, options.max_width, pad_style.clone());
780            }
781        }
782        // Honor an explicit height by cropping/padding to exactly that many rows
783        // (matching `Console.render_lines`'s height handling — used by height-
784        // aware containers such as `Panel` inside a `Layout`).
785        if let Some(height) = options.height {
786            lines.truncate(height);
787            while lines.len() < height {
788                lines.push(if pad {
789                    vec![Segment::new(
790                        " ".repeat(options.max_width),
791                        pad_style.clone(),
792                    )]
793                } else {
794                    Vec::new()
795                });
796            }
797        }
798        lines
799    }
800
801    /// Render a value exactly as [`print`](Console::print) would write it,
802    /// returning the string (including the single trailing newline). For tests
803    /// and export.
804    pub fn render_export(&self, renderable: &dyn Renderable) -> String {
805        let segments = self.render_segments(renderable);
806        let mut out = self.segments_to_string(&segments);
807        if !segments.is_empty() {
808            out.push('\n');
809        }
810        out
811    }
812
813    /// Render a value and write it to stdout, followed by a newline.
814    pub fn print(&self, renderable: &dyn Renderable) {
815        let segments = self.render_segments(renderable);
816        self.emit(segments);
817    }
818
819    /// Print with explicit render options, the equivalent of upstream's
820    /// `Console.print(renderable, justify=…, overflow=…, no_wrap=…)`. Start
821    /// from [`options`](Self::options) and set the fields to override. A
822    /// printed `Text` defers to these options, because upstream's `Text.join`
823    /// drops the text's own `justify`, `overflow` and `no_wrap`.
824    pub fn print_with(&self, renderable: &dyn Renderable, options: &ConsoleOptions) {
825        let segments = self.render_segments_with(renderable, options);
826        self.emit(segments);
827    }
828
829    /// Like [`render_export`](Self::render_export), with explicit render
830    /// options as for [`print_with`](Self::print_with).
831    pub fn render_export_with(
832        &self,
833        renderable: &dyn Renderable,
834        options: &ConsoleOptions,
835    ) -> String {
836        let segments = self.render_segments_with(renderable, options);
837        let mut out = self.segments_to_string(&segments);
838        if !segments.is_empty() {
839            out.push('\n');
840        }
841        out
842    }
843
844    /// Write a terminal control sequence to stdout.
845    ///
846    /// Port of `Console.control`. Control codes are only written when output is
847    /// a real terminal (they are meaningless when redirected to a file).
848    pub fn control(&self, control: &crate::control::Control) {
849        if !self.is_terminal {
850            return;
851        }
852        let text = control.as_str();
853        // Upstream buffers control codes with everything else, so a capture
854        // records them.
855        if let Some(buffer) = current_capture(&mut self.lock_captures()) {
856            if !text.is_empty() {
857                buffer.push(Segment::control(text));
858            }
859            return;
860        }
861        if !text.is_empty() {
862            let stdout = std::io::stdout();
863            let mut lock = stdout.lock();
864            let _ = write!(lock, "{text}");
865        }
866    }
867
868    /// Whether the alternate screen is enabled. Port of
869    /// `Console.is_alt_screen`.
870    pub fn is_alt_screen(&self) -> bool {
871        self.is_alt_screen.load(std::sync::atomic::Ordering::SeqCst)
872    }
873
874    /// Enable or disable the alternate screen. Port of
875    /// `Console.set_alt_screen`: only a terminal (not a legacy Windows
876    /// console) switches, and the result says whether it did.
877    pub fn set_alt_screen(&self, enable: bool) -> bool {
878        if self.is_terminal && !self.legacy_windows {
879            self.control(&crate::control::Control::alt_screen(enable));
880            self.is_alt_screen
881                .store(enable, std::sync::atomic::Ordering::SeqCst);
882            return true;
883        }
884        false
885    }
886
887    /// Render `renderable` into `region` of the alternate screen (the whole
888    /// screen when `None`). Port of `Console.update_screen`.
889    pub fn update_screen(
890        &self,
891        renderable: &dyn Renderable,
892        region: Option<crate::region::Region>,
893        options: Option<&ConsoleOptions>,
894    ) -> crate::errors::Result<()> {
895        if !self.is_alt_screen() {
896            return Err(no_alt_screen());
897        }
898        let render_options = options.cloned().unwrap_or_else(|| self.options());
899        let (x, y, render_options) = match region {
900            None => {
901                let height = render_options
902                    .height
903                    .filter(|&height| height > 0)
904                    .unwrap_or(self.height);
905                let width = render_options.max_width;
906                (0, 0, render_options.update_dimensions(width, height))
907            }
908            Some(region) => (
909                region.x,
910                region.y,
911                render_options.update_dimensions(region.width, region.height),
912            ),
913        };
914        let lines = self.render_lines(renderable, &render_options, true);
915        self.update_screen_lines(&lines, x, y)
916    }
917
918    /// Write rendered `lines` to the alternate screen at column `x`, row `y`.
919    /// Port of `Console.update_screen_lines` (through [`ScreenUpdate`]).
920    pub fn update_screen_lines(
921        &self,
922        lines: &[Vec<Segment>],
923        x: usize,
924        y: usize,
925    ) -> crate::errors::Result<()> {
926        if !self.is_alt_screen() {
927            return Err(no_alt_screen());
928        }
929        let update = ScreenUpdate::new(lines.to_vec(), x, y);
930        let segments = self.render(&update, None);
931        self.emit_end(segments, false);
932        Ok(())
933    }
934
935    /// Show or hide the cursor. Port of `Console.show_cursor`.
936    pub fn show_cursor(&self, show: bool) {
937        self.control(&crate::control::Control::show_cursor(show));
938    }
939
940    /// Clear the screen. Port of `Console.clear`.
941    pub fn clear(&self) {
942        self.control(&crate::control::Control::clear());
943    }
944
945    /// Ring the terminal bell. Port of `Console.bell`.
946    pub fn bell(&self) {
947        self.control(&crate::control::Control::bell());
948    }
949
950    /// Capture everything printed inside `f` instead of writing it to stdout,
951    /// returning it as a rendered (ANSI) string.
952    ///
953    /// The Rust analogue of upstream's `with console.capture() as capture:` —
954    /// the closure receives the same console, and captures nest correctly.
955    /// Equivalent to what would have been written to the terminal.
956    pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
957        let segments = self.record(f);
958        self.segments_to_string(&segments)
959    }
960
961    /// Like [`capture`](Self::capture) but with all styles stripped, returning
962    /// plain text. Port of `Console.export_text(styles=False)`.
963    pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
964        let segments = self.record(f);
965        segments_to_plain(&segments)
966    }
967
968    /// Buffer everything printed inside `f` and display it through the system
969    /// pager. The Rust analogue of upstream's `with console.pager():` block.
970    ///
971    /// Styles are stripped unless `styles` is set, matching
972    /// `Console.pager(styles=False)`. When there's no terminal to page in (piped
973    /// output, `TERM=dumb`) or no pager can be started, the content is written
974    /// straight to stdout.
975    pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
976        self.page_with(&crate::pager::SystemPager, styles, f)
977    }
978
979    /// Like [`page`](Self::page) but with an explicit [`Pager`](crate::pager::Pager)
980    /// — the seam upstream exposes as `Console.pager(pager=…)`.
981    pub fn page_with(
982        &self,
983        pager: &dyn crate::pager::Pager,
984        styles: bool,
985        f: impl FnOnce(&Console),
986    ) -> std::io::Result<()> {
987        let segments = self.record(f);
988        let content = if styles {
989            self.segments_to_string(&segments)
990        } else {
991            segments_to_plain(&segments)
992        };
993        pager.show(&content)
994    }
995
996    /// Capture output printed inside `f` and export it as a self-contained HTML
997    /// document (inline styles), using the default terminal theme. Port of
998    /// `Console.export_html(inline_styles=True)`.
999    pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
1000        self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1001    }
1002
1003    /// Like [`export_html`](Self::export_html) but with an explicit palette —
1004    /// upstream's `export_html(theme=…)`. See [`terminal_theme`] for the
1005    /// bundled presets.
1006    ///
1007    /// [`terminal_theme`]: crate::terminal_theme
1008    pub fn export_html_themed(
1009        &self,
1010        theme: &crate::terminal_theme::TerminalTheme,
1011        f: impl FnOnce(&Console),
1012    ) -> String {
1013        let segments = self.record(f);
1014        crate::export::export_html_inline(&segments, theme)
1015    }
1016
1017    /// Like [`export_html`](Self::export_html) but with a generated CSS-class
1018    /// stylesheet (`.r1 {…}`) instead of inline styles. Port of upstream's
1019    /// default `Console.export_html(inline_styles=False)`.
1020    pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
1021        self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1022    }
1023
1024    /// Like [`export_html_classes`](Self::export_html_classes) but with an
1025    /// explicit palette — upstream's `export_html(theme=…, inline_styles=False)`.
1026    pub fn export_html_classes_themed(
1027        &self,
1028        theme: &crate::terminal_theme::TerminalTheme,
1029        f: impl FnOnce(&Console),
1030    ) -> String {
1031        let segments = self.record(f);
1032        crate::export::export_html_classes(&segments, theme)
1033    }
1034
1035    /// Capture output printed inside `f` and export it as a self-contained SVG
1036    /// image of a terminal window, using [`SVG_EXPORT_THEME`]. Port of
1037    /// `Console.export_svg`.
1038    ///
1039    /// `unique_id` prefixes every generated id/class. Upstream's auto-computed
1040    /// default hashes Python `repr()` output (not reproducible in Rust), so this
1041    /// port takes an explicit id; output is byte-parity with
1042    /// `export_svg(title=…, unique_id=…)` (see docs/DIVERGENCES.md #15).
1043    ///
1044    /// [`SVG_EXPORT_THEME`]: crate::terminal_theme::SVG_EXPORT_THEME
1045    pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
1046        self.export_svg_themed(
1047            &crate::terminal_theme::SVG_EXPORT_THEME,
1048            title,
1049            unique_id,
1050            f,
1051        )
1052    }
1053
1054    /// Like [`export_svg`](Self::export_svg) but with an explicit palette —
1055    /// upstream's `export_svg(theme=…)`.
1056    pub fn export_svg_themed(
1057        &self,
1058        theme: &crate::terminal_theme::TerminalTheme,
1059        title: &str,
1060        unique_id: &str,
1061        f: impl FnOnce(&Console),
1062    ) -> String {
1063        let segments = self.record(f);
1064        crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
1065    }
1066
1067    /// Capture output printed inside `f` and export it as HTML with every
1068    /// option of upstream's `Console.export_html`: `code_format` (default
1069    /// [`CONSOLE_HTML_FORMAT`](crate::export::CONSOLE_HTML_FORMAT)) and
1070    /// `inline_styles`. See [`export::export_html_with`](crate::export::export_html_with).
1071    pub fn export_html_with(
1072        &self,
1073        theme: &crate::terminal_theme::TerminalTheme,
1074        code_format: Option<&str>,
1075        inline_styles: bool,
1076        f: impl FnOnce(&Console),
1077    ) -> Result<String, crate::export::ExportFormatError> {
1078        let segments = self.record(f);
1079        crate::export::export_html_with(&segments, theme, code_format, inline_styles)
1080    }
1081
1082    /// Capture output printed inside `f` and export it as SVG with every
1083    /// option of upstream's `Console.export_svg`: `code_format` (default
1084    /// [`CONSOLE_SVG_FORMAT`](crate::svg::CONSOLE_SVG_FORMAT)) and
1085    /// `font_aspect_ratio` (upstream default 0.61). See
1086    /// [`svg::export_svg_with`](crate::svg::export_svg_with).
1087    #[allow(clippy::too_many_arguments)]
1088    pub fn export_svg_with(
1089        &self,
1090        theme: &crate::terminal_theme::TerminalTheme,
1091        title: &str,
1092        unique_id: &str,
1093        code_format: Option<&str>,
1094        font_aspect_ratio: f64,
1095        f: impl FnOnce(&Console),
1096    ) -> Result<String, crate::export::ExportFormatError> {
1097        let segments = self.record(f);
1098        crate::svg::export_svg_with(
1099            &segments,
1100            theme,
1101            title,
1102            unique_id,
1103            self.width(),
1104            code_format.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1105            font_aspect_ratio,
1106        )
1107    }
1108
1109    /// Record everything `f` prints and hand back the raw segments, without
1110    /// writing to the terminal.
1111    ///
1112    /// This is the seam for producing *several* outputs from one render — the
1113    /// terminal bytes and an HTML and an SVG file, say — which is what
1114    /// `rich --export-html … --export-svg …` needs. Upstream reaches the same
1115    /// place with `Console(record=True)` plus `save_html(clear=False)`; here the
1116    /// buffer is returned instead of being held on the console, so the caller
1117    /// decides what to do with it and there is no hidden state to clear.
1118    ///
1119    /// Pair with [`segments_to_string`](Self::segments_to_string) to get the
1120    /// terminal form, [`export::export_html_classes`](crate::export::export_html_classes)
1121    /// for HTML, and [`svg::export_svg`](crate::svg::export_svg) for SVG.
1122    ///
1123    /// Rendering twice instead would be wrong, not merely wasteful: a renderable
1124    /// reading standard input only yields its content once.
1125    pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1126        self.record(f)
1127    }
1128
1129    /// Run `f` with this thread's output recorded to a fresh buffer, returning
1130    /// the captured segments. Captures nest (each pushes its own buffer), are
1131    /// per thread (upstream's `ConsoleThreadLocals`), and end even if `f`
1132    /// panics (upstream's `Capture.__exit__` always runs).
1133    fn record(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1134        /// Pops this thread's capture buffer on drop, so an unwinding `f`
1135        /// does not leave the console capturing.
1136        struct CaptureGuard<'a>(&'a Console);
1137        impl CaptureGuard<'_> {
1138            fn pop(&self) -> Vec<Segment> {
1139                let id = std::thread::current().id();
1140                let mut captures = self.0.lock_captures();
1141                let Some(stack) = captures.get_mut(&id) else {
1142                    return Vec::new();
1143                };
1144                let captured = stack.pop().unwrap_or_default();
1145                if stack.is_empty() {
1146                    captures.remove(&id);
1147                }
1148                captured
1149            }
1150        }
1151        impl Drop for CaptureGuard<'_> {
1152            fn drop(&mut self) {
1153                self.pop();
1154            }
1155        }
1156        self.lock_captures()
1157            .entry(std::thread::current().id())
1158            .or_default()
1159            .push(Vec::new());
1160        let guard = CaptureGuard(self);
1161        f(self);
1162        let captured = guard.pop();
1163        std::mem::forget(guard);
1164        captured
1165    }
1166
1167    /// The per-thread capture stacks, recovering them if a panicking print
1168    /// poisoned the lock.
1169    fn lock_captures(&self) -> std::sync::MutexGuard<'_, CaptureStacks> {
1170        self.captures
1171            .lock()
1172            .unwrap_or_else(std::sync::PoisonError::into_inner)
1173    }
1174
1175    /// Parse `content` as console markup, apply registered highlighters, and
1176    /// print it. This is the `console.print("...")` path.
1177    pub fn print_str(&self, content: &str) {
1178        let text = self.build_text(content);
1179        self.print(&text);
1180    }
1181
1182    /// Same as [`Console::print_str`] but returns the ANSI string.
1183    pub fn render_str_to_string(&self, content: &str) -> String {
1184        let text = self.build_text(content);
1185        self.render_to_string(&text)
1186    }
1187
1188    /// Parse `content` as console markup (expanding emoji + applying the active
1189    /// highlighters), returning the styled [`Text`] that `print_str` would print.
1190    /// Exposed so callers can wrap the markup in another renderable.
1191    pub fn build_text(&self, content: &str) -> Text {
1192        // Malformed markup falls back to printing the text as-is. Upstream would
1193        // raise `MarkupError` instead; use `try_build_text` (or `try_print_str`)
1194        // when the markup comes from a user and a mistake should be reported
1195        // rather than rendered. See docs/DIVERGENCES.md §2.
1196        self.try_build_text(content)
1197            .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
1198    }
1199
1200    /// As [`build_text`](Console::build_text), but returns
1201    /// [`RichError::Markup`](crate::errors::RichError::Markup) for malformed
1202    /// markup instead of falling back to the raw text — upstream's behaviour.
1203    pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
1204        if !self.markup {
1205            // `Console(markup=False)`: the string is taken literally.
1206            return Ok(self.decorate(Text::new(self.expand_emoji_plain(content))));
1207        }
1208        let markup = self.parse_markup(content, self.emoji)?;
1209
1210        // The highlighter runs on the *markup-stripped* text and its spans go on
1211        // first; the markup spans are appended afterwards. Spans combine in
1212        // order, so this is what makes an explicit tag beat the highlighter —
1213        // `[green]123[/]` is green, not `repr.number` cyan.
1214        //
1215        // Upstream reaches the same result a different way: `Console.render_str`
1216        // highlights a fresh `Text(str(rich_text))` and then calls
1217        // `highlight_text.copy_styles(rich_text)`, whose `_spans.extend` appends
1218        // the markup spans last. Decorating the markup `Text` in place — the
1219        // obvious reading — inverts the precedence.
1220        let mut text = self.decorate(Text::new(markup.plain()));
1221        for span in markup.spans() {
1222            text.push_span(span.clone());
1223        }
1224        Ok(text)
1225    }
1226
1227    /// Convert a plain string to [`Text`] the way a `str` renderable is
1228    /// converted upstream: emoji codes expand (per the console), console
1229    /// markup is parsed, and highlighting runs when `highlight` (or, when
1230    /// `None`, the console default) enables it. Port of `Console.render_str`,
1231    /// which `Table` cells, `Tree` labels and `Columns` items go through.
1232    ///
1233    /// Malformed markup falls back to the literal text, as
1234    /// [`build_text`](Console::build_text) does (docs/DIVERGENCES.md §2).
1235    pub fn render_str(&self, content: &str, highlight: Option<bool>) -> Text {
1236        let highlight = highlight.unwrap_or(self.highlight);
1237        // `markup.render` returns the (emoji-replaced) string untouched when it
1238        // holds no `[`; skip the parser for the common plain cell.
1239        let markup = if !self.markup {
1240            Text::new(self.expand_emoji_plain(content))
1241        } else if content.contains('[') {
1242            self.parse_markup(content, self.emoji)
1243                .unwrap_or_else(|_| Text::new(self.expand_emoji(content)))
1244        } else if content.contains(':') {
1245            Text::new(self.expand_emoji(content))
1246        } else {
1247            Text::new(content)
1248        };
1249        if !highlight {
1250            return markup;
1251        }
1252        // Highlight the plain text, then append the markup spans, as
1253        // `highlight_text.copy_styles(rich_text)` does (see `try_build_text`).
1254        let mut text = self.decorate_with_repr(Text::new(markup.plain()));
1255        for span in markup.spans() {
1256            text.push_span(span.clone());
1257        }
1258        text
1259    }
1260
1261    /// As [`print_str`](Console::print_str), but reports malformed markup.
1262    pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
1263        self.print(&self.try_build_text(content)?);
1264        Ok(())
1265    }
1266
1267    /// As [`print_justified`](Console::print_justified), but reports malformed
1268    /// markup.
1269    pub fn try_print_justified(
1270        &self,
1271        content: &str,
1272        justify: Justify,
1273    ) -> crate::errors::Result<()> {
1274        let text = self.try_build_text(content)?;
1275        let mut options = self.options();
1276        options.justify = justify;
1277        self.emit(text.rich_render(self, &options));
1278        Ok(())
1279    }
1280
1281    /// Expand `:emoji:` shortcodes. Runs before markup parsing (matching
1282    /// upstream's default `emoji=True`); `:name:` and `[tag]` don't overlap.
1283    ///
1284    /// The console's default emoji variant applies only when `content` holds
1285    /// no `[`: upstream's `markup.render` passes `default_variant` on its
1286    /// tag-free fast path only, and replaces the text between tags without it.
1287    pub(crate) fn expand_emoji(&self, content: &str) -> String {
1288        if !self.emoji {
1289            return content.to_string();
1290        }
1291        let variant = if content.contains('[') {
1292            None
1293        } else {
1294            self.emoji_variant
1295        };
1296        crate::emoji::replace_with_variant(content, variant)
1297    }
1298
1299    /// Port of `markup.render(content, emoji=emoji, emoji_variant=…)`: a
1300    /// string with no `[` is only emoji-replaced (with the console's default
1301    /// variant); otherwise emoji codes are replaced chunk by chunk between the
1302    /// tags, so error positions refer to the original string.
1303    fn parse_markup(&self, content: &str, emoji: bool) -> crate::errors::Result<Text> {
1304        if !content.contains('[') {
1305            return Ok(Text::new(if emoji {
1306                crate::emoji::replace_with_variant(content, self.emoji_variant)
1307            } else {
1308                content.to_string()
1309            }));
1310        }
1311        if emoji {
1312            crate::markup::render_emoji(content)
1313        } else {
1314            crate::markup::render(content)
1315        }
1316    }
1317
1318    /// Expand `:emoji:` shortcodes in a string that is not markup, with the
1319    /// console's default variant (upstream's `markup=False` branch of
1320    /// `render_str`).
1321    fn expand_emoji_plain(&self, content: &str) -> String {
1322        if self.emoji {
1323            crate::emoji::replace_with_variant(content, self.emoji_variant)
1324        } else {
1325            content.to_string()
1326        }
1327    }
1328
1329    /// Convert a string to [`Text`] with every keyword upstream's
1330    /// `Console.render_str` takes. Port of `Console.render_str`, strict:
1331    /// malformed markup is an error (`MarkupError`), not the literal text.
1332    ///
1333    /// Emoji, markup and highlighting default to the console's settings when
1334    /// the option is `None`. As upstream, a highlighted result is a fresh
1335    /// `Text` carrying only spans (the highlighter's, then the markup's): the
1336    /// `style`, `justify` and `overflow` are dropped by `copy_styles`.
1337    pub fn render_str_with(
1338        &self,
1339        content: &str,
1340        options: &RenderStrOptions<'_>,
1341    ) -> crate::errors::Result<Text> {
1342        let emoji = options.emoji.unwrap_or(self.emoji);
1343        let markup = options.markup.unwrap_or(self.markup);
1344        let highlight = options.highlight.unwrap_or(self.highlight);
1345
1346        let mut rich_text = if markup {
1347            self.parse_markup(content, emoji)?
1348        } else if emoji {
1349            Text::new(crate::emoji::replace_with_variant(
1350                content,
1351                self.emoji_variant,
1352            ))
1353        } else {
1354            Text::new(content)
1355        };
1356        rich_text.set_base_style(options.style.clone());
1357        rich_text.set_justify(options.justify.unwrap_or_default());
1358        rich_text.set_overflow(options.overflow);
1359
1360        if !highlight {
1361            return Ok(rich_text);
1362        }
1363        let mut text = Text::new(rich_text.plain());
1364        match options.highlighter {
1365            Some(highlighter) => highlighter.highlight(&mut text),
1366            None => text = self.decorate_with_repr(text),
1367        }
1368        for span in rich_text.spans() {
1369            text.push_span(span.clone());
1370        }
1371        Ok(text)
1372    }
1373
1374    /// Whether `:emoji:` codes are replaced by default. Port of
1375    /// `Console(emoji=…)`.
1376    pub fn emoji(&self) -> bool {
1377        self.emoji
1378    }
1379
1380    /// Whether printed strings are highlighted by default. Port of
1381    /// `Console(highlight=…)`.
1382    pub fn highlight(&self) -> bool {
1383        self.highlight
1384    }
1385
1386    /// Whether printed strings are parsed as console markup by default. Port
1387    /// of `Console(markup=…)`.
1388    pub fn markup(&self) -> bool {
1389        self.markup
1390    }
1391
1392    /// The default emoji variant. Port of `Console(emoji_variant=…)`.
1393    pub fn emoji_variant(&self) -> Option<crate::emoji::EmojiVariant> {
1394        self.emoji_variant
1395    }
1396
1397    /// The tab stop width `Text` expands tabs to. Port of `Console.tab_size`.
1398    pub fn tab_size(&self) -> usize {
1399        self.tab_size
1400    }
1401
1402    /// Change the tab stop width. Upstream's `Console.tab_size` is a plain
1403    /// attribute.
1404    pub fn set_tab_size(&mut self, tab_size: usize) {
1405        self.tab_size = tab_size;
1406    }
1407
1408    /// Set the terminal window title. Port of `Console.set_window_title`:
1409    /// only a terminal is sent the code, and the return value says whether it
1410    /// was.
1411    pub fn set_window_title(&self, title: &str) -> bool {
1412        if self.is_terminal {
1413            self.control(&crate::control::Control::title(title));
1414            true
1415        } else {
1416            false
1417        }
1418    }
1419
1420    /// Apply the registered highlighters, plus the built-in `ReprHighlighter`
1421    /// when `highlight` is on.
1422    fn decorate(&self, mut text: Text) -> Text {
1423        for highlighter in &self.highlighters {
1424            highlighter.highlight(&mut text);
1425        }
1426        if self.highlight {
1427            crate::highlighter::ReprHighlighter::new().highlight(&mut text);
1428        }
1429        text
1430    }
1431
1432    /// [`decorate`](Self::decorate) for a caller that has already decided to
1433    /// highlight (upstream's `highlight=True` override of the console default).
1434    fn decorate_with_repr(&self, mut text: Text) -> Text {
1435        for highlighter in &self.highlighters {
1436            highlighter.highlight(&mut text);
1437        }
1438        crate::highlighter::ReprHighlighter::new().highlight(&mut text);
1439        text
1440    }
1441
1442    /// Parse `content` as markup and print it justified to the console width.
1443    /// This is the `console.print("...", justify=...)` path.
1444    pub fn print_justified(&self, content: &str, justify: Justify) {
1445        let text = self.build_text(content);
1446        let mut options = self.options();
1447        options.justify = justify;
1448        let segments = text.rich_render(self, &options);
1449        self.emit(segments);
1450    }
1451
1452    /// Same as [`Console::print_justified`] but returns the ANSI string.
1453    ///
1454    /// The justify is passed via `options.justify`, which — matching upstream —
1455    /// disables the measurement-fit so the text pads to the full width.
1456    pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
1457        let text = self.build_text(content);
1458        let mut options = self.options();
1459        options.justify = justify;
1460        let segments = text.rich_render(self, &options);
1461        self.segments_to_string(&segments)
1462    }
1463
1464    /// Convert rendered segments into a terminal string, applying this console's
1465    /// colour system (and honouring `no_color`). Port of `Console._render_buffer`.
1466    pub fn segments_to_string(&self, segments: &[Segment]) -> String {
1467        let system = self.color_system;
1468        // `if self.no_color and color_system: buffer = Segment.remove_color(…)`:
1469        // colours go, every other attribute stays.
1470        let colorless;
1471        let segments = if self.no_color && system.is_some() {
1472            colorless = Segment::remove_color(segments);
1473            &colorless[..]
1474        } else {
1475            segments
1476        };
1477        let mut out = String::new();
1478        for segment in segments {
1479            // Control codes are meaningless off a terminal — upstream's
1480            // `_render_buffer` drops them when `not is_terminal`.
1481            if segment.control && !self.is_terminal {
1482                continue;
1483            }
1484            match (&segment.style, system) {
1485                (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
1486                _ => out.push_str(&segment.text),
1487            }
1488        }
1489        out
1490    }
1491}
1492
1493/// Join the visible text of a segment stream, dropping control codes. Port of
1494/// `Console.export_text(styles=False)`'s join.
1495fn segments_to_plain(segments: &[Segment]) -> String {
1496    segments
1497        .iter()
1498        .filter(|s| !s.control)
1499        .map(|s| s.text.as_str())
1500        .collect()
1501}
1502
1503/// A string renders as upstream's `Console.render` renders a `str`: through
1504/// [`Console::render_str`] with the options' `highlight` and `markup`
1505/// (`None` taking the console's defaults), then as that `Text`. Malformed
1506/// markup falls back to the literal text, as `render_str` does.
1507impl Renderable for String {
1508    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1509        self.options_text(console, options, options.highlight)
1510            .rich_render(console, options)
1511    }
1512
1513    /// `Measurement.get` of a `str`: `render_str(markup=options.markup,
1514    /// highlight=False)`, then the text's measurement.
1515    fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
1516        let text = self.options_text(console, options, Some(false));
1517        crate::measure::Measurement::get(console, options, &text)
1518    }
1519}
1520
1521/// `render_str` for a `str` renderable under `options`.
1522trait OptionsText {
1523    fn options_text(
1524        &self,
1525        console: &Console,
1526        options: &ConsoleOptions,
1527        highlight: Option<bool>,
1528    ) -> Text;
1529}
1530
1531impl OptionsText for String {
1532    fn options_text(
1533        &self,
1534        console: &Console,
1535        options: &ConsoleOptions,
1536        highlight: Option<bool>,
1537    ) -> Text {
1538        let render_options = RenderStrOptions {
1539            highlight,
1540            markup: options.markup,
1541            ..Default::default()
1542        };
1543        console
1544            .render_str_with(self, &render_options)
1545            .unwrap_or_else(|_| {
1546                console
1547                    .render_str_with(
1548                        self,
1549                        &RenderStrOptions {
1550                            markup: Some(false),
1551                            ..render_options
1552                        },
1553                    )
1554                    .unwrap_or_else(|_| Text::new(self.as_str()))
1555            })
1556    }
1557}
1558
1559impl Renderable for Text {
1560    fn printed_text(&self) -> Option<Text> {
1561        // `Text("").join([self])`: the text and its spans survive; justify,
1562        // overflow and no_wrap come from the blank separator (#446). The base
1563        // style does not: `join` takes the separator's (none) and re-applies
1564        // this text's as a leading span (`if text.style: append_span(...)`),
1565        // so print-level justify padding is left unstyled.
1566        let mut text = self.clone();
1567        text.clear_layout_options();
1568        text.base_style_to_span();
1569        Some(text)
1570    }
1571
1572    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
1573        // Empty Text still represents a printable blank line; an empty
1574        // generator such as Markdown does not. Preserve that distinction.
1575        // Wrap to the available width; the effective justify is this text's own
1576        // justify, falling back to the console options' justify.
1577        let justify = self.get_justify_option().unwrap_or(options.justify);
1578        // A justified empty line is still padded to the width (upstream's
1579        // `truncate(width, pad=True)`), so only an unjustified one is bare.
1580        if self.is_empty() && matches!(justify, Justify::Default | Justify::Full) {
1581            return vec![Segment::new("", None)];
1582        }
1583        // Same precedence for overflow and no_wrap: the text's own setting wins,
1584        // then the options', then upstream's default. Mirrors the `self.x or
1585        // options.x or DEFAULT` chain in `Text.__rich_console__`.
1586        let overflow = self
1587            .get_overflow()
1588            .or(options.overflow)
1589            .unwrap_or(Overflow::Fold);
1590        let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
1591        // `tab_size = console.tab_size if self.tab_size is None else …`.
1592        self.render_joined_wrapped_tabs(
1593            console.theme(),
1594            console.base_style(),
1595            options.max_width,
1596            justify,
1597            overflow,
1598            no_wrap,
1599            self.console_tab_size(console),
1600        )
1601    }
1602
1603    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
1604        let (minimum, maximum) = self.measurement();
1605        crate::measure::Measurement::new(
1606            minimum.min(options.max_width),
1607            maximum.min(options.max_width),
1608        )
1609    }
1610}
1611
1612/// Builder for [`Console`], allowing detection to be overridden.
1613pub struct ConsoleBuilder {
1614    force_terminal: Option<bool>,
1615    color_system: Option<ColorSystem>,
1616    color_system_set: bool,
1617    width: Option<usize>,
1618    height: Option<usize>,
1619    no_color: Option<bool>,
1620    get_time: Option<GetTime>,
1621    emoji: Option<bool>,
1622    highlight: Option<bool>,
1623    legacy_windows: Option<bool>,
1624    safe_box: Option<bool>,
1625    ascii_only: Option<bool>,
1626    theme: Option<Theme>,
1627    markup: Option<bool>,
1628    emoji_variant: Option<crate::emoji::EmojiVariant>,
1629    tab_size: Option<usize>,
1630}
1631
1632impl ConsoleBuilder {
1633    fn new() -> Self {
1634        ConsoleBuilder {
1635            force_terminal: None,
1636            color_system: None,
1637            color_system_set: false,
1638            width: None,
1639            height: None,
1640            no_color: None,
1641            get_time: None,
1642            emoji: None,
1643            highlight: None,
1644            legacy_windows: None,
1645            safe_box: None,
1646            ascii_only: None,
1647            theme: None,
1648            markup: None,
1649            emoji_variant: None,
1650            tab_size: None,
1651        }
1652    }
1653
1654    /// Enable/disable console markup in printed strings (default enabled).
1655    /// Port of `Console(markup=…)`.
1656    pub fn markup(mut self, value: bool) -> Self {
1657        self.markup = Some(value);
1658        self
1659    }
1660
1661    /// The emoji variant appended to codes that name none (default none).
1662    /// Port of `Console(emoji_variant=…)`.
1663    pub fn emoji_variant(mut self, variant: Option<crate::emoji::EmojiVariant>) -> Self {
1664        self.emoji_variant = variant;
1665        self
1666    }
1667
1668    /// The tab stop width `Text` expands tabs to (default 8). Port of
1669    /// `Console(tab_size=…)`.
1670    pub fn tab_size(mut self, tab_size: usize) -> Self {
1671        self.tab_size = Some(tab_size);
1672        self
1673    }
1674
1675    pub fn force_terminal(mut self, value: bool) -> Self {
1676        self.force_terminal = Some(value);
1677        self
1678    }
1679
1680    /// Force legacy-Windows-console behavior (box substitution). Default off.
1681    pub fn legacy_windows(mut self, value: bool) -> Self {
1682        self.legacy_windows = Some(value);
1683        self
1684    }
1685
1686    /// Enable/disable terminal-safe box substitution (default on).
1687    pub fn safe_box(mut self, value: bool) -> Self {
1688        self.safe_box = Some(value);
1689        self
1690    }
1691
1692    /// Force ASCII-only box rendering (default off). Set for non-UTF-8 terminals.
1693    pub fn ascii_only(mut self, value: bool) -> Self {
1694        self.ascii_only = Some(value);
1695        self
1696    }
1697
1698    /// Force a specific color system (use for reproducible output/tests).
1699    pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
1700        self.color_system = system;
1701        self.color_system_set = true;
1702        self
1703    }
1704
1705    pub fn width(mut self, width: usize) -> Self {
1706        self.width = Some(width);
1707        self
1708    }
1709
1710    /// Set the console height in rows (used by [`Layout`](crate::layout::Layout)).
1711    pub fn height(mut self, height: usize) -> Self {
1712        self.height = Some(height);
1713        self
1714    }
1715
1716    /// Enable no-colour mode: colours are stripped from output while other
1717    /// attributes (bold, underline, …) still render. Unset, a non-empty
1718    /// `NO_COLOR` environment variable enables it. Port of `no_color=`.
1719    pub fn no_color(mut self, value: bool) -> Self {
1720        self.no_color = Some(value);
1721        self
1722    }
1723
1724    /// Read the current time (seconds) from `clock` instead of the monotonic
1725    /// clock. Port of `Console(get_time=…)`; animations such as
1726    /// [`Spinner`](crate::spinner::Spinner) render the frame for this time.
1727    pub fn get_time(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
1728        self.get_time = Some(std::sync::Arc::new(clock));
1729        self
1730    }
1731
1732    /// Enable/disable `:emoji:` shortcode replacement (default enabled).
1733    pub fn emoji(mut self, value: bool) -> Self {
1734        self.emoji = Some(value);
1735        self
1736    }
1737
1738    /// Enable/disable automatic repr highlighting. Defaults to **on**, matching
1739    /// upstream `Console(highlight=True)`.
1740    pub fn highlight(mut self, value: bool) -> Self {
1741        self.highlight = Some(value);
1742        self
1743    }
1744
1745    pub fn theme(mut self, theme: Theme) -> Self {
1746        self.theme = Some(theme);
1747        self
1748    }
1749
1750    pub fn build(self) -> Console {
1751        let is_terminal = self
1752            .force_terminal
1753            .unwrap_or_else(|| std::io::stdout().is_terminal());
1754        // Upstream's rule is `environ.get("NO_COLOR", "") != ""`, so an EMPTY
1755        // NO_COLOR does not disable colour — only a non-empty value does. That
1756        // matters because a shell that exports `NO_COLOR=` (a common way to
1757        // clear it) would otherwise still be treated as opting out.
1758        let no_color = self
1759            .no_color
1760            .unwrap_or_else(|| std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()));
1761        let color_system = if self.color_system_set {
1762            self.color_system
1763        } else if is_terminal && !is_dumb_terminal() {
1764            Some(detect_color_system())
1765        } else {
1766            None
1767        };
1768        let width = self.width.unwrap_or_else(detect_width);
1769        let height = self.height.unwrap_or_else(detect_height);
1770        Console {
1771            render_environment: None,
1772            code_highlighting: None,
1773            color_system,
1774            width,
1775            height,
1776            is_terminal,
1777            no_color,
1778            get_time: self
1779                .get_time
1780                .unwrap_or_else(|| std::sync::Arc::new(monotonic)),
1781            emoji: self.emoji.unwrap_or(true),
1782            // Upstream's `Console(highlight=True)` default. Getting this wrong is
1783            // invisible in the fixtures (every one is captured with
1784            // highlight=False) but is the first thing a user sees: numbers,
1785            // paths, booleans and URLs come out plain instead of coloured.
1786            highlight: self.highlight.unwrap_or(true),
1787            legacy_windows: self.legacy_windows.unwrap_or(false),
1788            safe_box: self.safe_box.unwrap_or(true),
1789            ascii_only: self.ascii_only.unwrap_or(false),
1790            theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
1791            base_style: Style::new(),
1792            highlighters: Vec::new(),
1793            markup: self.markup.unwrap_or(true),
1794            emoji_variant: self.emoji_variant,
1795            // `tab_size: int = 8`.
1796            tab_size: self.tab_size.unwrap_or(crate::text::DEFAULT_TAB_SIZE),
1797            captures: std::sync::Mutex::new(CaptureStacks::new()),
1798            is_alt_screen: std::sync::atomic::AtomicBool::new(false),
1799        }
1800    }
1801}
1802
1803/// Whether `TERM` names a terminal that cannot render styles. Port of
1804/// `Console.is_dumb_terminal` (the caller supplies the `is_terminal` half):
1805/// `_detect_color_system` returns no colour system for one, so its output is
1806/// plain even though it is a terminal.
1807fn is_dumb_terminal() -> bool {
1808    std::env::var("TERM")
1809        .map(|term| matches!(term.to_lowercase().as_str(), "dumb" | "unknown"))
1810        .unwrap_or(false)
1811}
1812
1813/// Detect the terminal color system.
1814///
1815/// `COLORTERM`/`TERM` are the portable signals, but **Windows sets neither**.
1816/// Detecting from them alone meant every Windows console fell back to
1817/// [`ColorSystem::Standard`] — 16 colors — for all output. Measured on a real
1818/// Windows Terminal session: 28 distinct colors in a rendered heat map against
1819/// 140 once truecolor was detected.
1820///
1821/// Upstream `rich` special-cases Windows for the same reason. It reaches the
1822/// platform APIs directly; we ask `anstyle-query`, which avoids hand-written
1823/// `unsafe` FFI for a console handle (see `docs/DIVERGENCES.md`).
1824fn detect_color_system() -> ColorSystem {
1825    if let Some(colorterm) = std::env::var_os("COLORTERM") {
1826        let colorterm = colorterm.to_string_lossy().to_ascii_lowercase();
1827        if colorterm.contains("truecolor") || colorterm.contains("24bit") {
1828            return ColorSystem::Truecolor;
1829        }
1830    }
1831
1832    // Windows. This function is only reached when stdout is a terminal (see
1833    // ConsoleBuilder::build), and every modern Windows console that can be a
1834    // terminal speaks 24-bit color, so report truecolor.
1835    //
1836    // The call below is for its SIDE EFFECT — it turns on
1837    // ENABLE_VIRTUAL_TERMINAL_PROCESSING, which legacy `conhost` needs before
1838    // it honours any escape sequence. Its RETURN VALUE is deliberately ignored:
1839    // it enables VT on stdout *and stderr* and propagates failure with `?`, so
1840    // merely redirecting stderr (`rich ... 2>log`, the most natural CI
1841    // invocation) made it report failure and dropped the whole console to 16
1842    // colors — even though stdout was still a fully capable terminal.
1843    #[cfg(windows)]
1844    {
1845        let _ = anstyle_query::windows::enable_ansi_colors();
1846        ColorSystem::Truecolor
1847    }
1848
1849    // `TERM` is meaningless on Windows and the branch above always returns, so
1850    // gating this keeps either platform free of unreachable code.
1851    #[cfg(not(windows))]
1852    {
1853        if let Some(term) = std::env::var_os("TERM") {
1854            if term.to_string_lossy().contains("256") {
1855                return ColorSystem::EightBit;
1856            }
1857        }
1858        ColorSystem::Standard
1859    }
1860}
1861
1862/// Detect the terminal width: `COLUMNS`, then the real terminal, then a default.
1863fn detect_width() -> usize {
1864    if let Some(columns) = std::env::var_os("COLUMNS") {
1865        if let Ok(value) = columns.to_string_lossy().trim().parse::<usize>() {
1866            if value > 0 {
1867                return value;
1868            }
1869        }
1870    }
1871    if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
1872        if w > 0 {
1873            return w as usize;
1874        }
1875    }
1876    DEFAULT_WIDTH
1877}
1878
1879/// Detect the terminal height: `LINES`, then the real terminal, then a default.
1880fn detect_height() -> usize {
1881    if let Some(lines) = std::env::var_os("LINES") {
1882        if let Ok(value) = lines.to_string_lossy().trim().parse::<usize>() {
1883            if value > 0 {
1884                return value;
1885            }
1886        }
1887    }
1888    if let Some((_, terminal_size::Height(h))) = terminal_size::terminal_size() {
1889        if h > 0 {
1890            return h as usize;
1891        }
1892    }
1893    DEFAULT_HEIGHT
1894}
1895
1896impl crate::protocol::ConsoleCodeHighlighting for Console {
1897    fn set_code_highlighting(&mut self, value: Option<crate::protocol::CodeHighlighting>) {
1898        self.code_highlighting = value;
1899    }
1900    fn code_highlighting(&self) -> Option<&crate::protocol::CodeHighlighting> {
1901        self.code_highlighting.as_ref()
1902    }
1903}
1904
1905impl crate::protocol::ConsoleEnvironment for Console {
1906    fn set_render_environment(
1907        &mut self,
1908        value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
1909    ) {
1910        self.render_environment = value;
1911    }
1912    fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
1913        self.render_environment.as_deref()
1914    }
1915}
1916
1917#[cfg(test)]
1918mod tests {
1919    use super::*;
1920
1921    fn test_console() -> Console {
1922        Console::builder()
1923            .force_terminal(true)
1924            .color_system(Some(ColorSystem::Truecolor))
1925            .width(80)
1926            .no_color(false)
1927            .build()
1928    }
1929
1930    /// The strict path reports malformed markup where the lenient one prints it
1931    /// literally. Both must still agree on markup that is actually valid.
1932    #[test]
1933    fn empty_text_and_empty_renderables_have_distinct_endings() {
1934        let console = Console::builder().force_terminal(false).build();
1935        assert_eq!(console.render_export(&Text::new("")), "\n");
1936        assert_eq!(
1937            console.render_export(&crate::markdown::Markdown::new("")),
1938            ""
1939        );
1940        assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
1941    }
1942
1943    #[test]
1944    fn try_build_text_reports_bad_markup() {
1945        let console = test_console();
1946
1947        let err = console
1948            .try_build_text("[/nope]")
1949            .expect_err("an unmatched closing tag must be an error");
1950        assert!(
1951            matches!(err, crate::errors::RichError::Markup(_)),
1952            "{err:?}"
1953        );
1954        // The lenient path swallows it and prints the source text as-is.
1955        assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
1956
1957        let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
1958        assert_eq!(strict.plain(), "hi");
1959        assert_eq!(
1960            strict.spans().len(),
1961            console.build_text("[bold]hi[/]").spans().len()
1962        );
1963    }
1964
1965    /// An unknown tag *name* is not an error — it renders as a no-op, tag
1966    /// consumed. Only genuine syntax errors fail.
1967    ///
1968    /// Verified against real rich 15.0.0: `Console().print("[nope]x[/]")` writes
1969    /// `x`, while `[bold]a[/italic]` raises `MarkupError`. Before names were
1970    /// carried on spans, the port resolved `nope` eagerly, failed, and fell back
1971    /// to printing the markup source literally.
1972    #[test]
1973    fn unknown_tag_names_render_as_no_ops() {
1974        let console = test_console();
1975        let text = console
1976            .try_build_text("[nope]x[/]")
1977            .expect("an unknown tag name is not a syntax error");
1978        assert_eq!(console.render_to_string(&text), "x");
1979        assert_eq!(
1980            console.render_to_string(&console.build_text("[a.b.c]x[/]")),
1981            "x"
1982        );
1983
1984        // A mismatched closing tag is still an error, on both paths.
1985        assert!(console.try_build_text("[bold]a[/italic]").is_err());
1986        assert!(console.try_build_text("[/nope]").is_err());
1987    }
1988
1989    /// Markup styles bind to the theme of the console that renders the text, not
1990    /// the one that parsed it. Verified against real rich 15.0.0.
1991    #[test]
1992    fn markup_styles_bind_at_render_not_at_parse() {
1993        let themed = |definition: &str| {
1994            let mut theme = Theme::default_theme();
1995            theme.insert("accent", Style::parse(definition).unwrap());
1996            Console::builder()
1997                .force_terminal(true)
1998                .color_system(Some(ColorSystem::Truecolor))
1999                .width(80)
2000                .no_color(false)
2001                .theme(theme)
2002                .build()
2003        };
2004        let red = themed("bold red");
2005        let green = themed("underline green");
2006
2007        // Built once, by the red console...
2008        let text = red.build_text("[accent]hi[/]");
2009        assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
2010        // ...and the green console still renders it in green.
2011        assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
2012    }
2013
2014    /// Emoji expansion and the highlighters have to run on both paths, or the
2015    /// strict variant would quietly render differently from the lenient one.
2016    #[test]
2017    fn try_build_text_expands_emoji_like_build_text() {
2018        let console = test_console();
2019        assert_eq!(
2020            console
2021                .try_build_text(":rocket: go")
2022                .expect("valid")
2023                .plain(),
2024            console.build_text(":rocket: go").plain()
2025        );
2026    }
2027
2028    #[test]
2029    fn renders_markup_string() {
2030        let console = test_console();
2031        assert_eq!(
2032            console.render_str_to_string("[bold red]hi[/]"),
2033            "\x1b[1;31mhi\x1b[0m"
2034        );
2035    }
2036
2037    #[test]
2038    fn print_justify_pads_to_width() {
2039        let console = Console::builder()
2040            .force_terminal(true)
2041            .color_system(Some(ColorSystem::Truecolor))
2042            .width(10)
2043            .build();
2044        // Captured from real rich 15.0.0: console.print("hi", justify=...).
2045        assert_eq!(
2046            console.render_justified_to_string("hi", Justify::Left),
2047            "hi        "
2048        );
2049        assert_eq!(
2050            console.render_justified_to_string("hi", Justify::Center),
2051            "    hi    "
2052        );
2053        assert_eq!(
2054            console.render_justified_to_string("hi", Justify::Right),
2055            "        hi"
2056        );
2057    }
2058
2059    #[test]
2060    fn capture_records_ansi_instead_of_stdout() {
2061        let console = Console::builder()
2062            .force_terminal(true)
2063            .color_system(Some(ColorSystem::Truecolor))
2064            .width(20)
2065            .build();
2066        // Captured from real rich 15.0.0 (Console.capture()).
2067        let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
2068        assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
2069    }
2070
2071    #[test]
2072    fn themed_exports_use_the_given_palette() {
2073        use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
2074
2075        let console = Console::builder()
2076            .force_terminal(true)
2077            .color_system(Some(ColorSystem::Truecolor))
2078            .width(20)
2079            .no_color(false)
2080            .build();
2081        let render = |c: &Console| c.print_str("hi");
2082
2083        // Monokai's background is #0c0c0c and Night Owlish's is #ffffff, so the
2084        // chosen theme has to show up in the emitted CSS.
2085        let monokai = console.export_html_themed(&MONOKAI, render);
2086        assert!(
2087            monokai.contains("#0c0c0c"),
2088            "monokai bg missing:\n{monokai}"
2089        );
2090
2091        let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
2092        assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
2093        assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
2094
2095        // The class form and SVG take a theme too.
2096        let classes = console.export_html_classes_themed(&MONOKAI, render);
2097        assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
2098        let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
2099        assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
2100
2101        // The convenience methods keep their documented defaults.
2102        assert!(console.export_html(render).contains("#ffffff"));
2103    }
2104
2105    #[test]
2106    fn page_with_honors_the_styles_flag() {
2107        use std::sync::Mutex;
2108
2109        #[derive(Default)]
2110        struct Recorder(Mutex<String>);
2111        impl crate::pager::Pager for Recorder {
2112            fn show(&self, content: &str) -> std::io::Result<()> {
2113                *self.0.lock().unwrap() = content.to_string();
2114                Ok(())
2115            }
2116        }
2117
2118        let console = Console::builder()
2119            .force_terminal(true)
2120            .color_system(Some(ColorSystem::Truecolor))
2121            .width(20)
2122            .no_color(false)
2123            .build();
2124
2125        // styles = false (upstream's `Console.pager()` default) strips ANSI.
2126        let plain = Recorder::default();
2127        console
2128            .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
2129            .unwrap();
2130        assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
2131
2132        // styles = true keeps it, matching `Console.pager(styles=True)`.
2133        let styled = Recorder::default();
2134        console
2135            .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
2136            .unwrap();
2137        assert_eq!(
2138            styled.0.lock().unwrap().as_str(),
2139            "\x1b[1;31mhi\x1b[0m there\n"
2140        );
2141    }
2142
2143    #[test]
2144    fn export_text_strips_styles() {
2145        let console = Console::builder()
2146            .force_terminal(true)
2147            .color_system(Some(ColorSystem::Truecolor))
2148            .width(20)
2149            .build();
2150        // Captured from real rich 15.0.0 (Console.export_text(styles=False)).
2151        let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
2152        assert_eq!(out, "hi there\n");
2153    }
2154
2155    #[test]
2156    fn export_html_matches_upstream() {
2157        let console = Console::builder()
2158            .force_terminal(true)
2159            .color_system(Some(ColorSystem::Truecolor))
2160            .width(20)
2161            .no_color(false)
2162            .build();
2163        let html = console.export_html(|c| {
2164            c.print_str("[bold red]hi[/] there");
2165            c.print_str("plain line");
2166        });
2167        // Regenerated from real rich by `scripts/capture_golden.py`, so CI's
2168        // drift check covers exports too. Keep this input in step with the
2169        // matching console in that script.
2170        let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
2171        assert_eq!(html, expected);
2172    }
2173
2174    #[test]
2175    fn export_html_classes_matches_upstream() {
2176        let console = Console::builder()
2177            .force_terminal(true)
2178            .color_system(Some(ColorSystem::Truecolor))
2179            .width(20)
2180            .no_color(false)
2181            .build();
2182        let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
2183        // As above: regenerated by `scripts/capture_golden.py`. Note this test
2184        // prints ONE line where the inline-styles test prints two.
2185        let expected =
2186            include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
2187        assert_eq!(html, expected);
2188    }
2189
2190    /// Upstream's `render_lines` pads with its `style` argument as given
2191    /// (default `None`), so the pad segments carry no style.
2192    #[test]
2193    fn render_lines_pads_with_the_given_style() {
2194        let console = test_console();
2195        let options = console.options().update_width(5).update_height(2);
2196        let lines = console.render_lines(&Text::new("hi"), &options, true);
2197        assert_eq!(lines.len(), 2);
2198        let width_pad = lines[0].last().unwrap();
2199        assert_eq!((width_pad.text.as_str(), &width_pad.style), ("   ", &None));
2200        assert_eq!(lines[1], vec![Segment::new("     ", None)]);
2201
2202        let red = Style::parse("red").unwrap();
2203        let lines = console.render_lines_styled(&Text::new("hi"), &options, Some(&red), true);
2204        assert_eq!(lines[0].last().unwrap().style, Some(red.clone()));
2205        assert_eq!(lines[1], vec![Segment::new("     ", Some(red))]);
2206    }
2207
2208    #[test]
2209    fn capture_matches_direct_render() {
2210        let console = test_console();
2211        let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
2212        assert_eq!(
2213            console.capture(|c| c.print(&panel)),
2214            console.render_export(&panel)
2215        );
2216    }
2217
2218    #[test]
2219    fn no_color_strips_styles() {
2220        let console = Console::builder()
2221            .force_terminal(true)
2222            .color_system(None)
2223            .build();
2224        assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
2225    }
2226
2227    #[test]
2228    fn used_theme_applies_through_the_guard_and_pops_on_drop() {
2229        let mut console = Console::builder()
2230            .force_terminal(true)
2231            .color_system(Some(ColorSystem::Truecolor))
2232            .width(20)
2233            .highlight(false)
2234            .build();
2235        let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
2236        {
2237            let themed = console.use_theme(theme);
2238            let out = themed.capture(|c| c.print_str("[accent]x[/]"));
2239            assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
2240        }
2241        assert!(console.theme().get("accent").is_none());
2242        assert!(console.pop_theme().is_err(), "the base theme must remain");
2243    }
2244
2245    #[test]
2246    fn used_theme_is_popped_during_a_panic_unwind() {
2247        let mut console = Console::builder().width(20).build();
2248        let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
2249        let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2250            let _themed = console.use_theme(theme);
2251            panic!("render failed");
2252        }));
2253        assert!(unwound.is_err());
2254        assert!(console.theme().get("accent").is_none());
2255    }
2256
2257    #[test]
2258    fn a_printed_text_defers_layout_to_the_print_options() {
2259        // Captured from real rich 15.0.0 (#446, #447): `Text.join` drops the
2260        // text's own overflow and justify; print-level options still apply.
2261        let console = Console::builder()
2262            .force_terminal(true)
2263            .color_system(Some(crate::color::ColorSystem::Truecolor))
2264            .width(6)
2265            .highlight(false)
2266            .build();
2267        let text = Text::new("abcdefghij").overflow(Overflow::Ellipsis);
2268        assert_eq!(console.render_export(&text), "abcdef\nghij\n");
2269        let mut options = console.options();
2270        options.overflow = Some(Overflow::Ellipsis);
2271        assert_eq!(
2272            console.render_export_with(&Text::new("abcdefghij"), &options),
2273            "abcde…\n"
2274        );
2275        let wide = Console::builder()
2276            .force_terminal(true)
2277            .color_system(Some(crate::color::ColorSystem::Truecolor))
2278            .width(20)
2279            .highlight(false)
2280            .build();
2281        let tabbed = Text::new("a\tb").justify(Justify::Right);
2282        assert_eq!(wide.render_export(&tabbed), "a       b\n");
2283    }
2284}