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