Skip to main content

rich/
console.rs

1//! The Console — the high-level rendering entry point.
2//!
3//! Port of upstream `rich/console.py`: terminal, colour-system and size
4//! detection, markup and highlighting, printing and logging, capture,
5//! recording and export, render hooks, the alternate screen and paging.
6//! What reads Python objects (file objects, Jupyter, `print_exception`,
7//! `log_locals`) is not ported (docs/DIVERGENCES.md §43).
8
9use std::io::{IsTerminal, Write};
10
11use crate::color::ColorSystem;
12use crate::protocol::{Highlighter, Renderable};
13use crate::segment::Segment;
14use crate::style::Style;
15use crate::text::Text;
16use crate::theme::Theme;
17
18const DEFAULT_WIDTH: usize = 80;
19const DEFAULT_HEIGHT: usize = 25;
20
21/// A source of the current time in seconds. Upstream's `GetTimeCallable`,
22/// shared by [`Console::get_time`] and [`Progress`](crate::progress::Progress).
23pub type GetTime = std::sync::Arc<dyn Fn() -> f64 + Send + Sync>;
24
25/// Seconds on a monotonic clock: upstream's default `time.monotonic`. Every
26/// default clock in the crate reads this one origin, so times taken from a
27/// console and from a progress display are comparable.
28pub(crate) fn monotonic() -> f64 {
29    static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
30    START
31        .get_or_init(std::time::Instant::now)
32        .elapsed()
33        .as_secs_f64()
34}
35
36/// Horizontal justification of a renderable within its width.
37/// Mirrors `rich.console.JustifyMethod`.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Justify {
40    /// Renderable-defined default (usually left, no padding).
41    #[default]
42    Default,
43    Left,
44    Center,
45    Right,
46    Full,
47}
48
49/// What to do with text that is wider than the space available.
50/// Mirrors `rich.console.OverflowMethod`.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
52pub enum Overflow {
53    /// Break over-long words across lines. Upstream's `DEFAULT_OVERFLOW`.
54    #[default]
55    Fold,
56    /// Cut the line off at the width.
57    Crop,
58    /// Cut the line off one cell early and mark it with `…`.
59    Ellipsis,
60    /// Leave over-long lines intact, and do not wrap.
61    Ignore,
62}
63
64/// The options passed to a [`Renderable`] describing the space it must fit into.
65///
66/// Port of the core of `rich.console.ConsoleOptions`. Only the fields needed by
67/// the currently-ported renderables are present; more are added as widgets land.
68#[derive(Debug, Clone)]
69pub struct ConsoleOptions {
70    pub min_width: usize,
71    pub max_width: usize,
72    pub height: Option<usize>,
73    pub justify: Justify,
74    /// Overflow method to impose on renderables, or `None` to let each pick its
75    /// own. Mirrors `ConsoleOptions.overflow`.
76    pub overflow: Option<Overflow>,
77    /// Disable wrapping, or `None` to let each renderable pick. Mirrors
78    /// `ConsoleOptions.no_wrap`.
79    pub no_wrap: Option<bool>,
80    /// Highlight override for strings rendered under these options, or `None`
81    /// for the console default. Mirrors `ConsoleOptions.highlight`: `Panel`,
82    /// `Table` and `Tree` set it for their children, and upstream's
83    /// `Console.render` passes it to `render_str` for a `str` renderable.
84    pub highlight: Option<bool>,
85    /// Markup override for strings rendered under these options, or `None` for
86    /// the console default. Mirrors `ConsoleOptions.markup`.
87    pub markup: Option<bool>,
88    /// Height of the container (starts as the terminal height). Mirrors
89    /// `ConsoleOptions.max_height`.
90    pub max_height: usize,
91    /// Encoding of the terminal (`"utf-8"`, or `"ascii"` for an ASCII-only
92    /// console). Mirrors `ConsoleOptions.encoding`.
93    pub encoding: String,
94    /// Whether the target is a terminal. Mirrors `ConsoleOptions.is_terminal`.
95    pub is_terminal: bool,
96    /// Whether the target is a legacy Windows console. Mirrors
97    /// `ConsoleOptions.legacy_windows`.
98    pub legacy_windows: bool,
99    /// The size of the console. Mirrors `ConsoleOptions.size`.
100    pub size: ConsoleDimensions,
101}
102
103/// The size of a console in cells. Mirrors `rich.console.ConsoleDimensions`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub struct ConsoleDimensions {
106    /// Width in cells.
107    pub width: usize,
108    /// Height in rows.
109    pub height: usize,
110}
111
112impl Default for ConsoleOptions {
113    /// The options of a default 80x25 UTF-8 console, as
114    /// [`Console::options`] builds them.
115    fn default() -> Self {
116        ConsoleOptions {
117            min_width: 1,
118            max_width: DEFAULT_WIDTH,
119            height: None,
120            justify: Justify::Default,
121            overflow: None,
122            no_wrap: None,
123            highlight: None,
124            markup: None,
125            max_height: DEFAULT_HEIGHT,
126            encoding: "utf-8".to_string(),
127            is_terminal: false,
128            legacy_windows: false,
129            size: ConsoleDimensions {
130                width: DEFAULT_WIDTH,
131                height: DEFAULT_HEIGHT,
132            },
133        }
134    }
135}
136
137impl ConsoleOptions {
138    /// Return a copy with `max_width` (and a clamped `min_width`) updated.
139    /// Port of `ConsoleOptions.update_width`.
140    pub fn update_width(&self, width: usize) -> ConsoleOptions {
141        // Copy-then-overwrite rather than a fresh literal, so fields added later
142        // are carried through instead of being silently reset to a default.
143        let mut options = self.clone();
144        options.min_width = width;
145        options.max_width = width;
146        options
147    }
148
149    /// Return a copy with both width and height pinned. Port of
150    /// `ConsoleOptions.update_dimensions`.
151    pub fn update_dimensions(&self, width: usize, height: usize) -> ConsoleOptions {
152        let mut options = self.update_width(width);
153        options.height = Some(height);
154        options.max_height = height;
155        options
156    }
157
158    /// Return a copy with the height (and `max_height`) set. Port of
159    /// `ConsoleOptions.update_height`.
160    pub fn update_height(&self, height: usize) -> ConsoleOptions {
161        let mut options = self.clone();
162        options.height = Some(height);
163        options.max_height = height;
164        options
165    }
166
167    /// Return a copy with `height` cleared. Port of
168    /// `ConsoleOptions.reset_height`.
169    pub fn reset_height(&self) -> ConsoleOptions {
170        let mut options = self.clone();
171        options.height = None;
172        options
173    }
174
175    /// Whether renderables should use ASCII only: the encoding is not a UTF
176    /// one. Port of the `ConsoleOptions.ascii_only` property.
177    pub fn ascii_only(&self) -> bool {
178        !self.encoding.starts_with("utf")
179    }
180}
181
182/// Keyword arguments of upstream's `Console.render_str`, for
183/// [`Console::render_str_with`]. `None` takes the console's default.
184#[derive(Clone, Default)]
185pub struct RenderStrOptions<'a> {
186    /// Base style of the result (`style=`, default none).
187    pub style: crate::style::StyleType,
188    /// `justify=`; `None` leaves the text's justify unset.
189    pub justify: Option<Justify>,
190    /// `overflow=`; `None` leaves the text's overflow unset.
191    pub overflow: Option<Overflow>,
192    /// `emoji=`: replace emoji codes, or `None` for the console default.
193    pub emoji: Option<bool>,
194    /// `markup=`: parse console markup, or `None` for the console default.
195    pub markup: Option<bool>,
196    /// `highlight=`: highlight, or `None` for the console default.
197    pub highlight: Option<bool>,
198    /// `highlighter=`: highlight with this instead of the console's
199    /// highlighters (registered ones plus `ReprHighlighter`).
200    pub highlighter: Option<&'a dyn Highlighter>,
201}
202
203/// An environment to detect the terminal from, in place of the process's:
204/// upstream's `Console(_environ=…)`. See [`ConsoleBuilder::environ`].
205pub type Environ = std::collections::HashMap<String, String>;
206
207/// A replacement for [`Console::on_broken_pipe`]'s default.
208type BrokenPipeHandler = std::sync::Arc<dyn Fn(&Console) + Send + Sync>;
209
210/// Upstream's `get_datetime`: the clock [`Console::log`] reads.
211type GetDateTime = std::sync::Arc<dyn Fn() -> std::time::SystemTime + Send + Sync>;
212
213/// Upstream's `FormatTimeCallable`: formats the time column of
214/// [`Console::log`].
215type FormatTime = std::sync::Arc<dyn Fn(std::time::SystemTime) -> Text + Send + Sync>;
216
217/// Upstream's `log_time_format`: a `strftime` format or a callable.
218#[derive(Clone)]
219enum LogTimeFormat {
220    Strftime(String),
221    Callable(FormatTime),
222}
223
224/// Read `key` from `environ`, or from the process environment without one.
225fn environ_get(environ: Option<&Environ>, key: &str) -> Option<String> {
226    match environ {
227        Some(environ) => environ.get(key).cloned(),
228        None => std::env::var_os(key).map(|value| value.to_string_lossy().into_owned()),
229    }
230}
231
232/// Python's `str.isdigit()` then `int(…)`, for `COLUMNS` and `LINES`: only
233/// a non-empty run of digits counts.
234fn env_int(environ: Option<&Environ>, key: &str) -> Option<usize> {
235    let value = environ_get(environ, key)?;
236    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
237        return None;
238    }
239    value.parse().ok()
240}
241
242/// Hooks in to the render process. Port of `rich.console.RenderHook`: a
243/// hook pushed with [`Console::push_render_hook`] is handed what each
244/// `print` and `log` is about to write, and returns what to write instead
245/// (upstream's live display adds a cursor move before the output and
246/// itself after it).
247///
248/// Upstream hands the hook the renderables themselves, rendered afterwards
249/// at the print's options. Here each one is already rendered at those
250/// options when the hook sees it, so that it can be kept beyond the call:
251/// putting one in another renderable (a panel, say) shows the same lines,
252/// where upstream's would render again at the new width. What the hook adds
253/// is rendered at the print's options, its last line ended unless it says
254/// otherwise ([`Renderable::print_end_line`]).
255pub trait RenderHook: Send + Sync {
256    /// Return the renderables to write in place of `renderables`. Port of
257    /// `RenderHook.process_renderables`.
258    fn process_renderables(
259        &self,
260        renderables: Vec<Box<dyn Renderable>>,
261    ) -> Vec<Box<dyn Renderable>>;
262}
263
264/// One printed renderable, already rendered at the print's options (its last
265/// line ended): what a [`RenderHook`] is handed.
266struct Rendered(Vec<Segment>);
267
268impl Renderable for Rendered {
269    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
270        self.0.clone()
271    }
272
273    fn measure(
274        &self,
275        _console: &Console,
276        _options: &ConsoleOptions,
277    ) -> crate::measure::Measurement {
278        let widest = Segment::split_lines(&self.0)
279            .iter()
280            .map(|line| Segment::get_line_length(line))
281            .max()
282            .unwrap_or(0);
283        crate::measure::Measurement::new(widest, widest)
284    }
285
286    /// The line break is already among the segments.
287    fn print_end_line(&self, _options: &ConsoleOptions) -> bool {
288        false
289    }
290}
291
292/// Per-thread capture buffer stacks (innermost capture last).
293type CaptureStacks = std::collections::HashMap<std::thread::ThreadId, Vec<Vec<Segment>>>;
294
295/// The calling thread's innermost capture buffer, if it is capturing.
296fn current_capture(captures: &mut CaptureStacks) -> Option<&mut Vec<Segment>> {
297    captures
298        .get_mut(&std::thread::current().id())
299        .and_then(|stack| stack.last_mut())
300}
301
302/// The high-level interface for rendering to a terminal. Mirrors
303/// `rich.console.Console`.
304pub struct Console {
305    render_environment: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
306    /// The semantic-region observer (see `ConsoleRegions`); none by default.
307    region_sink: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>,
308    /// The default code highlighter (see `ConsoleCodeHighlighting`).
309    code_highlighting: Option<crate::protocol::CodeHighlighting>,
310    color_system: Option<ColorSystem>,
311    /// Upstream's `Console._width`: the width given, or read from `COLUMNS`
312    /// when the console was built. `None` falls back to `terminal_size`.
313    width: Option<usize>,
314    /// Upstream's `Console._height`, as `width` (`LINES`).
315    height: Option<usize>,
316    /// The terminal's size when the console was built (80x25 when it has
317    /// none), the width already less one on a legacy Windows console. Upstream
318    /// asks `os.get_terminal_size` on each `size` read; this port asks once.
319    terminal_size: ConsoleDimensions,
320    /// Upstream's `Console._environ`: the environment detection reads, or
321    /// `None` for the process environment.
322    environ: Option<std::sync::Arc<Environ>>,
323    is_terminal: bool,
324    no_color: bool,
325    /// Upstream's `Console.quiet`: suppress all output. Atomic because
326    /// [`on_broken_pipe`](Console::on_broken_pipe) sets it through `&self`.
327    quiet: std::sync::atomic::AtomicBool,
328    /// Upstream's `Console.soft_wrap`: the default of `print(soft_wrap=…)`.
329    soft_wrap: bool,
330    /// Upstream's `Console.style`: a style applied to all output.
331    style: Option<crate::style::StyleType>,
332    /// What a write to a closed pipe calls: upstream's overridable
333    /// `Console.on_broken_pipe`. `None` runs upstream's default.
334    on_broken_pipe: Option<BrokenPipeHandler>,
335    /// Upstream's `Console.get_time`: the clock animations read.
336    get_time: GetTime,
337    emoji: bool,
338    highlight: bool,
339    legacy_windows: bool,
340    safe_box: bool,
341    ascii_only: bool,
342    /// Upstream's `ThemeStack`: the builder's theme at the bottom, pushed
343    /// themes above it. Never empty; styles resolve against the top entry.
344    theme_stack: Vec<Theme>,
345    base_style: Style,
346    /// Registered highlighters. Shared (`Arc`) so a [`Clone`] of the console
347    /// keeps them; each is behind a lock so the console is `Sync`.
348    highlighters: Vec<std::sync::Arc<dyn Highlighter + Send + Sync>>,
349    /// Upstream's `Console(markup=…)`: whether printed strings are parsed as
350    /// console markup.
351    markup: bool,
352    /// Upstream's `Console(emoji_variant=…)`: the variant appended to emoji
353    /// codes that name none.
354    emoji_variant: Option<crate::emoji::EmojiVariant>,
355    /// Upstream's `Console(tab_size=…)`: the tab stop width `Text` expands to.
356    tab_size: usize,
357    /// While a thread is capturing, its print paths append their segments to
358    /// the innermost buffer of its stack here instead of writing to stdout.
359    /// Mirrors upstream's `ConsoleThreadLocals.buffer`: capture state is per
360    /// thread, so a thread printing while another captures is not swallowed
361    /// by that capture, and concurrent captures never mix. Each nested
362    /// capture on a thread pushes a buffer and pops it when it ends.
363    captures: std::sync::Mutex<CaptureStacks>,
364    /// Upstream's `Console._is_alt_screen`: set by
365    /// [`set_alt_screen`](Console::set_alt_screen).
366    is_alt_screen: std::sync::atomic::AtomicBool,
367    /// Upstream's `Console.record`: keep what is written for the
368    /// `export_*_recorded` methods.
369    record: std::sync::atomic::AtomicBool,
370    /// Upstream's `Console._record_buffer`: every segment written while
371    /// recording (not what a capture takes, nor what a quiet console drops).
372    record_buffer: std::sync::Mutex<Vec<Segment>>,
373    /// Upstream's `Console._log_render`, built from `log_time` and
374    /// `log_path`; it remembers the last time shown.
375    log_render: std::sync::Mutex<crate::log_render::LogRender>,
376    log_time: bool,
377    log_path: bool,
378    log_time_format: LogTimeFormat,
379    /// Upstream's `Console.get_datetime` (default `datetime.now`).
380    get_datetime: GetDateTime,
381    /// Upstream's `Console._render_hooks`.
382    render_hooks: std::sync::Mutex<Vec<std::sync::Arc<dyn RenderHook>>>,
383    /// Upstream's `Console._live_stack`. Shared with every clone: a `Live`
384    /// owns its console, so displays meant for one console get clones of
385    /// it, and nest as upstream's displays on one console object do.
386    live_stack: LiveStack,
387}
388
389/// A console's live stack, shared by its clones.
390type LiveStack =
391    std::sync::Arc<std::sync::Mutex<Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>>>>;
392
393/// A `Send`-only highlighter behind a lock, so it can be shared by a `Sync`
394/// console.
395struct LockedHighlighter(std::sync::Mutex<Box<dyn Highlighter + Send>>);
396
397impl Highlighter for LockedHighlighter {
398    fn highlight(&self, text: &mut Text) {
399        let highlighter = self
400            .0
401            .lock()
402            .unwrap_or_else(std::sync::PoisonError::into_inner);
403        highlighter.highlight(text);
404    }
405}
406
407/// `NoAltScreen("Alt screen must be enabled to call update_screen")`.
408fn no_alt_screen() -> crate::errors::RichError {
409    crate::errors::RichError::NoAltScreen(
410        "Alt screen must be enabled to call update_screen".to_string(),
411    )
412}
413
414/// Rendered lines placed at an offset on the screen: each line is preceded
415/// by a cursor move to its row. Port of `rich.console.ScreenUpdate`.
416pub struct ScreenUpdate {
417    lines: Vec<Vec<Segment>>,
418    x: usize,
419    y: usize,
420}
421
422impl ScreenUpdate {
423    /// `lines` drawn from column `x`, row `y`.
424    pub fn new(lines: Vec<Vec<Segment>>, x: usize, y: usize) -> Self {
425        ScreenUpdate { lines, x, y }
426    }
427}
428
429impl Renderable for ScreenUpdate {
430    fn rich_render(&self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment> {
431        let mut segments = Vec::new();
432        for (offset, line) in self.lines.iter().enumerate() {
433            // `Control.move_to(x, y + offset)`, widened so huge coordinates
434            // are written in full (upstream's ints are unbounded).
435            let to = crate::control::move_to_code(self.x as u128, self.y as u128 + offset as u128);
436            segments.push(Segment::control(to));
437            segments.extend(line.iter().cloned());
438        }
439        segments
440    }
441}
442
443impl Clone for Console {
444    /// An independent console with the same configuration, theme stack and
445    /// highlighters (shared). The clone starts with an empty capture buffer
446    /// and is not capturing, whatever the original is doing.
447    ///
448    /// The live stack ([`set_live`](Console::set_live)) is the one thing the
449    /// clone shares with the original: each [`Live`](crate::live::Live)
450    /// owns a console, so displays on one terminal are given clones of one
451    /// console, and a display started on a clone while another runs nests
452    /// inside it, as upstream's displays on a single console do.
453    fn clone(&self) -> Self {
454        Console {
455            render_environment: self.render_environment.clone(),
456            region_sink: self.region_sink.clone(),
457            code_highlighting: self.code_highlighting.clone(),
458            color_system: self.color_system,
459            width: self.width,
460            height: self.height,
461            terminal_size: self.terminal_size,
462            environ: self.environ.clone(),
463            is_terminal: self.is_terminal,
464            no_color: self.no_color,
465            quiet: std::sync::atomic::AtomicBool::new(self.quiet()),
466            soft_wrap: self.soft_wrap,
467            style: self.style.clone(),
468            on_broken_pipe: self.on_broken_pipe.clone(),
469            get_time: self.get_time.clone(),
470            emoji: self.emoji,
471            highlight: self.highlight,
472            legacy_windows: self.legacy_windows,
473            safe_box: self.safe_box,
474            ascii_only: self.ascii_only,
475            theme_stack: self.theme_stack.clone(),
476            base_style: self.base_style.clone(),
477            highlighters: self.highlighters.clone(),
478            markup: self.markup,
479            emoji_variant: self.emoji_variant,
480            tab_size: self.tab_size,
481            captures: std::sync::Mutex::new(CaptureStacks::new()),
482            is_alt_screen: std::sync::atomic::AtomicBool::new(false),
483            record: std::sync::atomic::AtomicBool::new(self.record()),
484            record_buffer: std::sync::Mutex::new(Vec::new()),
485            render_hooks: std::sync::Mutex::new(Vec::new()),
486            live_stack: self.live_stack.clone(),
487            log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
488            log_time: self.log_time,
489            log_path: self.log_path,
490            log_time_format: self.log_time_format.clone(),
491            get_datetime: self.get_datetime.clone(),
492        }
493    }
494}
495
496/// A theme in use on a [`Console`] until this guard drops. Returned by
497/// [`Console::use_theme`]; upstream's `ThemeContext`.
498pub struct ThemeContext<'a> {
499    console: &'a mut Console,
500}
501
502impl std::ops::Deref for ThemeContext<'_> {
503    type Target = Console;
504
505    fn deref(&self) -> &Console {
506        self.console
507    }
508}
509
510impl std::ops::DerefMut for ThemeContext<'_> {
511    fn deref_mut(&mut self) -> &mut Console {
512        self.console
513    }
514}
515
516impl Drop for ThemeContext<'_> {
517    fn drop(&mut self) {
518        // Upstream's `__exit__` pops unconditionally. This only fails if the
519        // caller already popped back down to the base through the guard.
520        let _ = self.console.pop_theme();
521    }
522}
523
524/// The alternate screen in use on a [`Console`] until this guard drops.
525/// Returned by [`Console::screen`]; upstream's `ScreenContext`.
526pub struct ScreenContext<'a> {
527    console: &'a Console,
528    hide_cursor: bool,
529    changed: bool,
530    /// Always `Some`; taken only while it is rebuilt.
531    screen: Option<crate::screen::Screen>,
532    style: crate::style::StyleType,
533}
534
535impl ScreenContext<'_> {
536    /// Print the screen, after replacing what it shows when `renderables`
537    /// are given (one after another, upstream's `Group`) and its style when
538    /// `style` is. Port of `ScreenContext.update(*renderables, style=…)`.
539    pub fn update(
540        &mut self,
541        renderables: Vec<Box<dyn Renderable>>,
542        style: Option<crate::style::StyleType>,
543    ) {
544        if let Some(style) = style {
545            self.style = style;
546        }
547        let screen = if renderables.is_empty() {
548            self.screen
549                .take()
550                .unwrap_or_else(crate::screen::Screen::empty)
551        } else {
552            crate::screen::Screen::from_renderables(renderables)
553        };
554        let screen = screen.style(self.style.clone());
555        // `console.print(self.screen, end="")`.
556        self.console.print(&screen);
557        self.screen = Some(screen);
558    }
559
560    /// The console the screen is on.
561    pub fn console(&self) -> &Console {
562        self.console
563    }
564}
565
566impl Drop for ScreenContext<'_> {
567    fn drop(&mut self) {
568        // Upstream's `__exit__`.
569        if self.changed {
570            self.console.set_alt_screen(false);
571            if self.hide_cursor {
572                self.console.show_cursor(true);
573            }
574        }
575    }
576}
577
578/// Lock `mutex`, recovering it if a panicking holder poisoned it.
579fn lock_or_recover<T>(mutex: &std::sync::Mutex<T>) -> std::sync::MutexGuard<'_, T> {
580    mutex
581        .lock()
582        .unwrap_or_else(std::sync::PoisonError::into_inner)
583}
584
585impl Default for Console {
586    fn default() -> Self {
587        Console::new()
588    }
589}
590
591impl Console {
592    /// Auto-detect terminal capabilities from the environment.
593    pub fn new() -> Self {
594        ConsoleBuilder::new().build()
595    }
596
597    /// Start configuring a console explicitly (used by tests and `rich-ext`).
598    pub fn builder() -> ConsoleBuilder {
599        ConsoleBuilder::new()
600    }
601
602    /// The active color system, or `None` when styles are not rendered at
603    /// all. Port of `Console.color_system`.
604    ///
605    /// Like upstream, this is independent of [`no_color`](Self::no_color):
606    /// no-colour mode keeps the colour system and strips only the colours at
607    /// output time, so bold, italic, underline and the like still render.
608    /// Callers asking "will colour reach the terminal?" must check both.
609    pub fn color_system(&self) -> Option<ColorSystem> {
610        self.color_system
611    }
612
613    /// Whether colour output is disabled. Port of `Console.no_color`: set by
614    /// the builder, or by a non-empty `NO_COLOR` environment variable.
615    pub fn no_color(&self) -> bool {
616        self.no_color
617    }
618
619    /// The current time in seconds from this console's clock. Port of
620    /// `Console.get_time` (default `time.monotonic`); override it with
621    /// [`ConsoleBuilder::get_time`] for deterministic animation.
622    pub fn get_time(&self) -> f64 {
623        (self.get_time)()
624    }
625
626    /// The width in cells. Port of the `Console.width` property: the
627    /// width of [`size`](Self::size).
628    pub fn width(&self) -> usize {
629        self.size().width
630    }
631
632    /// The height in rows. Port of the `Console.height` property: the
633    /// height of [`size`](Self::size). Used by height-aware renderables such
634    /// as [`Layout`](crate::layout::Layout).
635    pub fn height(&self) -> usize {
636        self.size().height
637    }
638
639    /// Set the width. Port of the `Console.width` setter.
640    pub fn set_width(&mut self, width: usize) {
641        self.width = Some(width);
642    }
643
644    /// Set the height. Port of the `Console.height` setter.
645    pub fn set_height(&mut self, height: usize) {
646        self.height = Some(height);
647    }
648
649    /// Set the width and height. Port of the `Console.size` setter.
650    pub fn set_size(&mut self, width: usize, height: usize) {
651        self.width = Some(width);
652        self.height = Some(height);
653    }
654
655    /// Whether all output is suppressed. Port of `Console.quiet`.
656    pub fn quiet(&self) -> bool {
657        self.quiet.load(std::sync::atomic::Ordering::SeqCst)
658    }
659
660    /// Suppress (or restore) all output. Upstream's `Console.quiet` is a
661    /// plain attribute; [`on_broken_pipe`](Self::on_broken_pipe) sets it.
662    pub fn set_quiet(&self, quiet: bool) {
663        self.quiet.store(quiet, std::sync::atomic::Ordering::SeqCst);
664    }
665
666    /// The default of `print(soft_wrap=…)`. Port of `Console.soft_wrap`.
667    pub fn soft_wrap(&self) -> bool {
668        self.soft_wrap
669    }
670
671    /// Change the soft wrap default. Upstream's `Console.soft_wrap` is a
672    /// plain attribute.
673    pub fn set_soft_wrap(&mut self, soft_wrap: bool) {
674        self.soft_wrap = soft_wrap;
675    }
676
677    /// The style applied to all output, if any. Port of `Console.style`.
678    pub fn style(&self) -> Option<&crate::style::StyleType> {
679        self.style.as_ref()
680    }
681
682    /// Change the style applied to all output. Upstream's `Console.style`
683    /// is a plain attribute.
684    pub fn set_style(&mut self, style: Option<crate::style::StyleType>) {
685        self.style = style;
686    }
687
688    /// Whether the console writes to a terminal that cannot render styles:
689    /// `TERM` is `dumb` or `unknown`. Port of `Console.is_dumb_terminal`.
690    pub fn is_dumb_terminal(&self) -> bool {
691        self.is_terminal && term_is_dumb(self.environ.as_deref())
692    }
693
694    /// Called when a write fails because the reader closed the pipe. Port of
695    /// `Console.on_broken_pipe`: by default the console goes
696    /// [quiet](Self::quiet) and the process exits with status 1 (upstream
697    /// raises `SystemExit(1)`). Upstream lets a subclass override it; here
698    /// [`ConsoleBuilder::on_broken_pipe`] gives the replacement.
699    ///
700    /// Upstream also points standard output at `os.devnull` so that the
701    /// interpreter's flush at exit does not fail again. A Rust process does
702    /// not flush at exit, so nothing is redirected.
703    pub fn on_broken_pipe(&self) {
704        if let Some(handler) = &self.on_broken_pipe {
705            handler(self);
706            return;
707        }
708        self.set_quiet(true);
709        std::process::exit(1);
710    }
711
712    /// Whether output is going to a real terminal.
713    pub fn is_terminal(&self) -> bool {
714        self.is_terminal
715    }
716
717    /// Whether output targets a legacy Windows console (drives box substitution).
718    pub fn legacy_windows(&self) -> bool {
719        self.legacy_windows
720    }
721
722    /// Whether to substitute box glyphs for terminal-safe variants (default on).
723    pub fn safe_box(&self) -> bool {
724        self.safe_box
725    }
726
727    /// Whether the terminal can only render ASCII (forces the `ASCII` box).
728    pub fn ascii_only(&self) -> bool {
729        self.ascii_only
730    }
731
732    /// The active theme: the top of the theme stack.
733    pub fn theme(&self) -> &Theme {
734        self.theme_stack
735            .last()
736            .expect("the theme stack always holds its base theme")
737    }
738
739    /// Resolve a style name (or pass a style through) against this console's
740    /// theme. Port of `Console.get_style`.
741    pub fn get_style(&self, style: &crate::style::StyleType) -> crate::errors::Result<Style> {
742        self.theme().get_style(style)
743    }
744
745    /// Push a theme on to the top of the stack. Port of `Console.push_theme`.
746    ///
747    /// With `inherit` the new top is the current top's styles overridden by
748    /// `theme`'s; without it, the new top is exactly `theme`. Prefer
749    /// [`use_theme`](Self::use_theme), which pops again automatically.
750    pub fn push_theme(&mut self, theme: Theme, inherit: bool) {
751        let top = if inherit {
752            let mut merged = self.theme().clone();
753            merged.extend_from(&theme);
754            merged
755        } else {
756            theme
757        };
758        self.theme_stack.push(top);
759    }
760
761    /// Remove the top theme, restoring the previous one. Port of
762    /// `Console.pop_theme`; popping the base theme is an error
763    /// (upstream's `ThemeStackError("Unable to pop base theme")`).
764    pub fn pop_theme(&mut self) -> crate::errors::Result<()> {
765        if self.theme_stack.len() == 1 {
766            return Err(crate::errors::RichError::ThemeStack(
767                "Unable to pop base theme".to_string(),
768            ));
769        }
770        self.theme_stack.pop();
771        Ok(())
772    }
773
774    /// Use a theme until the returned guard is dropped. Port of
775    /// `Console.use_theme`, Python's context manager as an RAII guard.
776    ///
777    /// The guard dereferences to the console, so print *through the guard*
778    /// while it is alive; dropping it pops the theme, including during a
779    /// panic unwind.
780    ///
781    /// ```
782    /// # use rich::{Console, Theme, Style};
783    /// let mut console = Console::builder().width(20).build();
784    /// let mut theme = Theme::new();
785    /// theme.insert("warning", Style::parse("bold red").unwrap());
786    /// {
787    ///     let themed = console.use_theme(theme);
788    ///     assert!(themed.theme().get("warning").is_some());
789    /// }
790    /// assert!(console.theme().get("warning").is_none());
791    /// ```
792    ///
793    /// Upstream's `use_theme` also takes `inherit`, but its `ThemeContext`
794    /// never passes it on to `push_theme`, so a used theme always inherits
795    /// (verified against rich 15.0.0). This port keeps that behaviour and
796    /// omits the ignored parameter; call [`push_theme`](Self::push_theme) to
797    /// replace the styles outright.
798    pub fn use_theme(&mut self, theme: Theme) -> ThemeContext<'_> {
799        self.push_theme(theme, true);
800        ThemeContext { console: self }
801    }
802
803    /// The whole-output base style.
804    pub fn base_style(&self) -> &Style {
805        &self.base_style
806    }
807
808    /// Register a highlighter. **The core plugin seam** — see docs/PLUGINS.md.
809    /// The highlighter must be `Send` so a [`Console`](Console) can move to a
810    /// background thread (e.g. an auto-refreshing [`Live`](crate::live::Live)).
811    pub fn add_highlighter(&mut self, highlighter: Box<dyn Highlighter + Send>) {
812        self.highlighters
813            .push(std::sync::Arc::new(LockedHighlighter(
814                std::sync::Mutex::new(highlighter),
815            )));
816    }
817
818    /// The default render options for this console (full width, no height).
819    pub fn options(&self) -> ConsoleOptions {
820        let size = self.size();
821        ConsoleOptions {
822            min_width: 1,
823            max_width: size.width,
824            height: None,
825            justify: Justify::Default,
826            overflow: None,
827            no_wrap: None,
828            highlight: None,
829            markup: None,
830            max_height: size.height,
831            encoding: self.encoding().to_string(),
832            is_terminal: self.is_terminal,
833            legacy_windows: self.legacy_windows,
834            size,
835        }
836    }
837
838    /// The size of the console. Port of the `Console.size` property: the
839    /// width and height given (the width one less on a legacy Windows
840    /// console, as upstream subtracts it when both are set), else 80x25 on
841    /// a [dumb terminal](Self::is_dumb_terminal), else the terminal's size
842    /// for whichever was not given.
843    pub fn size(&self) -> ConsoleDimensions {
844        if let (Some(width), Some(height)) = (self.width, self.height) {
845            return ConsoleDimensions {
846                width: width.saturating_sub(usize::from(self.legacy_windows)),
847                height,
848            };
849        }
850        if self.is_dumb_terminal() {
851            return ConsoleDimensions {
852                width: DEFAULT_WIDTH,
853                height: DEFAULT_HEIGHT,
854            };
855        }
856        ConsoleDimensions {
857            width: self.width.unwrap_or(self.terminal_size.width),
858            height: self.height.unwrap_or(self.terminal_size.height),
859        }
860    }
861
862    /// The output encoding: `"ascii"` for an [ASCII-only](Self::ascii_only)
863    /// console, else `"utf-8"`. Port of the `Console.encoding` property (which
864    /// reads the output file's encoding; this port has no file to ask).
865    pub fn encoding(&self) -> &'static str {
866        if self.ascii_only {
867            "ascii"
868        } else {
869            "utf-8"
870        }
871    }
872
873    /// Render a value to an ANSI string (no trailing newline). Primarily for
874    /// tests and inline rendering.
875    ///
876    /// When no explicit justify is requested, the width is first shrunk to the
877    /// renderable's measured width (matching upstream's measurement-fit for a
878    /// bare top-level renderable).
879    pub fn render_to_string(&self, renderable: &dyn Renderable) -> String {
880        let segments = self.render_segments(renderable);
881        self.segments_to_string(&segments)
882    }
883
884    /// Render a renderable to segments as `Console.print` does (shared by the
885    /// string and print paths): a printed `Text` goes through upstream's
886    /// `Text.join`, and an extension that opts into measurement-fit is shrunk
887    /// to its measured width when no explicit justify is set.
888    fn render_segments(&self, renderable: &dyn Renderable) -> Vec<Segment> {
889        self.render_segments_with(renderable, &self.options())
890    }
891
892    /// Render a renderable to segments with explicit render options, exactly
893    /// as [`print_with`](Self::print_with) does before writing: a printed
894    /// `Text` goes through upstream's `Text.join`, a renderable that opts in
895    /// is fitted to its measurement, and the result is cropped to the console
896    /// width (`print(crop=True)`). No trailing newline is added.
897    ///
898    /// For upstream's lower-level `Console.render`, see [`render`](Self::render).
899    pub fn render_segments_with(
900        &self,
901        renderable: &dyn Renderable,
902        options: &ConsoleOptions,
903    ) -> Vec<Segment> {
904        self.print_segments(renderable, options, false)
905    }
906
907    /// The segments `print` writes for `renderable`: those of
908    /// [`render_segments_with`](Self::render_segments_with), followed, when
909    /// `end_line` and the renderable ends its last line, by the line break.
910    ///
911    /// The console's [`soft_wrap`](Self::soft_wrap) applies as upstream's
912    /// `print(soft_wrap=None)` takes it (no wrapping, `overflow="ignore"`,
913    /// nothing cropped), and its [`style`](Self::style) goes under all of
914    /// the output (`_collect_renderables` wraps each renderable in `Styled`).
915    fn print_segments(
916        &self,
917        renderable: &dyn Renderable,
918        options: &ConsoleOptions,
919        end_line: bool,
920    ) -> Vec<Segment> {
921        let mut options = options.clone();
922        let crop = !self.soft_wrap;
923        if self.soft_wrap {
924            options.no_wrap = options.no_wrap.or(Some(true));
925            options.overflow = options.overflow.or(Some(Overflow::Ignore));
926        }
927        let ends_line = end_line && Self::print_ends_line(renderable, &options);
928        let joined;
929        let joined_text = renderable.printed_text();
930        let renderable = match joined_text.clone() {
931            Some(text) => {
932                // `print(justify="left"|"center"|"right")` wraps the joined
933                // text in `Align`, which renders a zero-width text (such as an
934                // empty one) at no width at all: nothing is printed.
935                if matches!(
936                    options.justify,
937                    Justify::Left | Justify::Center | Justify::Right
938                ) && text.measurement().1 == 0
939                {
940                    return Vec::new();
941                }
942                joined = text;
943                &joined as &dyn Renderable
944            }
945            None => renderable,
946        };
947        // `print(justify="left"|"center"|"right")` wraps every other
948        // renderable in `Align(renderable, justify)` (`_collect_renderables`).
949        let align = match options.justify {
950            Justify::Left if joined_text.is_none() => Some(crate::align::HorizontalAlign::Left),
951            Justify::Center if joined_text.is_none() => Some(crate::align::HorizontalAlign::Center),
952            Justify::Right if joined_text.is_none() => Some(crate::align::HorizontalAlign::Right),
953            _ => None,
954        };
955        if options.justify == Justify::Default && renderable.fit_to_measurement() {
956            let measurement = renderable.measure(self, &options);
957            options.max_width = measurement.maximum.min(options.max_width).max(1);
958        }
959        let mut segments = match align {
960            // `Console.render` yields nothing when there is no width.
961            _ if options.max_width < 1 => Vec::new(),
962            Some(align) => crate::align::Align::render_child(renderable, align, self, &options),
963            None => renderable.rich_render(self, &options),
964        };
965        if !segments.is_empty() {
966            if ends_line {
967                segments.push(Segment::line());
968            }
969            if let Some(style) = self.console_style() {
970                segments = Segment::apply_style_with(&segments, Some(&style), None);
971            }
972        }
973        // A print (not a render to a string) goes through the render hooks.
974        if end_line {
975            segments = self.process_render_hooks(vec![segments], &options).concat();
976        }
977        if segments.is_empty() || !crop {
978            return segments;
979        }
980        // `Console.print(crop=True)`: the final backstop against a line running
981        // off the side of the terminal. Renderables that fit are untouched; this
982        // is what gives `Overflow::Ignore` its "wrap nothing, but still don't
983        // corrupt the display" behaviour. The line breaks come out plain, as
984        // upstream's `split_and_crop_lines` writes them.
985        Segment::crop_lines(&segments, self.width())
986    }
987
988    /// The console's [`style`](Self::style), resolved against its theme
989    /// (an unknown name is no style on this lenient print path).
990    fn console_style(&self) -> Option<Style> {
991        let style = self.style.as_ref()?;
992        self.get_style(style).ok().filter(|style| !style.is_null())
993    }
994
995    /// Render a renderable to segments. Port of `Console.render`: `options`
996    /// defaults to [`options`](Self::options), and nothing is rendered when
997    /// there is no width (`max_width < 1`).
998    ///
999    /// Unlike the print path ([`render_segments_with`](Self::render_segments_with))
1000    /// the renderable is rendered as-is: no `Text.join`, no fitting and no
1001    /// crop. Container renderables use this for their children.
1002    ///
1003    /// As everywhere in this port, lines are *separated* by newline segments:
1004    /// the final line has no trailing `"\n"` where upstream's has one.
1005    pub fn render(
1006        &self,
1007        renderable: &dyn Renderable,
1008        options: Option<&ConsoleOptions>,
1009    ) -> Vec<Segment> {
1010        let default_options;
1011        let options = match options {
1012            Some(options) => options,
1013            None => {
1014                default_options = self.options();
1015                &default_options
1016            }
1017        };
1018        if options.max_width < 1 {
1019            return Vec::new();
1020        }
1021        renderable.rich_render(self, options)
1022    }
1023
1024    /// Write (or, while capturing, record) a rendered segment stream, adding a
1025    /// trailing newline. The single sink for every `print*` path.
1026    fn emit(&self, segments: Vec<Segment>) {
1027        self.emit_end(segments, true);
1028    }
1029
1030    /// [`emit`](Self::emit), with the trailing newline optional: upstream's
1031    /// `print(…, end="")` when `newline` is false. Output written straight to
1032    /// stdout is flushed in that case, so a prompt shows before input is read.
1033    fn emit_end(&self, segments: Vec<Segment>, newline: bool) {
1034        // `_check_buffer`: a quiet console drops its output, captured or not.
1035        if segments.is_empty() || self.quiet() {
1036            return;
1037        }
1038        let segments = {
1039            let mut captures = self.lock_captures();
1040            if let Some(buffer) = current_capture(&mut captures) {
1041                buffer.extend(segments);
1042                if newline {
1043                    buffer.push(Segment::line());
1044                }
1045                return;
1046            }
1047            segments
1048        };
1049        // `_write_buffer`: what is written is recorded first.
1050        if self.record() {
1051            let mut recorded = self.lock_record_buffer();
1052            recorded.extend(segments.iter().cloned());
1053            if newline {
1054                recorded.push(Segment::line());
1055            }
1056        }
1057        let mut output = self.segments_to_string(&segments);
1058        if newline {
1059            output.push('\n');
1060        }
1061        let stdout = std::io::stdout();
1062        let mut lock = stdout.lock();
1063        let mut written = lock.write_all(output.as_bytes());
1064        if written.is_ok() && !newline {
1065            written = lock.flush();
1066        }
1067        drop(lock);
1068        // `except BrokenPipeError: self.on_broken_pipe()`. Other write
1069        // errors are ignored, as this port has always done.
1070        if written.is_err_and(|error| error.kind() == std::io::ErrorKind::BrokenPipe) {
1071            self.on_broken_pipe();
1072        }
1073    }
1074
1075    /// Display `prompt` and read a line of input from standard input. Port of
1076    /// `Console.input`: the prompt is console markup, printed through the
1077    /// console with `end=""` so it is captured and exported like any other
1078    /// output. `None` means end of input.
1079    pub fn input(&self, prompt: &str) -> std::io::Result<Option<String>> {
1080        let prompt = self.build_text(prompt);
1081        self.input_from(&prompt, &mut crate::prompt::StdinInput)
1082    }
1083
1084    /// [`input`](Self::input) with a renderable prompt (upstream accepts a
1085    /// `Text`) and an explicit input source — upstream's `stream=` argument.
1086    pub fn input_from(
1087        &self,
1088        prompt: &dyn Renderable,
1089        stream: &mut dyn crate::prompt::InputSource,
1090    ) -> std::io::Result<Option<String>> {
1091        // `if prompt: self.print(prompt, end="")`.
1092        let segments = self.render_segments(prompt);
1093        if segments.iter().any(|segment| !segment.text.is_empty()) {
1094            self.emit_end(segments, false);
1095        }
1096        stream.read_line()
1097    }
1098
1099    /// Render a value into a list of lines, each a list of [`Segment`]s.
1100    ///
1101    /// Port of `Console.render_lines`. When `pad` is true, every line is padded
1102    /// (or cropped) to `options.max_width` — this is what container renderables
1103    /// such as `Panel`/`Padding` rely on to get uniform-width child rows.
1104    pub fn render_lines(
1105        &self,
1106        renderable: &dyn Renderable,
1107        options: &ConsoleOptions,
1108        pad: bool,
1109    ) -> Vec<Vec<Segment>> {
1110        self.render_lines_styled(renderable, options, None, pad)
1111    }
1112
1113    /// [`render_lines`](Self::render_lines) with upstream's `style=` argument:
1114    /// the style is applied under every rendered segment and to the padding
1115    /// that fills each line, as `Panel` and `Padding` use it.
1116    pub fn render_lines_styled(
1117        &self,
1118        renderable: &dyn Renderable,
1119        options: &ConsoleOptions,
1120        style: Option<&Style>,
1121        pad: bool,
1122    ) -> Vec<Vec<Segment>> {
1123        // Upstream pads with the `style` argument as given (default `None`),
1124        // so the pad segments carry no style unless one was passed.
1125        let pad_style = style.cloned();
1126        let style = style.filter(|style| !style.is_null());
1127        // Upstream `Console.render` yields nothing when `max_width < 1`, so a
1128        // renderable squeezed to zero width contributes no lines (#449).
1129        let mut segments = if options.max_width < 1 {
1130            Vec::new()
1131        } else {
1132            renderable.rich_render(self, options)
1133        };
1134        if let Some(style) = style {
1135            segments = Segment::apply_style(&segments, style);
1136        }
1137        // An empty `Text` renders as a lone empty segment: upstream renders it
1138        // as its `end` newline, which `split_and_crop_lines` turns into one
1139        // blank line (#442).
1140        let only_empty = !segments.is_empty()
1141            && segments
1142                .iter()
1143                .all(|segment| !segment.control && segment.text.is_empty());
1144        let mut lines = Segment::split_lines_owned(segments);
1145        if lines.is_empty() && only_empty {
1146            lines.push(Vec::new());
1147        }
1148        if pad {
1149            for line in &mut lines {
1150                *line = Segment::adjust_line_length(line, options.max_width, pad_style.clone());
1151            }
1152        }
1153        // Honor an explicit height by cropping/padding to exactly that many rows
1154        // (matching `Console.render_lines`'s height handling — used by height-
1155        // aware containers such as `Panel` inside a `Layout`).
1156        if let Some(height) = options.height {
1157            lines.truncate(height);
1158            while lines.len() < height {
1159                lines.push(if pad {
1160                    vec![Segment::new(
1161                        " ".repeat(options.max_width),
1162                        pad_style.clone(),
1163                    )]
1164                } else {
1165                    Vec::new()
1166                });
1167            }
1168        }
1169        lines
1170    }
1171
1172    /// Render a value exactly as [`print`](Console::print) would write it,
1173    /// returning the string (including the single trailing newline). For tests
1174    /// and export.
1175    pub fn render_export(&self, renderable: &dyn Renderable) -> String {
1176        self.render_export_with(renderable, &self.options())
1177    }
1178
1179    /// Render a value and write it to stdout, followed by a newline (unless
1180    /// the renderable ends without one upstream:
1181    /// [`Renderable::print_end_line`]).
1182    pub fn print(&self, renderable: &dyn Renderable) {
1183        self.print_with(renderable, &self.options());
1184    }
1185
1186    /// Whether printing `renderable` under `options` ends its last line: a
1187    /// renderable that `print(justify=…)` wraps in `Align` always does.
1188    fn print_ends_line(renderable: &dyn Renderable, options: &ConsoleOptions) -> bool {
1189        renderable.print_end_line(options)
1190            || (renderable.printed_text().is_none()
1191                && matches!(
1192                    options.justify,
1193                    Justify::Left | Justify::Center | Justify::Right
1194                ))
1195    }
1196
1197    /// Print with explicit render options, the equivalent of upstream's
1198    /// `Console.print(renderable, justify=…, overflow=…, no_wrap=…)`. Start
1199    /// from [`options`](Self::options) and set the fields to override. A
1200    /// printed `Text` defers to these options, because upstream's `Text.join`
1201    /// drops the text's own `justify`, `overflow` and `no_wrap`.
1202    pub fn print_with(&self, renderable: &dyn Renderable, options: &ConsoleOptions) {
1203        let segments = self.print_segments(renderable, options, true);
1204        self.emit_end(segments, false);
1205    }
1206
1207    /// Like [`render_export`](Self::render_export), with explicit render
1208    /// options as for [`print_with`](Self::print_with).
1209    pub fn render_export_with(
1210        &self,
1211        renderable: &dyn Renderable,
1212        options: &ConsoleOptions,
1213    ) -> String {
1214        let segments = self.print_segments(renderable, options, true);
1215        self.segments_to_string(&segments)
1216    }
1217
1218    /// Write a terminal control sequence to stdout.
1219    ///
1220    /// Port of `Console.control`. Control codes are only written when output is
1221    /// a real terminal (they are meaningless when redirected to a file).
1222    pub fn control(&self, control: &crate::control::Control) {
1223        // Upstream buffers control codes with everything else, so a capture
1224        // or a recording keeps them; only a terminal is sent them
1225        // (`_render_buffer` drops them otherwise).
1226        let text = control.as_str();
1227        if !text.is_empty() {
1228            self.emit_end(vec![Segment::control(text)], false);
1229        }
1230    }
1231
1232    /// Add a render hook to the stack. Port of `Console.push_render_hook`.
1233    pub fn push_render_hook(&self, hook: std::sync::Arc<dyn RenderHook>) {
1234        lock_or_recover(&self.render_hooks).push(hook);
1235    }
1236
1237    /// Remove the last render hook pushed. Port of
1238    /// `Console.pop_render_hook`; with none left, nothing happens (upstream
1239    /// raises `IndexError`).
1240    pub fn pop_render_hook(&self) {
1241        lock_or_recover(&self.render_hooks).pop();
1242    }
1243
1244    /// Hand what a print is about to write to the render hooks, in the order
1245    /// they were pushed, and render what they return. Each item is one
1246    /// renderable already rendered, its last line ended.
1247    fn process_render_hooks(
1248        &self,
1249        items: Vec<Vec<Segment>>,
1250        options: &ConsoleOptions,
1251    ) -> Vec<Vec<Segment>> {
1252        let hooks = lock_or_recover(&self.render_hooks).clone();
1253        if hooks.is_empty() {
1254            return items;
1255        }
1256        let mut renderables: Vec<Box<dyn Renderable>> = items
1257            .into_iter()
1258            .map(|segments| Box::new(Rendered(segments)) as Box<dyn Renderable>)
1259            .collect();
1260        for hook in &hooks {
1261            renderables = hook.process_renderables(renderables);
1262        }
1263        renderables
1264            .iter()
1265            .map(|renderable| {
1266                let mut segments = self.render(renderable.as_ref(), Some(options));
1267                if !segments.is_empty() && renderable.print_end_line(options) {
1268                    segments.push(Segment::line());
1269                }
1270                segments
1271            })
1272            .collect()
1273    }
1274
1275    /// Push a live display on to this console's stack, returning whether it
1276    /// is the only one (the display that draws). Port of `Console.set_live`,
1277    /// which upstream's `Live` calls when it starts.
1278    ///
1279    /// The stack is shared by the console's clones. A
1280    /// [`Live`](crate::live::Live) pushes itself here when it starts and
1281    /// pops the stack when it stops; one started while the stack is not
1282    /// empty is nested, and the first display draws it below its own
1283    /// renderable. Anything else pushed here takes a place on the stack and
1284    /// draws nothing.
1285    pub fn set_live(&self, live: std::sync::Arc<dyn std::any::Any + Send + Sync>) -> bool {
1286        let mut stack = lock_or_recover(&self.live_stack);
1287        stack.push(live);
1288        stack.len() == 1
1289    }
1290
1291    /// Pop the last live display pushed by [`set_live`](Self::set_live).
1292    /// Port of `Console.clear_live`; with none left, nothing happens
1293    /// (upstream raises `IndexError`).
1294    pub fn clear_live(&self) {
1295        lock_or_recover(&self.live_stack).pop();
1296    }
1297
1298    /// What the live stack holds now, first pushed first (upstream's
1299    /// `Console._live_stack`).
1300    pub(crate) fn live_stack(&self) -> Vec<std::sync::Arc<dyn std::any::Any + Send + Sync>> {
1301        lock_or_recover(&self.live_stack).clone()
1302    }
1303
1304    /// Use the alternate screen until the returned guard drops. Port of
1305    /// `Console.screen(hide_cursor=…, style=…)`, Python's context manager as
1306    /// an RAII guard: the alternate screen is switched on (and the cursor
1307    /// hidden, when `hide_cursor`), and switched back when the guard drops,
1308    /// if it was switched at all (only a terminal is). Print a full-screen
1309    /// renderable with [`ScreenContext::update`].
1310    pub fn screen(
1311        &self,
1312        hide_cursor: bool,
1313        style: Option<crate::style::StyleType>,
1314    ) -> ScreenContext<'_> {
1315        let changed = self.set_alt_screen(true);
1316        if changed && hide_cursor {
1317            self.show_cursor(false);
1318        }
1319        // `Screen(style=style or "")`.
1320        let style = style.unwrap_or_else(|| "".into());
1321        ScreenContext {
1322            console: self,
1323            hide_cursor,
1324            changed,
1325            screen: Some(crate::screen::Screen::empty().style(style.clone())),
1326            style,
1327        }
1328    }
1329
1330    /// Display a status and spinner until it is stopped. Port of
1331    /// `Console.status`: `status` carries the message and upstream's
1332    /// keyword arguments (`spinner`, `spinner_style`, `speed`,
1333    /// `refresh_per_second`), and the display starts at once on a copy of
1334    /// this console, writing to standard output, as `with
1335    /// console.status(…):` does; [`LiveStatus::stop`](crate::status::LiveStatus::stop)
1336    /// ends it.
1337    pub fn status(
1338        &self,
1339        status: crate::status::Status,
1340    ) -> crate::status::LiveStatus<std::io::Stdout> {
1341        self.status_to(status, std::io::stdout())
1342    }
1343
1344    /// [`status`](Self::status), writing to `writer` instead of standard
1345    /// output (upstream's console `file`).
1346    pub fn status_to<W: Write + Send + 'static>(
1347        &self,
1348        status: crate::status::Status,
1349        writer: W,
1350    ) -> crate::status::LiveStatus<W> {
1351        status.start(self.clone(), writer)
1352    }
1353
1354    /// Whether the alternate screen is enabled. Port of
1355    /// `Console.is_alt_screen`.
1356    pub fn is_alt_screen(&self) -> bool {
1357        self.is_alt_screen.load(std::sync::atomic::Ordering::SeqCst)
1358    }
1359
1360    /// Enable or disable the alternate screen. Port of
1361    /// `Console.set_alt_screen`: only a terminal (not a legacy Windows
1362    /// console) switches, and the result says whether it did.
1363    pub fn set_alt_screen(&self, enable: bool) -> bool {
1364        if self.is_terminal && !self.legacy_windows {
1365            self.control(&crate::control::Control::alt_screen(enable));
1366            self.is_alt_screen
1367                .store(enable, std::sync::atomic::Ordering::SeqCst);
1368            return true;
1369        }
1370        false
1371    }
1372
1373    /// Render `renderable` into `region` of the alternate screen (the whole
1374    /// screen when `None`). Port of `Console.update_screen`.
1375    pub fn update_screen(
1376        &self,
1377        renderable: &dyn Renderable,
1378        region: Option<crate::region::Region>,
1379        options: Option<&ConsoleOptions>,
1380    ) -> crate::errors::Result<()> {
1381        if !self.is_alt_screen() {
1382            return Err(no_alt_screen());
1383        }
1384        let render_options = options.cloned().unwrap_or_else(|| self.options());
1385        let (x, y, render_options) = match region {
1386            None => {
1387                let height = render_options
1388                    .height
1389                    .filter(|&height| height > 0)
1390                    .unwrap_or(self.height());
1391                let width = render_options.max_width;
1392                (0, 0, render_options.update_dimensions(width, height))
1393            }
1394            Some(region) => (
1395                region.x,
1396                region.y,
1397                render_options.update_dimensions(region.width, region.height),
1398            ),
1399        };
1400        let lines = self.render_lines(renderable, &render_options, true);
1401        self.update_screen_lines(&lines, x, y)
1402    }
1403
1404    /// Write rendered `lines` to the alternate screen at column `x`, row `y`.
1405    /// Port of `Console.update_screen_lines` (through [`ScreenUpdate`]).
1406    pub fn update_screen_lines(
1407        &self,
1408        lines: &[Vec<Segment>],
1409        x: usize,
1410        y: usize,
1411    ) -> crate::errors::Result<()> {
1412        if !self.is_alt_screen() {
1413            return Err(no_alt_screen());
1414        }
1415        let update = ScreenUpdate::new(lines.to_vec(), x, y);
1416        let segments = self.render(&update, None);
1417        self.emit_end(segments, false);
1418        Ok(())
1419    }
1420
1421    /// Show or hide the cursor. Port of `Console.show_cursor`.
1422    pub fn show_cursor(&self, show: bool) {
1423        self.control(&crate::control::Control::show_cursor(show));
1424    }
1425
1426    /// Clear the screen. Port of `Console.clear`.
1427    pub fn clear(&self) {
1428        self.control(&crate::control::Control::clear());
1429    }
1430
1431    /// Ring the terminal bell. Port of `Console.bell`.
1432    pub fn bell(&self) {
1433        self.control(&crate::control::Control::bell());
1434    }
1435
1436    /// Capture everything printed inside `f` instead of writing it to stdout,
1437    /// returning it as a rendered (ANSI) string.
1438    ///
1439    /// The Rust analogue of upstream's `with console.capture() as capture:` —
1440    /// the closure receives the same console, and captures nest correctly.
1441    /// Equivalent to what would have been written to the terminal.
1442    ///
1443    /// Each closure capture holds only what was printed inside it. Upstream's
1444    /// nested `capture()` contexts share one buffer, so the inner one takes
1445    /// everything printed since the outer one began;
1446    /// [`begin_capture`](Self::begin_capture) and
1447    /// [`end_capture`](Self::end_capture) behave that way.
1448    pub fn capture(&self, f: impl FnOnce(&Console)) -> String {
1449        let segments = self.capture_segments(f);
1450        self.segments_to_string(&segments)
1451    }
1452
1453    /// Begin capturing this thread's output: what is printed is held until
1454    /// [`end_capture`](Self::end_capture) instead of written. Port of
1455    /// `Console.begin_capture`.
1456    pub fn begin_capture(&self) {
1457        self.lock_captures()
1458            .entry(std::thread::current().id())
1459            .or_default()
1460            .push(Vec::new());
1461    }
1462
1463    /// End the innermost capture begun with
1464    /// [`begin_capture`](Self::begin_capture) on this thread, returning what
1465    /// was printed as a terminal string. Port of `Console.end_capture`.
1466    ///
1467    /// As upstream, which holds one buffer per thread, everything captured
1468    /// on the thread so far is returned, including what outer captures hold,
1469    /// and those are left empty. Without a capture to end, the result is
1470    /// empty (upstream's console would stop writing altogether).
1471    pub fn end_capture(&self) -> String {
1472        let id = std::thread::current().id();
1473        let segments: Vec<Segment> = {
1474            let mut captures = self.lock_captures();
1475            let Some(stack) = captures.get_mut(&id) else {
1476                return String::new();
1477            };
1478            let segments = stack.iter_mut().flat_map(std::mem::take).collect();
1479            stack.pop();
1480            if stack.is_empty() {
1481                captures.remove(&id);
1482            }
1483            segments
1484        };
1485        self.segments_to_string(&segments)
1486    }
1487
1488    /// Like [`capture`](Self::capture) but with all styles stripped, returning
1489    /// plain text. Port of `Console.export_text(styles=False)`.
1490    pub fn export_text(&self, f: impl FnOnce(&Console)) -> String {
1491        let segments = self.capture_segments(f);
1492        segments_to_plain(&segments)
1493    }
1494
1495    /// Like [`export_text`](Self::export_text) with escape codes. Port of
1496    /// `Console.export_text(styles=True)`: each segment in the console's
1497    /// colour system (upstream's style keeps the codes printing rendered),
1498    /// or in truecolor, `Style.render`'s default, without one.
1499    pub fn export_text_styled(&self, f: impl FnOnce(&Console)) -> String {
1500        let segments = self.capture_segments(f);
1501        self.segments_to_styled_text(&segments)
1502    }
1503
1504    /// Write what `f` prints to `path` as text, with escape codes when
1505    /// `styles`. Port of `Console.save_text`.
1506    pub fn save_text(
1507        &self,
1508        path: impl AsRef<std::path::Path>,
1509        styles: bool,
1510        f: impl FnOnce(&Console),
1511    ) -> std::io::Result<()> {
1512        let text = if styles {
1513            self.export_text_styled(f)
1514        } else {
1515            self.export_text(f)
1516        };
1517        std::fs::write(path, text)
1518    }
1519
1520    /// Write what `f` prints to `path` as HTML with a stylesheet. Port of
1521    /// `Console.save_html` (its default `inline_styles=False`).
1522    pub fn save_html(
1523        &self,
1524        path: impl AsRef<std::path::Path>,
1525        f: impl FnOnce(&Console),
1526    ) -> std::io::Result<()> {
1527        std::fs::write(path, self.export_html_classes(f))
1528    }
1529
1530    /// Write what `f` prints to `path` as SVG. Port of `Console.save_svg`;
1531    /// as for [`export_svg`](Self::export_svg), the `unique_id` is given.
1532    pub fn save_svg(
1533        &self,
1534        path: impl AsRef<std::path::Path>,
1535        title: &str,
1536        unique_id: &str,
1537        f: impl FnOnce(&Console),
1538    ) -> std::io::Result<()> {
1539        std::fs::write(path, self.export_svg(title, unique_id, f))
1540    }
1541
1542    /// Buffer everything printed inside `f` and display it through the system
1543    /// pager. The Rust analogue of upstream's `with console.pager():` block.
1544    ///
1545    /// Styles are stripped unless `styles` is set, matching
1546    /// `Console.pager(styles=False)`. When there's no terminal to page in (piped
1547    /// output, `TERM=dumb`) or no pager can be started, the content is written
1548    /// straight to stdout.
1549    pub fn page(&self, styles: bool, f: impl FnOnce(&Console)) -> std::io::Result<()> {
1550        self.page_with(&crate::pager::SystemPager, styles, f)
1551    }
1552
1553    /// Like [`page`](Self::page) but with an explicit [`Pager`](crate::pager::Pager)
1554    /// — the seam upstream exposes as `Console.pager(pager=…)`. Hyperlinks
1555    /// are stripped, as upstream's default `links=False` does.
1556    pub fn page_with(
1557        &self,
1558        pager: &dyn crate::pager::Pager,
1559        styles: bool,
1560        f: impl FnOnce(&Console),
1561    ) -> std::io::Result<()> {
1562        self.page_with_links(pager, styles, false, f)
1563    }
1564
1565    /// Like [`page_with`](Self::page_with), keeping hyperlinks when `links`
1566    /// and `styles` are both set. Port of `Console.pager(pager, styles,
1567    /// links)`: without `styles` everything is plain, and without `links`
1568    /// each style loses its link (`Segment.strip_links`).
1569    pub fn page_with_links(
1570        &self,
1571        pager: &dyn crate::pager::Pager,
1572        styles: bool,
1573        links: bool,
1574        f: impl FnOnce(&Console),
1575    ) -> std::io::Result<()> {
1576        let mut segments = self.capture_segments(f);
1577        let content = if !styles {
1578            segments_to_plain(&segments)
1579        } else {
1580            if !links {
1581                for segment in &mut segments {
1582                    if let Some(style) = segment.style.as_mut() {
1583                        if style.link().is_some() {
1584                            *style = style.update_link(None);
1585                        }
1586                    }
1587                }
1588            }
1589            self.segments_to_string(&segments)
1590        };
1591        pager.show(&content)
1592    }
1593
1594    /// Capture output printed inside `f` and export it as a self-contained HTML
1595    /// document (inline styles), using the default terminal theme. Port of
1596    /// `Console.export_html(inline_styles=True)`.
1597    pub fn export_html(&self, f: impl FnOnce(&Console)) -> String {
1598        self.export_html_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1599    }
1600
1601    /// Like [`export_html`](Self::export_html) but with an explicit palette —
1602    /// upstream's `export_html(theme=…)`. See [`terminal_theme`] for the
1603    /// bundled presets.
1604    ///
1605    /// [`terminal_theme`]: crate::terminal_theme
1606    pub fn export_html_themed(
1607        &self,
1608        theme: &crate::terminal_theme::TerminalTheme,
1609        f: impl FnOnce(&Console),
1610    ) -> String {
1611        let segments = self.capture_segments(f);
1612        crate::export::export_html_inline(&segments, theme)
1613    }
1614
1615    /// Like [`export_html`](Self::export_html) but with a generated CSS-class
1616    /// stylesheet (`.r1 {…}`) instead of inline styles. Port of upstream's
1617    /// default `Console.export_html(inline_styles=False)`.
1618    pub fn export_html_classes(&self, f: impl FnOnce(&Console)) -> String {
1619        self.export_html_classes_themed(&crate::terminal_theme::DEFAULT_TERMINAL_THEME, f)
1620    }
1621
1622    /// Like [`export_html_classes`](Self::export_html_classes) but with an
1623    /// explicit palette — upstream's `export_html(theme=…, inline_styles=False)`.
1624    pub fn export_html_classes_themed(
1625        &self,
1626        theme: &crate::terminal_theme::TerminalTheme,
1627        f: impl FnOnce(&Console),
1628    ) -> String {
1629        let segments = self.capture_segments(f);
1630        crate::export::export_html_classes(&segments, theme)
1631    }
1632
1633    /// Capture output printed inside `f` and export it as a self-contained SVG
1634    /// image of a terminal window, using [`SVG_EXPORT_THEME`]. Port of
1635    /// `Console.export_svg`.
1636    ///
1637    /// `unique_id` prefixes every generated id/class. Upstream's auto-computed
1638    /// default hashes Python `repr()` output (not reproducible in Rust), so this
1639    /// port takes an explicit id; output is byte-parity with
1640    /// `export_svg(title=…, unique_id=…)` (see docs/DIVERGENCES.md #15).
1641    ///
1642    /// [`SVG_EXPORT_THEME`]: crate::terminal_theme::SVG_EXPORT_THEME
1643    pub fn export_svg(&self, title: &str, unique_id: &str, f: impl FnOnce(&Console)) -> String {
1644        self.export_svg_themed(
1645            &crate::terminal_theme::SVG_EXPORT_THEME,
1646            title,
1647            unique_id,
1648            f,
1649        )
1650    }
1651
1652    /// Like [`export_svg`](Self::export_svg) but with an explicit palette —
1653    /// upstream's `export_svg(theme=…)`.
1654    pub fn export_svg_themed(
1655        &self,
1656        theme: &crate::terminal_theme::TerminalTheme,
1657        title: &str,
1658        unique_id: &str,
1659        f: impl FnOnce(&Console),
1660    ) -> String {
1661        let segments = self.capture_segments(f);
1662        crate::svg::export_svg(&segments, theme, title, unique_id, self.width())
1663    }
1664
1665    /// Capture output printed inside `f` and export it as HTML with every
1666    /// option of upstream's `Console.export_html`: `code_format` (default
1667    /// [`CONSOLE_HTML_FORMAT`](crate::export::CONSOLE_HTML_FORMAT)) and
1668    /// `inline_styles`. See [`export::export_html_with`](crate::export::export_html_with).
1669    pub fn export_html_with(
1670        &self,
1671        theme: &crate::terminal_theme::TerminalTheme,
1672        code_format: Option<&str>,
1673        inline_styles: bool,
1674        f: impl FnOnce(&Console),
1675    ) -> Result<String, crate::export::ExportFormatError> {
1676        let segments = self.capture_segments(f);
1677        crate::export::export_html_with(&segments, theme, code_format, inline_styles)
1678    }
1679
1680    /// Capture output printed inside `f` and export it as SVG with every
1681    /// option of upstream's `Console.export_svg`: `code_format` (default
1682    /// [`CONSOLE_SVG_FORMAT`](crate::svg::CONSOLE_SVG_FORMAT)) and
1683    /// `font_aspect_ratio` (upstream default 0.61). See
1684    /// [`svg::export_svg_with`](crate::svg::export_svg_with).
1685    #[allow(clippy::too_many_arguments)]
1686    pub fn export_svg_with(
1687        &self,
1688        theme: &crate::terminal_theme::TerminalTheme,
1689        title: &str,
1690        unique_id: &str,
1691        code_format: Option<&str>,
1692        font_aspect_ratio: f64,
1693        f: impl FnOnce(&Console),
1694    ) -> Result<String, crate::export::ExportFormatError> {
1695        let segments = self.capture_segments(f);
1696        crate::svg::export_svg_with(
1697            &segments,
1698            theme,
1699            title,
1700            unique_id,
1701            self.width(),
1702            code_format.unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1703            font_aspect_ratio,
1704        )
1705    }
1706
1707    /// Record everything `f` prints and hand back the raw segments, without
1708    /// writing to the terminal.
1709    ///
1710    /// This is the seam for producing *several* outputs from one render — the
1711    /// terminal bytes and an HTML and an SVG file, say — which is what
1712    /// `rich --export-html … --export-svg …` needs. Upstream reaches the same
1713    /// place with `Console(record=True)` plus `save_html(clear=False)`; here the
1714    /// buffer is returned instead of being held on the console, so the caller
1715    /// decides what to do with it and there is no hidden state to clear.
1716    ///
1717    /// Pair with [`segments_to_string`](Self::segments_to_string) to get the
1718    /// terminal form, [`export::export_html_classes`](crate::export::export_html_classes)
1719    /// for HTML, and [`svg::export_svg`](crate::svg::export_svg) for SVG.
1720    ///
1721    /// Rendering twice instead would be wrong, not merely wasteful: a renderable
1722    /// reading standard input only yields its content once.
1723    pub fn record_output(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1724        self.capture_segments(f)
1725    }
1726
1727    /// Run `f` with this thread's output recorded to a fresh buffer, returning
1728    /// the captured segments. Captures nest (each pushes its own buffer), are
1729    /// per thread (upstream's `ConsoleThreadLocals`), and end even if `f`
1730    /// panics (upstream's `Capture.__exit__` always runs).
1731    fn capture_segments(&self, f: impl FnOnce(&Console)) -> Vec<Segment> {
1732        /// Pops this thread's capture buffer on drop, so an unwinding `f`
1733        /// does not leave the console capturing.
1734        struct CaptureGuard<'a>(&'a Console);
1735        impl CaptureGuard<'_> {
1736            fn pop(&self) -> Vec<Segment> {
1737                let id = std::thread::current().id();
1738                let mut captures = self.0.lock_captures();
1739                let Some(stack) = captures.get_mut(&id) else {
1740                    return Vec::new();
1741                };
1742                let captured = stack.pop().unwrap_or_default();
1743                if stack.is_empty() {
1744                    captures.remove(&id);
1745                }
1746                captured
1747            }
1748        }
1749        impl Drop for CaptureGuard<'_> {
1750            fn drop(&mut self) {
1751                self.pop();
1752            }
1753        }
1754        self.lock_captures()
1755            .entry(std::thread::current().id())
1756            .or_default()
1757            .push(Vec::new());
1758        let guard = CaptureGuard(self);
1759        f(self);
1760        let captured = guard.pop();
1761        std::mem::forget(guard);
1762        captured
1763    }
1764
1765    /// Whether what is written is recorded. Port of `Console.record`.
1766    pub fn record(&self) -> bool {
1767        self.record.load(std::sync::atomic::Ordering::SeqCst)
1768    }
1769
1770    /// Start (or stop) recording. Upstream's `Console.record` is a plain
1771    /// attribute.
1772    pub fn set_record(&self, record: bool) {
1773        self.record
1774            .store(record, std::sync::atomic::Ordering::SeqCst);
1775    }
1776
1777    /// The recorded segments, cleared when `clear`; an error when the
1778    /// console is not recording (upstream's assertion).
1779    fn take_recorded(&self, clear: bool) -> crate::errors::Result<Vec<Segment>> {
1780        if !self.record() {
1781            return Err(crate::errors::RichError::Console(
1782                "To export console contents set record=True in the constructor or instance"
1783                    .to_string(),
1784            ));
1785        }
1786        let mut recorded = self.lock_record_buffer();
1787        Ok(if clear {
1788            std::mem::take(&mut *recorded)
1789        } else {
1790            recorded.clone()
1791        })
1792    }
1793
1794    /// The recorded output as text, with escape codes when `styles`, the
1795    /// record cleared when `clear`. Port of `Console.export_text(clear=…,
1796    /// styles=…)` for a console built with
1797    /// [`record`](ConsoleBuilder::record); upstream's default is
1798    /// `clear=True, styles=False`. Without plain text, control codes are
1799    /// left out; with styles they stay, as upstream's.
1800    ///
1801    /// The styled form renders each style in the console's colour system,
1802    /// truecolor without one, as [`export_text_styled`](Self::export_text_styled).
1803    ///
1804    /// # Errors
1805    ///
1806    /// [`RichError::Console`](crate::errors::RichError::Console) when the
1807    /// console is not recording (upstream asserts).
1808    pub fn export_text_recorded(&self, clear: bool, styles: bool) -> crate::errors::Result<String> {
1809        let segments = self.take_recorded(clear)?;
1810        Ok(if styles {
1811            self.segments_to_styled_text(&segments)
1812        } else {
1813            segments_to_plain(&segments)
1814        })
1815    }
1816
1817    /// Write [`export_text_recorded`](Self::export_text_recorded) to `path`.
1818    /// Port of `Console.save_text(path, clear=…, styles=…)`.
1819    pub fn save_text_recorded(
1820        &self,
1821        path: impl AsRef<std::path::Path>,
1822        clear: bool,
1823        styles: bool,
1824    ) -> std::io::Result<()> {
1825        let text = self
1826            .export_text_recorded(clear, styles)
1827            .map_err(std::io::Error::other)?;
1828        std::fs::write(path, text)
1829    }
1830
1831    /// The recorded output as an HTML document. Port of
1832    /// `Console.export_html(theme=…, clear=…, code_format=…,
1833    /// inline_styles=…)` for a console built with
1834    /// [`record`](ConsoleBuilder::record).
1835    ///
1836    /// # Errors
1837    ///
1838    /// [`RichError::Console`](crate::errors::RichError::Console) when the
1839    /// console is not recording, or when `code_format` is not a template
1840    /// this port can fill (see [`export::export_html_with`](crate::export::export_html_with)).
1841    pub fn export_html_recorded(
1842        &self,
1843        options: &ExportHtmlOptions<'_>,
1844    ) -> crate::errors::Result<String> {
1845        let segments = self.take_recorded(options.clear)?;
1846        let theme = options
1847            .theme
1848            .unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
1849        crate::export::export_html_with(
1850            &segments,
1851            theme,
1852            options.code_format,
1853            options.inline_styles,
1854        )
1855        .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1856    }
1857
1858    /// Write [`export_html_recorded`](Self::export_html_recorded) to `path`.
1859    /// Port of `Console.save_html`.
1860    pub fn save_html_recorded(
1861        &self,
1862        path: impl AsRef<std::path::Path>,
1863        options: &ExportHtmlOptions<'_>,
1864    ) -> std::io::Result<()> {
1865        let html = self
1866            .export_html_recorded(options)
1867            .map_err(std::io::Error::other)?;
1868        std::fs::write(path, html)
1869    }
1870
1871    /// The recorded output as an SVG image of a terminal window. Port of
1872    /// `Console.export_svg(title=…, theme=…, clear=…, code_format=…,
1873    /// font_aspect_ratio=…, unique_id=…)` for a console built with
1874    /// [`record`](ConsoleBuilder::record). Control codes are left out, and
1875    /// the image is as wide as the console is now.
1876    ///
1877    /// # Errors
1878    ///
1879    /// As for [`export_html_recorded`](Self::export_html_recorded).
1880    pub fn export_svg_recorded(
1881        &self,
1882        options: &ExportSvgOptions<'_>,
1883    ) -> crate::errors::Result<String> {
1884        let segments: Vec<Segment> = self
1885            .take_recorded(options.clear)?
1886            .into_iter()
1887            .filter(|segment| !segment.control)
1888            .collect();
1889        let theme = options
1890            .theme
1891            .unwrap_or(&crate::terminal_theme::SVG_EXPORT_THEME);
1892        crate::svg::export_svg_with(
1893            &segments,
1894            theme,
1895            options.title,
1896            options.unique_id,
1897            self.width(),
1898            options
1899                .code_format
1900                .unwrap_or(crate::svg::CONSOLE_SVG_FORMAT),
1901            options.font_aspect_ratio,
1902        )
1903        .map_err(|error| crate::errors::RichError::Console(error.to_string()))
1904    }
1905
1906    /// Write [`export_svg_recorded`](Self::export_svg_recorded) to `path`.
1907    /// Port of `Console.save_svg`.
1908    pub fn save_svg_recorded(
1909        &self,
1910        path: impl AsRef<std::path::Path>,
1911        options: &ExportSvgOptions<'_>,
1912    ) -> std::io::Result<()> {
1913        let svg = self
1914            .export_svg_recorded(options)
1915            .map_err(std::io::Error::other)?;
1916        std::fs::write(path, svg)
1917    }
1918
1919    /// The record buffer, recovering it if a panicking print poisoned the
1920    /// lock.
1921    fn lock_record_buffer(&self) -> std::sync::MutexGuard<'_, Vec<Segment>> {
1922        self.record_buffer
1923            .lock()
1924            .unwrap_or_else(std::sync::PoisonError::into_inner)
1925    }
1926
1927    /// Segments as text with each style's escape codes, in the console's
1928    /// colour system (truecolor without one); control codes kept.
1929    fn segments_to_styled_text(&self, segments: &[Segment]) -> String {
1930        let system = self.color_system().unwrap_or(ColorSystem::Truecolor);
1931        segments
1932            .iter()
1933            .map(|segment| match &segment.style {
1934                Some(style) => style.render(&segment.text, Some(system)),
1935                None => segment.text.to_string(),
1936            })
1937            .collect()
1938    }
1939
1940    /// The per-thread capture stacks, recovering them if a panicking print
1941    /// poisoned the lock.
1942    fn lock_captures(&self) -> std::sync::MutexGuard<'_, CaptureStacks> {
1943        self.captures
1944            .lock()
1945            .unwrap_or_else(std::sync::PoisonError::into_inner)
1946    }
1947
1948    /// Parse `content` as console markup, apply registered highlighters, and
1949    /// print it. This is the `console.print("...")` path.
1950    pub fn print_str(&self, content: &str) {
1951        let text = self.build_text(content);
1952        self.print(&text);
1953    }
1954
1955    /// Same as [`Console::print_str`] but returns the ANSI string.
1956    pub fn render_str_to_string(&self, content: &str) -> String {
1957        let text = self.build_text(content);
1958        self.render_to_string(&text)
1959    }
1960
1961    /// Parse `content` as console markup (expanding emoji + applying the active
1962    /// highlighters), returning the styled [`Text`] that `print_str` would print.
1963    /// Exposed so callers can wrap the markup in another renderable.
1964    pub fn build_text(&self, content: &str) -> Text {
1965        // Malformed markup falls back to printing the text as-is. Upstream would
1966        // raise `MarkupError` instead; use `try_build_text` (or `try_print_str`)
1967        // when the markup comes from a user and a mistake should be reported
1968        // rather than rendered. See docs/DIVERGENCES.md §2.
1969        self.try_build_text(content)
1970            .unwrap_or_else(|_| self.decorate(Text::new(self.expand_emoji(content))))
1971    }
1972
1973    /// As [`build_text`](Console::build_text), but returns
1974    /// [`RichError::Markup`](crate::errors::RichError::Markup) for malformed
1975    /// markup instead of falling back to the raw text — upstream's behaviour.
1976    pub fn try_build_text(&self, content: &str) -> crate::errors::Result<Text> {
1977        if !self.markup {
1978            // `Console(markup=False)`: the string is taken literally.
1979            return Ok(self.decorate(Text::new(self.expand_emoji_plain(content))));
1980        }
1981        let markup = self.parse_markup(content, self.emoji)?;
1982
1983        // The highlighter runs on the *markup-stripped* text and its spans go on
1984        // first; the markup spans are appended afterwards. Spans combine in
1985        // order, so this is what makes an explicit tag beat the highlighter —
1986        // `[green]123[/]` is green, not `repr.number` cyan.
1987        //
1988        // Upstream reaches the same result a different way: `Console.render_str`
1989        // highlights a fresh `Text(str(rich_text))` and then calls
1990        // `highlight_text.copy_styles(rich_text)`, whose `_spans.extend` appends
1991        // the markup spans last. Decorating the markup `Text` in place — the
1992        // obvious reading — inverts the precedence.
1993        let mut text = self.decorate(Text::new(markup.plain()));
1994        for span in markup.spans() {
1995            text.push_span(span.clone());
1996        }
1997        Ok(text)
1998    }
1999
2000    /// Convert a plain string to [`Text`] the way a `str` renderable is
2001    /// converted upstream: emoji codes expand (per the console), console
2002    /// markup is parsed, and highlighting runs when `highlight` (or, when
2003    /// `None`, the console default) enables it. Port of `Console.render_str`,
2004    /// which `Table` cells, `Tree` labels and `Columns` items go through.
2005    ///
2006    /// Malformed markup falls back to the literal text, as
2007    /// [`build_text`](Console::build_text) does (docs/DIVERGENCES.md §2).
2008    pub fn render_str(&self, content: &str, highlight: Option<bool>) -> Text {
2009        let highlight = highlight.unwrap_or(self.highlight);
2010        // `markup.render` returns the (emoji-replaced) string untouched when it
2011        // holds no `[`; skip the parser for the common plain cell.
2012        let markup = if !self.markup {
2013            Text::new(self.expand_emoji_plain(content))
2014        } else if content.contains('[') {
2015            self.parse_markup(content, self.emoji)
2016                .unwrap_or_else(|_| Text::new(self.expand_emoji(content)))
2017        } else if content.contains(':') {
2018            Text::new(self.expand_emoji(content))
2019        } else {
2020            Text::new(content)
2021        };
2022        if !highlight {
2023            return markup;
2024        }
2025        // Highlight the plain text, then append the markup spans, as
2026        // `highlight_text.copy_styles(rich_text)` does (see `try_build_text`).
2027        let mut text = self.decorate_with_repr(Text::new(markup.plain()));
2028        for span in markup.spans() {
2029            text.push_span(span.clone());
2030        }
2031        text
2032    }
2033
2034    /// As [`print_str`](Console::print_str), but reports malformed markup.
2035    pub fn try_print_str(&self, content: &str) -> crate::errors::Result<()> {
2036        self.print(&self.try_build_text(content)?);
2037        Ok(())
2038    }
2039
2040    /// As [`print_justified`](Console::print_justified), but reports malformed
2041    /// markup.
2042    pub fn try_print_justified(
2043        &self,
2044        content: &str,
2045        justify: Justify,
2046    ) -> crate::errors::Result<()> {
2047        let text = self.try_build_text(content)?;
2048        let mut options = self.options();
2049        options.justify = justify;
2050        self.emit(text.rich_render(self, &options));
2051        Ok(())
2052    }
2053
2054    /// Expand `:emoji:` shortcodes. Runs before markup parsing (matching
2055    /// upstream's default `emoji=True`); `:name:` and `[tag]` don't overlap.
2056    ///
2057    /// The console's default emoji variant applies only when `content` holds
2058    /// no `[`: upstream's `markup.render` passes `default_variant` on its
2059    /// tag-free fast path only, and replaces the text between tags without it.
2060    pub(crate) fn expand_emoji(&self, content: &str) -> String {
2061        if !self.emoji {
2062            return content.to_string();
2063        }
2064        let variant = if content.contains('[') {
2065            None
2066        } else {
2067            self.emoji_variant
2068        };
2069        crate::emoji::replace_with_variant(content, variant)
2070    }
2071
2072    /// Port of `markup.render(content, emoji=emoji, emoji_variant=…)`: a
2073    /// string with no `[` is only emoji-replaced (with the console's default
2074    /// variant); otherwise emoji codes are replaced chunk by chunk between the
2075    /// tags, so error positions refer to the original string.
2076    fn parse_markup(&self, content: &str, emoji: bool) -> crate::errors::Result<Text> {
2077        if !content.contains('[') {
2078            return Ok(Text::new(if emoji {
2079                crate::emoji::replace_with_variant(content, self.emoji_variant)
2080            } else {
2081                content.to_string()
2082            }));
2083        }
2084        if emoji {
2085            crate::markup::render_emoji(content)
2086        } else {
2087            crate::markup::render(content)
2088        }
2089    }
2090
2091    /// Expand `:emoji:` shortcodes in a string that is not markup, with the
2092    /// console's default variant (upstream's `markup=False` branch of
2093    /// `render_str`).
2094    fn expand_emoji_plain(&self, content: &str) -> String {
2095        if self.emoji {
2096            crate::emoji::replace_with_variant(content, self.emoji_variant)
2097        } else {
2098            content.to_string()
2099        }
2100    }
2101
2102    /// Convert a string to [`Text`] with every keyword upstream's
2103    /// `Console.render_str` takes. Port of `Console.render_str`, strict:
2104    /// malformed markup is an error (`MarkupError`), not the literal text.
2105    ///
2106    /// Emoji, markup and highlighting default to the console's settings when
2107    /// the option is `None`. As upstream, a highlighted result is a fresh
2108    /// `Text` carrying only spans (the highlighter's, then the markup's): the
2109    /// `style`, `justify` and `overflow` are dropped by `copy_styles`.
2110    pub fn render_str_with(
2111        &self,
2112        content: &str,
2113        options: &RenderStrOptions<'_>,
2114    ) -> crate::errors::Result<Text> {
2115        let emoji = options.emoji.unwrap_or(self.emoji);
2116        let markup = options.markup.unwrap_or(self.markup);
2117        let highlight = options.highlight.unwrap_or(self.highlight);
2118
2119        let mut rich_text = if markup {
2120            self.parse_markup(content, emoji)?
2121        } else if emoji {
2122            Text::new(crate::emoji::replace_with_variant(
2123                content,
2124                self.emoji_variant,
2125            ))
2126        } else {
2127            Text::new(content)
2128        };
2129        rich_text.set_base_style(options.style.clone());
2130        rich_text.set_justify(options.justify.unwrap_or_default());
2131        rich_text.set_overflow(options.overflow);
2132
2133        if !highlight {
2134            return Ok(rich_text);
2135        }
2136        let mut text = Text::new(rich_text.plain());
2137        match options.highlighter {
2138            Some(highlighter) => highlighter.highlight(&mut text),
2139            None => text = self.decorate_with_repr(text),
2140        }
2141        for span in rich_text.spans() {
2142            text.push_span(span.clone());
2143        }
2144        Ok(text)
2145    }
2146
2147    /// Whether `:emoji:` codes are replaced by default. Port of
2148    /// `Console(emoji=…)`.
2149    pub fn emoji(&self) -> bool {
2150        self.emoji
2151    }
2152
2153    /// Whether printed strings are highlighted by default. Port of
2154    /// `Console(highlight=…)`.
2155    pub fn highlight(&self) -> bool {
2156        self.highlight
2157    }
2158
2159    /// Whether printed strings are parsed as console markup by default. Port
2160    /// of `Console(markup=…)`.
2161    pub fn markup(&self) -> bool {
2162        self.markup
2163    }
2164
2165    /// The default emoji variant. Port of `Console(emoji_variant=…)`.
2166    pub fn emoji_variant(&self) -> Option<crate::emoji::EmojiVariant> {
2167        self.emoji_variant
2168    }
2169
2170    /// The tab stop width `Text` expands tabs to. Port of `Console.tab_size`.
2171    pub fn tab_size(&self) -> usize {
2172        self.tab_size
2173    }
2174
2175    /// Change the tab stop width. Upstream's `Console.tab_size` is a plain
2176    /// attribute.
2177    pub fn set_tab_size(&mut self, tab_size: usize) {
2178        self.tab_size = tab_size;
2179    }
2180
2181    /// Set the terminal window title. Port of `Console.set_window_title`:
2182    /// only a terminal is sent the code, and the return value says whether it
2183    /// was.
2184    pub fn set_window_title(&self, title: &str) -> bool {
2185        if self.is_terminal {
2186            self.control(&crate::control::Control::title(title));
2187            true
2188        } else {
2189            false
2190        }
2191    }
2192
2193    /// Apply the registered highlighters, plus the built-in `ReprHighlighter`
2194    /// when `highlight` is on.
2195    fn decorate(&self, mut text: Text) -> Text {
2196        for highlighter in &self.highlighters {
2197            highlighter.highlight(&mut text);
2198        }
2199        if self.highlight {
2200            crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2201        }
2202        text
2203    }
2204
2205    /// [`decorate`](Self::decorate) for a caller that has already decided to
2206    /// highlight (upstream's `highlight=True` override of the console default).
2207    fn decorate_with_repr(&self, mut text: Text) -> Text {
2208        for highlighter in &self.highlighters {
2209            highlighter.highlight(&mut text);
2210        }
2211        crate::highlighter::ReprHighlighter::new().highlight(&mut text);
2212        text
2213    }
2214
2215    /// Parse `content` as markup and print it justified to the console width.
2216    /// This is the `console.print("...", justify=...)` path.
2217    pub fn print_justified(&self, content: &str, justify: Justify) {
2218        let text = self.build_text(content);
2219        let mut options = self.options();
2220        options.justify = justify;
2221        let segments = text.rich_render(self, &options);
2222        self.emit(segments);
2223    }
2224
2225    /// Same as [`Console::print_justified`] but returns the ANSI string.
2226    ///
2227    /// The justify is passed via `options.justify`, which — matching upstream —
2228    /// disables the measurement-fit so the text pads to the full width.
2229    pub fn render_justified_to_string(&self, content: &str, justify: Justify) -> String {
2230        let text = self.build_text(content);
2231        let mut options = self.options();
2232        options.justify = justify;
2233        let segments = text.rich_render(self, &options);
2234        self.segments_to_string(&segments)
2235    }
2236
2237    /// Convert rendered segments into a terminal string, applying this console's
2238    /// colour system (and honouring `no_color`). Port of `Console._render_buffer`.
2239    pub fn segments_to_string(&self, segments: &[Segment]) -> String {
2240        let system = self.color_system;
2241        // `if self.no_color and color_system: buffer = Segment.remove_color(…)`:
2242        // colours go, every other attribute stays.
2243        let colorless;
2244        let segments = if self.no_color && system.is_some() {
2245            colorless = Segment::remove_color(segments);
2246            &colorless[..]
2247        } else {
2248            segments
2249        };
2250        let mut out = String::new();
2251        for segment in segments {
2252            // Control codes are meaningless off a terminal — upstream's
2253            // `_render_buffer` drops them when `not is_terminal`.
2254            if segment.control && !self.is_terminal {
2255                continue;
2256            }
2257            match (&segment.style, system) {
2258                (Some(style), Some(sys)) => out.push_str(&style.render(&segment.text, Some(sys))),
2259                _ => out.push_str(&segment.text),
2260            }
2261        }
2262        out
2263    }
2264}
2265
2266/// Join the visible text of a segment stream, dropping control codes. Port of
2267/// `Console.export_text(styles=False)`'s join.
2268fn segments_to_plain(segments: &[Segment]) -> String {
2269    segments
2270        .iter()
2271        .filter(|s| !s.control)
2272        .map(|s| s.text.as_str())
2273        .collect()
2274}
2275
2276/// A string renders as upstream's `Console.render` renders a `str`: through
2277/// [`Console::render_str`] with the options' `highlight` and `markup`
2278/// (`None` taking the console's defaults), then as that `Text`. Malformed
2279/// markup falls back to the literal text, as `render_str` does.
2280impl Renderable for String {
2281    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2282        self.options_text(console, options, options.highlight)
2283            .rich_render(console, options)
2284    }
2285
2286    /// `Measurement.get` of a `str`: `render_str(markup=options.markup,
2287    /// highlight=False)`, then the text's measurement.
2288    fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2289        let text = self.options_text(console, options, Some(false));
2290        crate::measure::Measurement::get(console, options, &text)
2291    }
2292}
2293
2294/// `render_str` for a `str` renderable under `options`.
2295trait OptionsText {
2296    fn options_text(
2297        &self,
2298        console: &Console,
2299        options: &ConsoleOptions,
2300        highlight: Option<bool>,
2301    ) -> Text;
2302}
2303
2304impl OptionsText for String {
2305    fn options_text(
2306        &self,
2307        console: &Console,
2308        options: &ConsoleOptions,
2309        highlight: Option<bool>,
2310    ) -> Text {
2311        let render_options = RenderStrOptions {
2312            highlight,
2313            markup: options.markup,
2314            ..Default::default()
2315        };
2316        console
2317            .render_str_with(self, &render_options)
2318            .unwrap_or_else(|_| {
2319                console
2320                    .render_str_with(
2321                        self,
2322                        &RenderStrOptions {
2323                            markup: Some(false),
2324                            ..render_options
2325                        },
2326                    )
2327                    .unwrap_or_else(|_| Text::new(self.as_str()))
2328            })
2329    }
2330}
2331
2332impl Renderable for Text {
2333    fn printed_text(&self) -> Option<Text> {
2334        // `Text("").join([self])`: the text and its spans survive; justify,
2335        // overflow and no_wrap come from the blank separator (#446). The base
2336        // style does not: `join` takes the separator's (none) and re-applies
2337        // this text's as a leading span (`if text.style: append_span(...)`),
2338        // so print-level justify padding is left unstyled.
2339        let mut text = self.clone();
2340        text.clear_layout_options();
2341        text.base_style_to_span();
2342        Some(text)
2343    }
2344
2345    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
2346        // Empty Text still represents a printable blank line; an empty
2347        // generator such as Markdown does not. Preserve that distinction.
2348        // Wrap to the available width; the effective justify is this text's own
2349        // justify, falling back to the console options' justify.
2350        let justify = self.get_justify_option().unwrap_or(options.justify);
2351        // A justified empty line is still padded to the width (upstream's
2352        // `truncate(width, pad=True)`), so only an unjustified one is bare.
2353        if self.is_empty() && matches!(justify, Justify::Default | Justify::Full) {
2354            return vec![Segment::new("", None)];
2355        }
2356        // Same precedence for overflow and no_wrap: the text's own setting wins,
2357        // then the options', then upstream's default. Mirrors the `self.x or
2358        // options.x or DEFAULT` chain in `Text.__rich_console__`.
2359        let overflow = self
2360            .get_overflow()
2361            .or(options.overflow)
2362            .unwrap_or(Overflow::Fold);
2363        let no_wrap = self.get_no_wrap().or(options.no_wrap).unwrap_or(false);
2364        // `tab_size = console.tab_size if self.tab_size is None else …`.
2365        self.render_joined_wrapped_tabs(
2366            console.theme(),
2367            console.base_style(),
2368            options.max_width,
2369            justify,
2370            overflow,
2371            no_wrap,
2372            self.console_tab_size(console),
2373        )
2374    }
2375
2376    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
2377        let (minimum, maximum) = self.measurement();
2378        crate::measure::Measurement::new(
2379            minimum.min(options.max_width),
2380            maximum.min(options.max_width),
2381        )
2382    }
2383}
2384
2385/// The keyword arguments of upstream's `Console.export_html` and
2386/// `save_html`, for [`Console::export_html_recorded`].
2387#[derive(Clone, Copy)]
2388pub struct ExportHtmlOptions<'a> {
2389    /// `theme=`: the palette, or `None` for
2390    /// [`DEFAULT_TERMINAL_THEME`](crate::terminal_theme::DEFAULT_TERMINAL_THEME).
2391    pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2392    /// `clear=`: empty the record afterwards (default `true`).
2393    pub clear: bool,
2394    /// `code_format=`: the page template, or `None` for
2395    /// [`CONSOLE_HTML_FORMAT`](crate::export::CONSOLE_HTML_FORMAT).
2396    pub code_format: Option<&'a str>,
2397    /// `inline_styles=`: styles on each span rather than in a stylesheet
2398    /// (default `false`).
2399    pub inline_styles: bool,
2400}
2401
2402impl Default for ExportHtmlOptions<'_> {
2403    fn default() -> Self {
2404        ExportHtmlOptions {
2405            theme: None,
2406            clear: true,
2407            code_format: None,
2408            inline_styles: false,
2409        }
2410    }
2411}
2412
2413/// The keyword arguments of upstream's `Console.export_svg` and `save_svg`,
2414/// for [`Console::export_svg_recorded`].
2415#[derive(Clone, Copy)]
2416pub struct ExportSvgOptions<'a> {
2417    /// `title=`: the window title (default `"Rich"`).
2418    pub title: &'a str,
2419    /// `theme=`: the palette, or `None` for
2420    /// [`SVG_EXPORT_THEME`](crate::terminal_theme::SVG_EXPORT_THEME).
2421    pub theme: Option<&'a crate::terminal_theme::TerminalTheme>,
2422    /// `clear=`: empty the record afterwards (default `true`).
2423    pub clear: bool,
2424    /// `code_format=`: the image template, or `None` for
2425    /// [`CONSOLE_SVG_FORMAT`](crate::svg::CONSOLE_SVG_FORMAT).
2426    pub code_format: Option<&'a str>,
2427    /// `font_aspect_ratio=`: the font's width to height (default 0.61).
2428    pub font_aspect_ratio: f64,
2429    /// `unique_id=`: the prefix of the image's ids and classes. Upstream's
2430    /// default hashes Python's `repr()` of the segments, which Rust cannot
2431    /// reproduce ([DIVERGENCES §15]); the default here is `"terminal"`.
2432    ///
2433    /// [DIVERGENCES §15]: https://github.com/buchochelliq-labs/rs-rich-cli/blob/main/docs/DIVERGENCES.md#15-exports-done-html-both-forms--svg-svg-needs-an-explicit-unique_id
2434    pub unique_id: &'a str,
2435}
2436
2437impl Default for ExportSvgOptions<'_> {
2438    fn default() -> Self {
2439        ExportSvgOptions {
2440            title: "Rich",
2441            theme: None,
2442            clear: true,
2443            code_format: None,
2444            font_aspect_ratio: 0.61,
2445            unique_id: "terminal",
2446        }
2447    }
2448}
2449
2450/// Builder for [`Console`], allowing detection to be overridden.
2451pub struct ConsoleBuilder {
2452    force_terminal: Option<bool>,
2453    color_system: Option<ColorSystem>,
2454    color_system_set: bool,
2455    width: Option<usize>,
2456    height: Option<usize>,
2457    no_color: Option<bool>,
2458    get_time: Option<GetTime>,
2459    emoji: Option<bool>,
2460    highlight: Option<bool>,
2461    legacy_windows: Option<bool>,
2462    safe_box: Option<bool>,
2463    ascii_only: Option<bool>,
2464    theme: Option<Theme>,
2465    markup: Option<bool>,
2466    emoji_variant: Option<crate::emoji::EmojiVariant>,
2467    tab_size: Option<usize>,
2468    environ: Option<Environ>,
2469    quiet: bool,
2470    soft_wrap: bool,
2471    style: Option<crate::style::StyleType>,
2472    on_broken_pipe: Option<BrokenPipeHandler>,
2473    record: bool,
2474    log_time: bool,
2475    log_path: bool,
2476    log_time_format: Option<LogTimeFormat>,
2477    get_datetime: Option<GetDateTime>,
2478}
2479
2480impl ConsoleBuilder {
2481    fn new() -> Self {
2482        ConsoleBuilder {
2483            force_terminal: None,
2484            color_system: None,
2485            color_system_set: false,
2486            width: None,
2487            height: None,
2488            no_color: None,
2489            get_time: None,
2490            emoji: None,
2491            highlight: None,
2492            legacy_windows: None,
2493            safe_box: None,
2494            ascii_only: None,
2495            theme: None,
2496            markup: None,
2497            emoji_variant: None,
2498            tab_size: None,
2499            environ: None,
2500            quiet: false,
2501            soft_wrap: false,
2502            style: None,
2503            on_broken_pipe: None,
2504            record: false,
2505            log_time: true,
2506            log_path: true,
2507            log_time_format: None,
2508            get_datetime: None,
2509        }
2510    }
2511
2512    /// Show the time column in [`Console::log`] (default on). Port of
2513    /// `Console(log_time=…)`.
2514    pub fn log_time(mut self, value: bool) -> Self {
2515        self.log_time = value;
2516        self
2517    }
2518
2519    /// Show the caller's file and line in [`Console::log`] (default on).
2520    /// Port of `Console(log_path=…)`.
2521    pub fn log_path(mut self, value: bool) -> Self {
2522        self.log_path = value;
2523        self
2524    }
2525
2526    /// The `strftime` format of the time column in [`Console::log`]
2527    /// (default `"[%X]"`). Port of `Console(log_time_format="…")`; the time
2528    /// is formatted in UTC (see [`get_datetime`](Self::get_datetime)).
2529    pub fn log_time_format(mut self, format: impl Into<String>) -> Self {
2530        self.log_time_format = Some(LogTimeFormat::Strftime(format.into()));
2531        self
2532    }
2533
2534    /// Format the time column of [`Console::log`] with `format`. Port of
2535    /// `Console(log_time_format=callable)`.
2536    pub fn log_time_formatter(
2537        mut self,
2538        format: impl Fn(std::time::SystemTime) -> Text + Send + Sync + 'static,
2539    ) -> Self {
2540        self.log_time_format = Some(LogTimeFormat::Callable(std::sync::Arc::new(format)));
2541        self
2542    }
2543
2544    /// Read the time [`Console::log`] shows from `clock` (default
2545    /// [`SystemTime::now`](std::time::SystemTime::now)). Port of
2546    /// `Console(get_datetime=…)`. Upstream's `datetime.now()` is local
2547    /// time; core has no time zone data, so its `strftime` formats the UTC
2548    /// wall-clock time, and a clock that wants local time adds the offset.
2549    pub fn get_datetime(
2550        mut self,
2551        clock: impl Fn() -> std::time::SystemTime + Send + Sync + 'static,
2552    ) -> Self {
2553        self.get_datetime = Some(std::sync::Arc::new(clock));
2554        self
2555    }
2556
2557    /// Record what is written (default off), for
2558    /// [`Console::export_text_recorded`], [`Console::export_html_recorded`]
2559    /// and [`Console::export_svg_recorded`]. Port of `Console(record=…)`.
2560    pub fn record(mut self, value: bool) -> Self {
2561        self.record = value;
2562        self
2563    }
2564
2565    /// Detect the terminal from `environ` instead of the process
2566    /// environment: `NO_COLOR`, `COLUMNS`, `LINES`, `TERM`, `COLORTERM`,
2567    /// `TTY_COMPATIBLE` and `FORCE_COLOR`. Port of `Console(_environ=…)`,
2568    /// which upstream provides for tests.
2569    pub fn environ(mut self, environ: Environ) -> Self {
2570        self.environ = Some(environ);
2571        self
2572    }
2573
2574    /// Suppress all output (default off). Port of `Console(quiet=…)`.
2575    pub fn quiet(mut self, value: bool) -> Self {
2576        self.quiet = value;
2577        self
2578    }
2579
2580    /// The default of `print(soft_wrap=…)` (default off): no wrapping and
2581    /// no cropping. Port of `Console(soft_wrap=…)`.
2582    pub fn soft_wrap(mut self, value: bool) -> Self {
2583        self.soft_wrap = value;
2584        self
2585    }
2586
2587    /// A style applied to all output (default none). Port of
2588    /// `Console(style=…)`.
2589    pub fn style(mut self, style: impl Into<crate::style::StyleType>) -> Self {
2590        self.style = Some(style.into());
2591        self
2592    }
2593
2594    /// Call `handler` in place of [`Console::on_broken_pipe`]'s default
2595    /// (quiet, then exit with status 1) when a write finds the pipe closed.
2596    /// Upstream overrides `Console.on_broken_pipe` in a subclass.
2597    pub fn on_broken_pipe(mut self, handler: impl Fn(&Console) + Send + Sync + 'static) -> Self {
2598        self.on_broken_pipe = Some(std::sync::Arc::new(handler));
2599        self
2600    }
2601
2602    /// Enable/disable console markup in printed strings (default enabled).
2603    /// Port of `Console(markup=…)`.
2604    pub fn markup(mut self, value: bool) -> Self {
2605        self.markup = Some(value);
2606        self
2607    }
2608
2609    /// The emoji variant appended to codes that name none (default none).
2610    /// Port of `Console(emoji_variant=…)`.
2611    pub fn emoji_variant(mut self, variant: Option<crate::emoji::EmojiVariant>) -> Self {
2612        self.emoji_variant = variant;
2613        self
2614    }
2615
2616    /// The tab stop width `Text` expands tabs to (default 8). Port of
2617    /// `Console(tab_size=…)`.
2618    pub fn tab_size(mut self, tab_size: usize) -> Self {
2619        self.tab_size = Some(tab_size);
2620        self
2621    }
2622
2623    pub fn force_terminal(mut self, value: bool) -> Self {
2624        self.force_terminal = Some(value);
2625        self
2626    }
2627
2628    /// Force legacy-Windows-console behavior (box substitution). Default off.
2629    pub fn legacy_windows(mut self, value: bool) -> Self {
2630        self.legacy_windows = Some(value);
2631        self
2632    }
2633
2634    /// Enable/disable terminal-safe box substitution (default on).
2635    pub fn safe_box(mut self, value: bool) -> Self {
2636        self.safe_box = Some(value);
2637        self
2638    }
2639
2640    /// Force ASCII-only box rendering (default off). Set for non-UTF-8 terminals.
2641    pub fn ascii_only(mut self, value: bool) -> Self {
2642        self.ascii_only = Some(value);
2643        self
2644    }
2645
2646    /// Force a specific color system (use for reproducible output/tests).
2647    pub fn color_system(mut self, system: Option<ColorSystem>) -> Self {
2648        self.color_system = system;
2649        self.color_system_set = true;
2650        self
2651    }
2652
2653    pub fn width(mut self, width: usize) -> Self {
2654        self.width = Some(width);
2655        self
2656    }
2657
2658    /// Set the console height in rows (used by [`Layout`](crate::layout::Layout)).
2659    pub fn height(mut self, height: usize) -> Self {
2660        self.height = Some(height);
2661        self
2662    }
2663
2664    /// Enable no-colour mode: colours are stripped from output while other
2665    /// attributes (bold, underline, …) still render. Unset, a non-empty
2666    /// `NO_COLOR` environment variable enables it. Port of `no_color=`.
2667    pub fn no_color(mut self, value: bool) -> Self {
2668        self.no_color = Some(value);
2669        self
2670    }
2671
2672    /// Read the current time (seconds) from `clock` instead of the monotonic
2673    /// clock. Port of `Console(get_time=…)`; animations such as
2674    /// [`Spinner`](crate::spinner::Spinner) render the frame for this time.
2675    pub fn get_time(mut self, clock: impl Fn() -> f64 + Send + Sync + 'static) -> Self {
2676        self.get_time = Some(std::sync::Arc::new(clock));
2677        self
2678    }
2679
2680    /// Enable/disable `:emoji:` shortcode replacement (default enabled).
2681    pub fn emoji(mut self, value: bool) -> Self {
2682        self.emoji = Some(value);
2683        self
2684    }
2685
2686    /// Enable/disable automatic repr highlighting. Defaults to **on**, matching
2687    /// upstream `Console(highlight=True)`.
2688    pub fn highlight(mut self, value: bool) -> Self {
2689        self.highlight = Some(value);
2690        self
2691    }
2692
2693    pub fn theme(mut self, theme: Theme) -> Self {
2694        self.theme = Some(theme);
2695        self
2696    }
2697
2698    pub fn build(self) -> Console {
2699        let environ = self.environ.map(std::sync::Arc::new);
2700        let env = environ.as_deref();
2701        let legacy_windows = self.legacy_windows.unwrap_or(false);
2702        let is_terminal = self
2703            .force_terminal
2704            .unwrap_or_else(|| detect_is_terminal(env));
2705        // Upstream's rule is `environ.get("NO_COLOR", "") != ""`, so an EMPTY
2706        // NO_COLOR does not disable colour — only a non-empty value does. That
2707        // matters because a shell that exports `NO_COLOR=` (a common way to
2708        // clear it) would otherwise still be treated as opting out.
2709        let no_color = self
2710            .no_color
2711            .unwrap_or_else(|| environ_get(env, "NO_COLOR").is_some_and(|value| !value.is_empty()));
2712        // `color_system="auto"` (the default) detects; an explicit system,
2713        // or `None`, is taken as given.
2714        let color_system = if self.color_system_set {
2715            self.color_system
2716        } else if is_terminal && !term_is_dumb(env) {
2717            Some(detect_color_system(env))
2718        } else {
2719            None
2720        };
2721        // `width = int(columns) - self.legacy_windows`: `COLUMNS` and
2722        // `LINES` become the console's own width and height.
2723        let width = self.width.or_else(|| {
2724            env_int(env, "COLUMNS")
2725                .map(|columns| columns.saturating_sub(usize::from(legacy_windows)))
2726        });
2727        let height = self.height.or_else(|| env_int(env, "LINES"));
2728        let terminal_size = detect_terminal_size(legacy_windows);
2729        Console {
2730            render_environment: None,
2731            region_sink: None,
2732            code_highlighting: None,
2733            color_system,
2734            width,
2735            height,
2736            terminal_size,
2737            environ,
2738            is_terminal,
2739            no_color,
2740            quiet: std::sync::atomic::AtomicBool::new(self.quiet),
2741            soft_wrap: self.soft_wrap,
2742            style: self.style,
2743            on_broken_pipe: self.on_broken_pipe,
2744            get_time: self
2745                .get_time
2746                .unwrap_or_else(|| std::sync::Arc::new(monotonic)),
2747            emoji: self.emoji.unwrap_or(true),
2748            // Upstream's `Console(highlight=True)` default. Getting this wrong is
2749            // invisible in the fixtures (every one is captured with
2750            // highlight=False) but is the first thing a user sees: numbers,
2751            // paths, booleans and URLs come out plain instead of coloured.
2752            highlight: self.highlight.unwrap_or(true),
2753            legacy_windows,
2754            safe_box: self.safe_box.unwrap_or(true),
2755            ascii_only: self.ascii_only.unwrap_or(false),
2756            theme_stack: vec![self.theme.unwrap_or_else(Theme::default_theme)],
2757            base_style: Style::new(),
2758            highlighters: Vec::new(),
2759            markup: self.markup.unwrap_or(true),
2760            emoji_variant: self.emoji_variant,
2761            // `tab_size: int = 8`.
2762            tab_size: self.tab_size.unwrap_or(crate::text::DEFAULT_TAB_SIZE),
2763            captures: std::sync::Mutex::new(CaptureStacks::new()),
2764            is_alt_screen: std::sync::atomic::AtomicBool::new(false),
2765            record: std::sync::atomic::AtomicBool::new(self.record),
2766            record_buffer: std::sync::Mutex::new(Vec::new()),
2767            render_hooks: std::sync::Mutex::new(Vec::new()),
2768            live_stack: LiveStack::default(),
2769            log_render: std::sync::Mutex::new(new_log_render(self.log_time, self.log_path)),
2770            log_time: self.log_time,
2771            log_path: self.log_path,
2772            log_time_format: self
2773                .log_time_format
2774                .unwrap_or_else(|| LogTimeFormat::Strftime("[%X]".to_string())),
2775            get_datetime: self
2776                .get_datetime
2777                .unwrap_or_else(|| std::sync::Arc::new(std::time::SystemTime::now)),
2778        }
2779    }
2780}
2781
2782/// Python's `os.path.abspath`: `path` joined to the working directory
2783/// when it is relative.
2784fn absolute_path(path: &str) -> String {
2785    let path = std::path::Path::new(path);
2786    if path.is_absolute() {
2787        return path.to_string_lossy().into_owned();
2788    }
2789    std::env::current_dir()
2790        .map(|directory| directory.join(path).to_string_lossy().into_owned())
2791        .unwrap_or_else(|_| path.to_string_lossy().into_owned())
2792}
2793
2794/// Upstream's `LogRender(show_time=log_time, show_path=log_path)`.
2795fn new_log_render(log_time: bool, log_path: bool) -> crate::log_render::LogRender {
2796    crate::log_render::LogRender::new()
2797        .show_time(log_time)
2798        .show_path(log_path)
2799}
2800
2801/// Whether `TERM` names a terminal that cannot render styles (`dumb` or
2802/// `unknown`, any case). The environment half of `Console.is_dumb_terminal`.
2803fn term_is_dumb(environ: Option<&Environ>) -> bool {
2804    environ_get(environ, "TERM")
2805        .is_some_and(|term| matches!(term.to_lowercase().as_str(), "dumb" | "unknown"))
2806}
2807
2808/// Whether standard output is a terminal. Port of the `Console.is_terminal`
2809/// property without `force_terminal`: `TTY_COMPATIBLE=0` or `1` decides,
2810/// then a set `FORCE_COLOR` (a terminal unless empty), then `isatty`.
2811fn detect_is_terminal(environ: Option<&Environ>) -> bool {
2812    match environ_get(environ, "TTY_COMPATIBLE").as_deref() {
2813        Some("0") => return false,
2814        Some("1") => return true,
2815        _ => {}
2816    }
2817    if let Some(force_color) = environ_get(environ, "FORCE_COLOR") {
2818        return !force_color.is_empty();
2819    }
2820    std::io::stdout().is_terminal()
2821}
2822
2823/// Upstream's `_TERM_COLORS`: the colour system named by the last
2824/// hyphenated part of `TERM`. Not on Windows, where `TERM` is not read.
2825#[cfg(not(windows))]
2826fn term_colors(colors: &str) -> Option<ColorSystem> {
2827    match colors {
2828        "kitty" | "256color" => Some(ColorSystem::EightBit),
2829        "16color" => Some(ColorSystem::Standard),
2830        _ => None,
2831    }
2832}
2833
2834/// Detect the terminal color system. Port of `Console._detect_color_system`
2835/// for a terminal that is not dumb (the caller checks).
2836///
2837/// `COLORTERM`/`TERM` are the portable signals, but **Windows sets neither**.
2838/// Detecting from them alone meant every Windows console fell back to
2839/// [`ColorSystem::Standard`] — 16 colors — for all output. Measured on a real
2840/// Windows Terminal session: 28 distinct colors in a rendered heat map against
2841/// 140 once truecolor was detected.
2842///
2843/// Upstream `rich` special-cases Windows for the same reason. It reaches the
2844/// platform APIs directly; we ask `anstyle-query`, which avoids hand-written
2845/// `unsafe` FFI for a console handle (see `docs/DIVERGENCES.md`).
2846#[cfg_attr(windows, allow(unused_variables))]
2847fn detect_color_system(environ: Option<&Environ>) -> ColorSystem {
2848    // Windows. This function is only reached when stdout is a terminal (see
2849    // ConsoleBuilder::build), and every modern Windows console that can be a
2850    // terminal speaks 24-bit color, so report truecolor.
2851    //
2852    // The call below is for its SIDE EFFECT — it turns on
2853    // ENABLE_VIRTUAL_TERMINAL_PROCESSING, which legacy `conhost` needs before
2854    // it honours any escape sequence. Its RETURN VALUE is deliberately ignored:
2855    // it enables VT on stdout *and stderr* and propagates failure with `?`, so
2856    // merely redirecting stderr (`rich ... 2>log`, the most natural CI
2857    // invocation) made it report failure and dropped the whole console to 16
2858    // colors — even though stdout was still a fully capable terminal.
2859    #[cfg(windows)]
2860    {
2861        let _ = anstyle_query::windows::enable_ansi_colors();
2862        ColorSystem::Truecolor
2863    }
2864
2865    // `TERM` is meaningless on Windows and the branch above always returns, so
2866    // gating this keeps either platform free of unreachable code.
2867    #[cfg(not(windows))]
2868    {
2869        let color_term = environ_get(environ, "COLORTERM")
2870            .unwrap_or_default()
2871            .trim()
2872            .to_lowercase();
2873        if matches!(color_term.as_str(), "truecolor" | "24bit") {
2874            return ColorSystem::Truecolor;
2875        }
2876        let term = environ_get(environ, "TERM")
2877            .unwrap_or_default()
2878            .trim()
2879            .to_lowercase();
2880        // `term.rpartition("-")[2]`: the whole name when it has no hyphen.
2881        let colors = term.rsplit('-').next().unwrap_or("");
2882        term_colors(colors).unwrap_or(ColorSystem::Standard)
2883    }
2884}
2885
2886/// The terminal's size, 80x25 where it reports none (or zero), the width
2887/// one less on a legacy Windows console. The `os.get_terminal_size` half of
2888/// upstream's `Console.size`.
2889fn detect_terminal_size(legacy_windows: bool) -> ConsoleDimensions {
2890    let (width, height) = match terminal_size::terminal_size() {
2891        Some((terminal_size::Width(w), terminal_size::Height(h))) => (w as usize, h as usize),
2892        None => (0, 0),
2893    };
2894    let width = if width == 0 { DEFAULT_WIDTH } else { width };
2895    let height = if height == 0 { DEFAULT_HEIGHT } else { height };
2896    ConsoleDimensions {
2897        width: width - usize::from(legacy_windows),
2898        height,
2899    }
2900}
2901
2902impl crate::protocol::ConsoleCodeHighlighting for Console {
2903    fn set_code_highlighting(&mut self, value: Option<crate::protocol::CodeHighlighting>) {
2904        self.code_highlighting = value;
2905    }
2906    fn code_highlighting(&self) -> Option<&crate::protocol::CodeHighlighting> {
2907        self.code_highlighting.as_ref()
2908    }
2909}
2910
2911impl crate::protocol::ConsoleRegions for Console {
2912    fn set_region_sink(&mut self, value: Option<std::sync::Arc<dyn crate::protocol::RegionSink>>) {
2913        self.region_sink = value;
2914    }
2915    fn region_sink(&self) -> Option<&dyn crate::protocol::RegionSink> {
2916        self.region_sink.as_deref()
2917    }
2918}
2919
2920impl crate::protocol::ConsoleEnvironment for Console {
2921    fn set_render_environment(
2922        &mut self,
2923        value: Option<std::sync::Arc<dyn crate::protocol::RenderEnvironment>>,
2924    ) {
2925        self.render_environment = value;
2926    }
2927    fn render_environment(&self) -> Option<&dyn crate::protocol::RenderEnvironment> {
2928        self.render_environment.as_deref()
2929    }
2930}
2931
2932/// One object passed to [`Console::print_objects`]: a string (rendered as
2933/// console markup), a `Text`, or any other renderable.
2934pub enum PrintObject<'a> {
2935    /// A string, through `render_str` with the call's `emoji`, `markup` and
2936    /// `highlight`.
2937    Str(&'a str),
2938    /// A `Text`, joined with the strings around it.
2939    Text(&'a Text),
2940    /// Any other renderable, which stands alone.
2941    Renderable(&'a dyn Renderable),
2942}
2943
2944/// The keyword arguments of upstream's `Console.print`, after defaults.
2945#[derive(Clone)]
2946pub struct PrintOptions<'a> {
2947    pub sep: &'a str,
2948    pub end: &'a str,
2949    pub style: Option<crate::style::StyleType>,
2950    pub justify: Option<Justify>,
2951    pub overflow: Option<Overflow>,
2952    pub no_wrap: Option<bool>,
2953    pub emoji: Option<bool>,
2954    pub markup: Option<bool>,
2955    pub highlight: Option<bool>,
2956    pub width: Option<usize>,
2957    pub height: Option<usize>,
2958    pub crop: bool,
2959    pub soft_wrap: Option<bool>,
2960    pub new_line_start: bool,
2961}
2962
2963impl Default for PrintOptions<'_> {
2964    fn default() -> Self {
2965        PrintOptions {
2966            sep: " ",
2967            end: "\n",
2968            style: None,
2969            justify: None,
2970            overflow: None,
2971            no_wrap: None,
2972            emoji: None,
2973            markup: None,
2974            highlight: None,
2975            width: None,
2976            height: None,
2977            crop: true,
2978            soft_wrap: None,
2979            new_line_start: false,
2980        }
2981    }
2982}
2983
2984/// One object passed to [`Console::log`]: a string (rendered as console
2985/// markup), a `Text`, or any other renderable. A log keeps its renderables
2986/// in the message column of a table, so they are shared rather than
2987/// borrowed.
2988#[derive(Clone)]
2989pub enum LogObject<'a> {
2990    /// A string, through `render_str` with the call's `emoji`, `markup` and
2991    /// `highlight`.
2992    Str(&'a str),
2993    /// A `Text`, joined with the strings around it.
2994    Text(&'a Text),
2995    /// Any other renderable, which stands alone.
2996    Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
2997}
2998
2999/// The keyword arguments of upstream's `Console.log`, after defaults.
3000/// `log_locals` is not ported ([DIVERGENCES §43]).
3001///
3002/// [DIVERGENCES §43]: https://github.com/buchochelliq-labs/rs-rich-cli/blob/main/docs/DIVERGENCES.md#43-console-features-tied-to-python-objects-or-jupyter
3003#[derive(Clone)]
3004pub struct LogOptions<'a> {
3005    pub sep: &'a str,
3006    pub end: &'a str,
3007    pub style: Option<crate::style::StyleType>,
3008    pub justify: Option<Justify>,
3009    pub emoji: Option<bool>,
3010    pub markup: Option<bool>,
3011    pub highlight: Option<bool>,
3012}
3013
3014impl Default for LogOptions<'_> {
3015    fn default() -> Self {
3016        LogOptions {
3017            sep: " ",
3018            end: "\n",
3019            style: None,
3020            justify: None,
3021            emoji: None,
3022            markup: None,
3023            highlight: None,
3024        }
3025    }
3026}
3027
3028/// A [`Console::log`] call that owns its arguments, so that it can be sent
3029/// to the thread that owns the console (a live display's refresh thread).
3030pub(crate) struct OwnedLog {
3031    objects: Vec<OwnedLogObject>,
3032    sep: String,
3033    end: String,
3034    style: Option<crate::style::StyleType>,
3035    justify: Option<Justify>,
3036    emoji: Option<bool>,
3037    markup: Option<bool>,
3038    highlight: Option<bool>,
3039    file: String,
3040    line: u32,
3041}
3042
3043enum OwnedLogObject {
3044    Str(String),
3045    Text(Text),
3046    Renderable(std::sync::Arc<dyn Renderable + Send + Sync>),
3047}
3048
3049impl OwnedLog {
3050    /// Copy `objects` and `options`, logged as from `file` and `line`.
3051    pub(crate) fn new(
3052        objects: &[LogObject<'_>],
3053        options: &LogOptions<'_>,
3054        file: &str,
3055        line: u32,
3056    ) -> Self {
3057        OwnedLog {
3058            objects: objects
3059                .iter()
3060                .map(|object| match object {
3061                    LogObject::Str(content) => OwnedLogObject::Str((*content).to_string()),
3062                    LogObject::Text(text) => OwnedLogObject::Text((*text).clone()),
3063                    LogObject::Renderable(renderable) => {
3064                        OwnedLogObject::Renderable(renderable.clone())
3065                    }
3066                })
3067                .collect(),
3068            sep: options.sep.to_string(),
3069            end: options.end.to_string(),
3070            style: options.style.clone(),
3071            justify: options.justify,
3072            emoji: options.emoji,
3073            markup: options.markup,
3074            highlight: options.highlight,
3075            file: file.to_string(),
3076            line,
3077        }
3078    }
3079
3080    /// [`Console::log_at`] on `console`.
3081    pub(crate) fn log(&self, console: &Console) -> crate::errors::Result<()> {
3082        let objects: Vec<LogObject<'_>> = self
3083            .objects
3084            .iter()
3085            .map(|object| match object {
3086                OwnedLogObject::Str(content) => LogObject::Str(content),
3087                OwnedLogObject::Text(text) => LogObject::Text(text),
3088                OwnedLogObject::Renderable(renderable) => LogObject::Renderable(renderable.clone()),
3089            })
3090            .collect();
3091        let options = LogOptions {
3092            sep: &self.sep,
3093            end: &self.end,
3094            style: self.style.clone(),
3095            justify: self.justify,
3096            emoji: self.emoji,
3097            markup: self.markup,
3098            highlight: self.highlight,
3099        };
3100        console.log_at(&objects, &options, &self.file, self.line)
3101    }
3102}
3103
3104/// One renderable `_collect_renderables` hands [`Console::log`], with the
3105/// wrappers upstream puts round it: `Align` for `justify`, `Styled` for the
3106/// console's style and then for the call's. A joined text carries its `end`.
3107struct LogItem {
3108    renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3109    align: Option<crate::align::HorizontalAlign>,
3110    /// The text's `end` after its lines, less the trailing line break this
3111    /// port's lines do not end with. What runs past the column's width is
3112    /// cropped, as upstream's.
3113    end: Option<String>,
3114    styles: Vec<Style>,
3115}
3116
3117impl Renderable for LogItem {
3118    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
3119        let mut segments = match self.align {
3120            Some(align) => {
3121                crate::align::Align::render_child(&*self.renderable, align, console, options)
3122            }
3123            None => self.renderable.rich_render(console, options),
3124        };
3125        if let Some(end) = self.end.as_deref().filter(|end| !end.is_empty()) {
3126            segments.push(Segment::new(end, None));
3127            if end.ends_with('\n') {
3128                // A line break ending the text still ends a (blank) line.
3129                segments.push(Segment::new("", None));
3130            }
3131        }
3132        for style in &self.styles {
3133            segments = Segment::apply_style_with(&segments, Some(style), None);
3134        }
3135        segments
3136    }
3137
3138    fn measure(&self, console: &Console, options: &ConsoleOptions) -> crate::measure::Measurement {
3139        crate::measure::Measurement::get(console, options, &*self.renderable)
3140    }
3141}
3142
3143/// The upstream `Console` methods built on its print pipeline.
3144impl Console {
3145    /// Log `objects` with the time and the caller's file and line. Port of
3146    /// `Console.log(*objects, sep=…, end=…, style=…, justify=…, emoji=…,
3147    /// markup=…, highlight=…)`: the objects are joined as for `print` and
3148    /// laid out by upstream's `LogRender` table, with the time (blank when
3149    /// it repeats the last one) and `file:line` linked to the file.
3150    ///
3151    /// The caller is the code calling `log`, found with `#[track_caller]`
3152    /// (upstream inspects the stack). [`log_at`](Self::log_at) names it.
3153    #[track_caller]
3154    pub fn log(
3155        &self,
3156        objects: &[LogObject<'_>],
3157        options: &LogOptions<'_>,
3158    ) -> crate::errors::Result<()> {
3159        let caller = std::panic::Location::caller();
3160        self.log_at(objects, options, caller.file(), caller.line())
3161    }
3162
3163    /// [`log`](Self::log) with the caller's `file` and `line` given:
3164    /// upstream's `_caller_frame_info`. The path column shows the file's
3165    /// name, linked to its absolute path unless it starts with `<`.
3166    pub fn log_at(
3167        &self,
3168        objects: &[LogObject<'_>],
3169        options: &LogOptions<'_>,
3170        file: &str,
3171        line: u32,
3172    ) -> crate::errors::Result<()> {
3173        let mut styles = Vec::new();
3174        if let Some(style) = &self.style {
3175            styles.push(self.get_style(style)?);
3176        }
3177        if let Some(style) = &options.style {
3178            styles.push(self.get_style(style)?);
3179        }
3180        styles.retain(|style| !style.is_null());
3181        let align = match options.justify {
3182            Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3183            Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3184            Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3185            _ => None,
3186        };
3187        let item = |renderable: std::sync::Arc<dyn Renderable + Send + Sync>,
3188                    end: Option<String>|
3189         -> std::sync::Arc<dyn Renderable + Send + Sync> {
3190            std::sync::Arc::new(LogItem {
3191                renderable,
3192                align,
3193                end,
3194                styles: styles.clone(),
3195            })
3196        };
3197        // Upstream's lines end with a break where this port's are joined by
3198        // one (`Renderables`), so the end loses one trailing break.
3199        let end = options
3200            .end
3201            .strip_suffix('\n')
3202            .unwrap_or(options.end)
3203            .to_string();
3204        let mut renderables: Vec<std::sync::Arc<dyn Renderable + Send + Sync>> = Vec::new();
3205        if objects.is_empty() {
3206            // `objects = (NewLine(),)`: one blank line.
3207            renderables.push(item(std::sync::Arc::new(Text::new("")), None));
3208        }
3209        let mut texts: Vec<Text> = Vec::new();
3210        let flush =
3211            |texts: &mut Vec<Text>,
3212             renderables: &mut Vec<std::sync::Arc<dyn Renderable + Send + Sync>>| {
3213                if !texts.is_empty() {
3214                    let mut separator = Text::new(options.sep);
3215                    separator.set_justify_option(options.justify);
3216                    let joined = separator.join(texts);
3217                    renderables.push(item(std::sync::Arc::new(joined), Some(end.clone())));
3218                    texts.clear();
3219                }
3220            };
3221        for object in objects {
3222            match object {
3223                LogObject::Str(content) => {
3224                    let render_str = RenderStrOptions {
3225                        emoji: options.emoji,
3226                        markup: options.markup,
3227                        highlight: options.highlight,
3228                        ..Default::default()
3229                    };
3230                    texts.push(self.render_str_with(content, &render_str)?);
3231                }
3232                LogObject::Text(text) => texts.push((*text).clone()),
3233                LogObject::Renderable(renderable) => {
3234                    flush(&mut texts, &mut renderables);
3235                    renderables.push(item(renderable.clone(), None));
3236                }
3237            }
3238        }
3239        flush(&mut texts, &mut renderables);
3240
3241        let time = self.log_time.then(|| {
3242            let now = (self.get_datetime)();
3243            match &self.log_time_format {
3244                LogTimeFormat::Strftime(format) => {
3245                    Text::new(crate::log_render::strftime(now, format))
3246                }
3247                LogTimeFormat::Callable(format) => format(now),
3248            }
3249        });
3250        // `path = filename.rpartition(os.sep)[-1]`, and the link is the
3251        // absolute path unless the name is a pseudo-file such as `<stdin>`.
3252        let path = file
3253            .rsplit(std::path::MAIN_SEPARATOR)
3254            .next()
3255            .unwrap_or(file);
3256        let link_path = (!file.starts_with('<')).then(|| absolute_path(file));
3257        let table = self
3258            .log_render
3259            .lock()
3260            .unwrap_or_else(std::sync::PoisonError::into_inner)
3261            .render_renderables(
3262                self,
3263                renderables,
3264                time,
3265                Text::new(""),
3266                Some(path),
3267                Some(line),
3268                link_path.as_deref(),
3269            );
3270        let mut segments = self.render(&table, None);
3271        if !segments.is_empty() {
3272            segments.push(Segment::line());
3273        }
3274        let segments = self
3275            .process_render_hooks(vec![segments], &self.options())
3276            .concat();
3277        let segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3278            .into_iter()
3279            .flatten()
3280            .collect();
3281        self.emit_end(segments, false);
3282        Ok(())
3283    }
3284
3285    /// Print `objects` as upstream's `Console.print(*objects, sep=…, end=…,
3286    /// style=…, justify=…, overflow=…, no_wrap=…, emoji=…, markup=…,
3287    /// highlight=…, width=…, height=…, crop=…, soft_wrap=…,
3288    /// new_line_start=…)` does. Strings and texts in a row join into one
3289    /// `Text` with `sep` and end with `end`; other renderables stand alone.
3290    /// Malformed markup is an error, as upstream's `MarkupError`.
3291    pub fn print_objects(
3292        &self,
3293        objects: &[PrintObject<'_>],
3294        options: &PrintOptions<'_>,
3295    ) -> crate::errors::Result<()> {
3296        let segments = self.render_objects(objects, options)?;
3297        self.emit_end(segments, false);
3298        Ok(())
3299    }
3300
3301    /// The segments [`print_objects`](Self::print_objects) writes, ending
3302    /// as upstream's do (a trailing newline included).
3303    pub fn render_objects(
3304        &self,
3305        objects: &[PrintObject<'_>],
3306        options: &PrintOptions<'_>,
3307    ) -> crate::errors::Result<Vec<Segment>> {
3308        let (mut no_wrap, mut overflow, mut crop) =
3309            (options.no_wrap, options.overflow, options.crop);
3310        // `soft_wrap=True`: no wrapping, nothing cropped. `None` takes the
3311        // console's default.
3312        if options.soft_wrap.unwrap_or(self.soft_wrap) {
3313            no_wrap = no_wrap.or(Some(true));
3314            overflow = overflow.or(Some(Overflow::Ignore));
3315            crop = false;
3316        }
3317        let align = match options.justify {
3318            Some(Justify::Left) => Some(crate::align::HorizontalAlign::Left),
3319            Some(Justify::Center) => Some(crate::align::HorizontalAlign::Center),
3320            Some(Justify::Right) => Some(crate::align::HorizontalAlign::Right),
3321            _ => None,
3322        };
3323        let mut render_options = self.options();
3324        if let Some(justify) = options.justify {
3325            render_options.justify = justify;
3326        }
3327        render_options.overflow = overflow;
3328        render_options.no_wrap = no_wrap;
3329        render_options.markup = options.markup;
3330        render_options.highlight = options.highlight;
3331        if let Some(width) = options.width {
3332            render_options = render_options.update_width(width.min(self.width()));
3333        }
3334        if options.height.is_some() {
3335            render_options.height = options.height;
3336        }
3337        let style = match &options.style {
3338            Some(style) => Some(self.get_style(style)?),
3339            None => None,
3340        };
3341        // `Console(style=…)`: `_collect_renderables` wraps every renderable
3342        // in `Styled`, its line ends included.
3343        let console_style = match &self.style {
3344            Some(style) => Some(self.get_style(style)?).filter(|style| !style.is_null()),
3345            None => None,
3346        };
3347
3348        // Each renderable `_collect_renderables` gives, rendered with its end.
3349        let mut items: Vec<Vec<Segment>> = Vec::new();
3350        let mut emit = |renderable: &dyn Renderable, end: Option<&str>| {
3351            let mut rendered = if render_options.max_width < 1 {
3352                Vec::new()
3353            } else {
3354                match align {
3355                    Some(align) => {
3356                        crate::align::Align::render_child(renderable, align, self, &render_options)
3357                    }
3358                    None => renderable.rich_render(self, &render_options),
3359                }
3360            };
3361            // This port's renderables separate their lines; upstream's end
3362            // each one, a text with its `end`.
3363            match end {
3364                Some(end) if !end.is_empty() => rendered.push(Segment::new(end, None)),
3365                Some(_) => {}
3366                // A renderable that ends without a line break upstream
3367                // (`Screen`, `LiveRender`, a rule with an open `end`) keeps
3368                // its last line open, as `print` does.
3369                None if !rendered.is_empty()
3370                    && Self::print_ends_line(renderable, &render_options) =>
3371                {
3372                    rendered.push(Segment::line())
3373                }
3374                None => {}
3375            }
3376            if let Some(console_style) = &console_style {
3377                rendered = Segment::apply_style_with(&rendered, Some(console_style), None);
3378            }
3379            items.push(rendered);
3380        };
3381
3382        // An empty print is a new line (`NewLine()`); with another `end`, an
3383        // empty string.
3384        let blank = [PrintObject::Str("")];
3385        let objects = if objects.is_empty() && options.end != "\n" {
3386            &blank[..]
3387        } else {
3388            objects
3389        };
3390        if objects.is_empty() {
3391            emit(&Text::new(""), Some("\n"));
3392        }
3393        let mut texts: Vec<Text> = Vec::new();
3394        let flush = |texts: &mut Vec<Text>, emit: &mut dyn FnMut(&dyn Renderable, Option<&str>)| {
3395            if !texts.is_empty() {
3396                let mut separator = Text::new(options.sep);
3397                separator.set_justify_option(options.justify);
3398                emit(&separator.join(texts), Some(options.end));
3399                texts.clear();
3400            }
3401        };
3402        for object in objects {
3403            match object {
3404                PrintObject::Str(content) => {
3405                    let render_str = RenderStrOptions {
3406                        emoji: options.emoji,
3407                        markup: options.markup,
3408                        highlight: options.highlight,
3409                        ..Default::default()
3410                    };
3411                    texts.push(self.render_str_with(content, &render_str)?);
3412                }
3413                PrintObject::Text(text) => texts.push((*text).clone()),
3414                PrintObject::Renderable(renderable) => {
3415                    flush(&mut texts, &mut emit);
3416                    emit(*renderable, None);
3417                }
3418            }
3419        }
3420        flush(&mut texts, &mut emit);
3421
3422        let mut segments: Vec<Segment> = Vec::new();
3423        for mut rendered in self.process_render_hooks(items, &render_options) {
3424            // The style goes on each line, never on the newlines.
3425            if let Some(style) = &style {
3426                let mut styled = Vec::with_capacity(rendered.len());
3427                for (line, terminated) in Segment::split_lines_terminator(&rendered) {
3428                    styled.extend(Segment::apply_style_with(&line, Some(style), None));
3429                    if terminated {
3430                        styled.push(Segment::line());
3431                    }
3432                }
3433                rendered = styled;
3434            }
3435            segments.extend(rendered);
3436        }
3437
3438        if options.new_line_start {
3439            let plain: String = segments
3440                .iter()
3441                .map(|segment| segment.text.as_str())
3442                .collect();
3443            if plain.lines().count() > 1 {
3444                segments.insert(0, Segment::line());
3445            }
3446        }
3447        if crop {
3448            segments = Segment::split_and_crop_lines(&segments, self.width(), None, false, true)
3449                .into_iter()
3450                .flatten()
3451                .collect();
3452        }
3453        Ok(segments)
3454    }
3455
3456    /// Write `count` blank lines. Port of `Console.line`.
3457    pub fn line(&self, count: usize) {
3458        if count > 0 {
3459            self.emit_end(vec![Segment::new("\n".repeat(count), None)], false);
3460        }
3461    }
3462
3463    /// Draw a rule with an optional title. Port of `Console.rule(title,
3464    /// characters=…, style=…, align=…)`.
3465    pub fn rule(
3466        &self,
3467        title: &str,
3468        characters: &str,
3469        style: impl Into<crate::style::StyleType>,
3470        align: crate::align::HorizontalAlign,
3471    ) {
3472        let rule = crate::rule::Rule::new(title)
3473            .characters(characters)
3474            .style(style)
3475            .align(align);
3476        self.print(&rule);
3477    }
3478
3479    /// Write `objects` joined by `sep` with no markup, emoji, wrapping or
3480    /// cropping. Port of `Console.out`.
3481    pub fn out(
3482        &self,
3483        objects: &[&str],
3484        sep: &str,
3485        end: &str,
3486        style: Option<crate::style::StyleType>,
3487        highlight: Option<bool>,
3488    ) -> crate::errors::Result<()> {
3489        let raw = objects.join(sep);
3490        self.print_objects(
3491            &[PrintObject::Str(&raw)],
3492            &PrintOptions {
3493                end,
3494                style,
3495                highlight,
3496                emoji: Some(false),
3497                markup: Some(false),
3498                no_wrap: Some(true),
3499                overflow: Some(Overflow::Ignore),
3500                crop: false,
3501                ..Default::default()
3502            },
3503        )
3504    }
3505
3506    /// Pretty print a JSON string. Port of `Console.print_json`: a
3507    /// [`Json`](crate::json::Json) printed with `soft_wrap=True`.
3508    pub fn print_json(
3509        &self,
3510        json: &str,
3511        options: &crate::json::JsonOptions,
3512    ) -> crate::errors::Result<()> {
3513        let renderable = crate::json::Json::with_options(json, options)?;
3514        self.print_objects(
3515            &[PrintObject::Renderable(&renderable)],
3516            &PrintOptions {
3517                soft_wrap: Some(true),
3518                ..Default::default()
3519            },
3520        )
3521    }
3522
3523    /// The width `renderable` needs under `options` (the console's when
3524    /// `None`). Port of `Console.measure`.
3525    pub fn measure(
3526        &self,
3527        renderable: &dyn Renderable,
3528        options: Option<&ConsoleOptions>,
3529    ) -> crate::measure::Measurement {
3530        let default_options;
3531        let options = match options {
3532            Some(options) => options,
3533            None => {
3534                default_options = self.options();
3535                &default_options
3536            }
3537        };
3538        crate::measure::Measurement::get(self, options, renderable)
3539    }
3540}
3541
3542#[cfg(test)]
3543mod tests {
3544    use super::*;
3545
3546    fn test_console() -> Console {
3547        Console::builder()
3548            .force_terminal(true)
3549            .color_system(Some(ColorSystem::Truecolor))
3550            .width(80)
3551            .no_color(false)
3552            .build()
3553    }
3554
3555    /// The strict path reports malformed markup where the lenient one prints it
3556    /// literally. Both must still agree on markup that is actually valid.
3557    #[test]
3558    fn empty_text_and_empty_renderables_have_distinct_endings() {
3559        let console = Console::builder().force_terminal(false).build();
3560        assert_eq!(console.render_export(&Text::new("")), "\n");
3561        #[cfg(feature = "markdown")]
3562        assert_eq!(
3563            console.render_export(&crate::markdown::Markdown::new("")),
3564            ""
3565        );
3566        assert_eq!(console.render_export(&crate::table::Table::new()), "\n");
3567    }
3568
3569    #[test]
3570    fn try_build_text_reports_bad_markup() {
3571        let console = test_console();
3572
3573        let err = console
3574            .try_build_text("[/nope]")
3575            .expect_err("an unmatched closing tag must be an error");
3576        assert!(
3577            matches!(err, crate::errors::RichError::Markup(_)),
3578            "{err:?}"
3579        );
3580        // The lenient path swallows it and prints the source text as-is.
3581        assert_eq!(console.build_text("[/nope]").plain(), "[/nope]");
3582
3583        let strict = console.try_build_text("[bold]hi[/]").expect("valid markup");
3584        assert_eq!(strict.plain(), "hi");
3585        assert_eq!(
3586            strict.spans().len(),
3587            console.build_text("[bold]hi[/]").spans().len()
3588        );
3589    }
3590
3591    /// An unknown tag *name* is not an error — it renders as a no-op, tag
3592    /// consumed. Only genuine syntax errors fail.
3593    ///
3594    /// Verified against real rich 15.0.0: `Console().print("[nope]x[/]")` writes
3595    /// `x`, while `[bold]a[/italic]` raises `MarkupError`. Before names were
3596    /// carried on spans, the port resolved `nope` eagerly, failed, and fell back
3597    /// to printing the markup source literally.
3598    #[test]
3599    fn unknown_tag_names_render_as_no_ops() {
3600        let console = test_console();
3601        let text = console
3602            .try_build_text("[nope]x[/]")
3603            .expect("an unknown tag name is not a syntax error");
3604        assert_eq!(console.render_to_string(&text), "x");
3605        assert_eq!(
3606            console.render_to_string(&console.build_text("[a.b.c]x[/]")),
3607            "x"
3608        );
3609
3610        // A mismatched closing tag is still an error, on both paths.
3611        assert!(console.try_build_text("[bold]a[/italic]").is_err());
3612        assert!(console.try_build_text("[/nope]").is_err());
3613    }
3614
3615    /// Markup styles bind to the theme of the console that renders the text, not
3616    /// the one that parsed it. Verified against real rich 15.0.0.
3617    #[test]
3618    fn markup_styles_bind_at_render_not_at_parse() {
3619        let themed = |definition: &str| {
3620            let mut theme = Theme::default_theme();
3621            theme.insert("accent", Style::parse(definition).unwrap());
3622            Console::builder()
3623                .force_terminal(true)
3624                .color_system(Some(ColorSystem::Truecolor))
3625                .width(80)
3626                .no_color(false)
3627                .theme(theme)
3628                .build()
3629        };
3630        let red = themed("bold red");
3631        let green = themed("underline green");
3632
3633        // Built once, by the red console...
3634        let text = red.build_text("[accent]hi[/]");
3635        assert_eq!(red.render_to_string(&text), "\x1b[1;31mhi\x1b[0m");
3636        // ...and the green console still renders it in green.
3637        assert_eq!(green.render_to_string(&text), "\x1b[4;32mhi\x1b[0m");
3638    }
3639
3640    /// Emoji expansion and the highlighters have to run on both paths, or the
3641    /// strict variant would quietly render differently from the lenient one.
3642    #[test]
3643    fn try_build_text_expands_emoji_like_build_text() {
3644        let console = test_console();
3645        assert_eq!(
3646            console
3647                .try_build_text(":rocket: go")
3648                .expect("valid")
3649                .plain(),
3650            console.build_text(":rocket: go").plain()
3651        );
3652    }
3653
3654    #[test]
3655    fn renders_markup_string() {
3656        let console = test_console();
3657        assert_eq!(
3658            console.render_str_to_string("[bold red]hi[/]"),
3659            "\x1b[1;31mhi\x1b[0m"
3660        );
3661    }
3662
3663    #[test]
3664    fn print_justify_pads_to_width() {
3665        let console = Console::builder()
3666            .force_terminal(true)
3667            .color_system(Some(ColorSystem::Truecolor))
3668            .width(10)
3669            .build();
3670        // Captured from real rich 15.0.0: console.print("hi", justify=...).
3671        assert_eq!(
3672            console.render_justified_to_string("hi", Justify::Left),
3673            "hi        "
3674        );
3675        assert_eq!(
3676            console.render_justified_to_string("hi", Justify::Center),
3677            "    hi    "
3678        );
3679        assert_eq!(
3680            console.render_justified_to_string("hi", Justify::Right),
3681            "        hi"
3682        );
3683    }
3684
3685    #[test]
3686    fn capture_records_ansi_instead_of_stdout() {
3687        let console = Console::builder()
3688            .force_terminal(true)
3689            .color_system(Some(ColorSystem::Truecolor))
3690            .width(20)
3691            .build();
3692        // Captured from real rich 15.0.0 (Console.capture()).
3693        let out = console.capture(|c| c.print_str("[bold red]hi[/] there"));
3694        assert_eq!(out, "\x1b[1;31mhi\x1b[0m there\n");
3695    }
3696
3697    #[test]
3698    fn themed_exports_use_the_given_palette() {
3699        use crate::terminal_theme::{MONOKAI, NIGHT_OWLISH};
3700
3701        let console = Console::builder()
3702            .force_terminal(true)
3703            .color_system(Some(ColorSystem::Truecolor))
3704            .width(20)
3705            .no_color(false)
3706            .build();
3707        let render = |c: &Console| c.print_str("hi");
3708
3709        // Monokai's background is #0c0c0c and Night Owlish's is #ffffff, so the
3710        // chosen theme has to show up in the emitted CSS.
3711        let monokai = console.export_html_themed(&MONOKAI, render);
3712        assert!(
3713            monokai.contains("#0c0c0c"),
3714            "monokai bg missing:\n{monokai}"
3715        );
3716
3717        let owlish = console.export_html_themed(&NIGHT_OWLISH, render);
3718        assert!(owlish.contains("#ffffff"), "owlish bg missing:\n{owlish}");
3719        assert!(!owlish.contains("#0c0c0c"), "leaked monokai into owlish");
3720
3721        // The class form and SVG take a theme too.
3722        let classes = console.export_html_classes_themed(&MONOKAI, render);
3723        assert!(classes.contains("#0c0c0c"), "class-form ignored the theme");
3724        let svg = console.export_svg_themed(&MONOKAI, "t", "id", render);
3725        assert!(svg.contains("#0c0c0c"), "svg ignored the theme");
3726
3727        // The convenience methods keep their documented defaults.
3728        assert!(console.export_html(render).contains("#ffffff"));
3729    }
3730
3731    #[test]
3732    fn page_with_honors_the_styles_flag() {
3733        use std::sync::Mutex;
3734
3735        #[derive(Default)]
3736        struct Recorder(Mutex<String>);
3737        impl crate::pager::Pager for Recorder {
3738            fn show(&self, content: &str) -> std::io::Result<()> {
3739                *self.0.lock().unwrap() = content.to_string();
3740                Ok(())
3741            }
3742        }
3743
3744        let console = Console::builder()
3745            .force_terminal(true)
3746            .color_system(Some(ColorSystem::Truecolor))
3747            .width(20)
3748            .no_color(false)
3749            .build();
3750
3751        // styles = false (upstream's `Console.pager()` default) strips ANSI.
3752        let plain = Recorder::default();
3753        console
3754            .page_with(&plain, false, |c| c.print_str("[bold red]hi[/] there"))
3755            .unwrap();
3756        assert_eq!(plain.0.lock().unwrap().as_str(), "hi there\n");
3757
3758        // styles = true keeps it, matching `Console.pager(styles=True)`.
3759        let styled = Recorder::default();
3760        console
3761            .page_with(&styled, true, |c| c.print_str("[bold red]hi[/] there"))
3762            .unwrap();
3763        assert_eq!(
3764            styled.0.lock().unwrap().as_str(),
3765            "\x1b[1;31mhi\x1b[0m there\n"
3766        );
3767
3768        // Links go unless `links` is set too (rich 15.0.0's
3769        // `console.pager(styles=True, links=…)`, less the random `id=`).
3770        let linked = |links: bool| {
3771            let recorder = Recorder::default();
3772            console
3773                .page_with_links(&recorder, true, links, |c| {
3774                    c.print_str("[bold link=https://x.io]hi[/] there")
3775                })
3776                .unwrap();
3777            let out = recorder.0.lock().unwrap().clone();
3778            out
3779        };
3780        assert_eq!(linked(false), "\x1b[1mhi\x1b[0m there\n");
3781        assert_eq!(
3782            linked(true),
3783            "\x1b]8;;https://x.io\x1b\\\x1b[1mhi\x1b[0m\x1b]8;;\x1b\\ there\n"
3784        );
3785    }
3786
3787    #[test]
3788    fn export_text_styled_renders_truecolor() {
3789        // rich 15.0.0, recording `[#ff8800]x[/] y`, `export_text(styles=True)`:
3790        // `\x1b[91mx…` on a `standard` console, truecolor with no colour.
3791        let console = |system| {
3792            Console::builder()
3793                .force_terminal(true)
3794                .color_system(system)
3795                .width(20)
3796                .no_color(false)
3797                .build()
3798        };
3799        let export =
3800            |console: Console| console.export_text_styled(|c| c.print_str("[#ff8800]x[/] y"));
3801        assert_eq!(
3802            export(console(Some(ColorSystem::Standard))),
3803            "\x1b[91mx\x1b[0m y\n"
3804        );
3805        assert_eq!(export(console(None)), "\x1b[38;2;255;136;0mx\x1b[0m y\n");
3806    }
3807
3808    #[test]
3809    fn export_text_strips_styles() {
3810        let console = Console::builder()
3811            .force_terminal(true)
3812            .color_system(Some(ColorSystem::Truecolor))
3813            .width(20)
3814            .build();
3815        // Captured from real rich 15.0.0 (Console.export_text(styles=False)).
3816        let out = console.export_text(|c| c.print_str("[bold red]hi[/] there"));
3817        assert_eq!(out, "hi there\n");
3818    }
3819
3820    #[test]
3821    fn export_html_matches_upstream() {
3822        let console = Console::builder()
3823            .force_terminal(true)
3824            .color_system(Some(ColorSystem::Truecolor))
3825            .width(20)
3826            .no_color(false)
3827            .build();
3828        let html = console.export_html(|c| {
3829            c.print_str("[bold red]hi[/] there");
3830            c.print_str("plain line");
3831        });
3832        // Regenerated from real rich by `scripts/capture_golden.py`, so CI's
3833        // drift check covers exports too. Keep this input in step with the
3834        // matching console in that script.
3835        let expected = include_str!("../tests/golden/export_html.html").replace("\r\n", "\n");
3836        assert_eq!(html, expected);
3837    }
3838
3839    #[test]
3840    fn export_html_classes_matches_upstream() {
3841        let console = Console::builder()
3842            .force_terminal(true)
3843            .color_system(Some(ColorSystem::Truecolor))
3844            .width(20)
3845            .no_color(false)
3846            .build();
3847        let html = console.export_html_classes(|c| c.print_str("[bold red]hi[/] there"));
3848        // As above: regenerated by `scripts/capture_golden.py`. Note this test
3849        // prints ONE line where the inline-styles test prints two.
3850        let expected =
3851            include_str!("../tests/golden/export_html_classes.html").replace("\r\n", "\n");
3852        assert_eq!(html, expected);
3853    }
3854
3855    /// Upstream's `render_lines` pads with its `style` argument as given
3856    /// (default `None`), so the pad segments carry no style.
3857    #[test]
3858    fn render_lines_pads_with_the_given_style() {
3859        let console = test_console();
3860        let options = console.options().update_width(5).update_height(2);
3861        let lines = console.render_lines(&Text::new("hi"), &options, true);
3862        assert_eq!(lines.len(), 2);
3863        let width_pad = lines[0].last().unwrap();
3864        assert_eq!((width_pad.text.as_str(), &width_pad.style), ("   ", &None));
3865        assert_eq!(lines[1], vec![Segment::new("     ", None)]);
3866
3867        let red = Style::parse("red").unwrap();
3868        let lines = console.render_lines_styled(&Text::new("hi"), &options, Some(&red), true);
3869        assert_eq!(lines[0].last().unwrap().style, Some(red.clone()));
3870        assert_eq!(lines[1], vec![Segment::new("     ", Some(red))]);
3871    }
3872
3873    #[test]
3874    fn capture_matches_direct_render() {
3875        let console = test_console();
3876        let panel = crate::panel::Panel::new(Box::new(Text::new("hi")));
3877        assert_eq!(
3878            console.capture(|c| c.print(&panel)),
3879            console.render_export(&panel)
3880        );
3881    }
3882
3883    #[test]
3884    fn no_color_strips_styles() {
3885        let console = Console::builder()
3886            .force_terminal(true)
3887            .color_system(None)
3888            .build();
3889        assert_eq!(console.render_str_to_string("[bold red]hi[/]"), "hi");
3890    }
3891
3892    #[test]
3893    fn used_theme_applies_through_the_guard_and_pops_on_drop() {
3894        let mut console = Console::builder()
3895            .force_terminal(true)
3896            .color_system(Some(ColorSystem::Truecolor))
3897            .width(20)
3898            .highlight(false)
3899            .build();
3900        let theme = Theme::from_styles([("accent", "bold red")], false).unwrap();
3901        {
3902            let themed = console.use_theme(theme);
3903            let out = themed.capture(|c| c.print_str("[accent]x[/]"));
3904            assert_eq!(out, "\x1b[1;31mx\x1b[0m\n");
3905        }
3906        assert!(console.theme().get("accent").is_none());
3907        assert!(console.pop_theme().is_err(), "the base theme must remain");
3908    }
3909
3910    #[test]
3911    fn used_theme_is_popped_during_a_panic_unwind() {
3912        let mut console = Console::builder().width(20).build();
3913        let theme = Theme::from_styles([("accent", "bold")], false).unwrap();
3914        let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3915            let _themed = console.use_theme(theme);
3916            panic!("render failed");
3917        }));
3918        assert!(unwound.is_err());
3919        assert!(console.theme().get("accent").is_none());
3920    }
3921
3922    #[test]
3923    fn print_objects_keeps_a_renderable_s_open_last_line() {
3924        // A `Screen` ends without a line break when printed
3925        // (`Renderable::print_end_line`); `print_objects` must not add one.
3926        let console = Console::builder()
3927            .force_terminal(true)
3928            .color_system(Some(crate::color::ColorSystem::Truecolor))
3929            .width(4)
3930            .height(2)
3931            .build();
3932        let screen = crate::screen::Screen::new(Box::new(Text::new("hi")));
3933        let printed = console.capture(|c| c.print(&screen));
3934        let objects = console.capture(|c| {
3935            c.print_objects(
3936                &[PrintObject::Renderable(&screen)],
3937                &PrintOptions::default(),
3938            )
3939            .unwrap()
3940        });
3941        assert_eq!(objects, printed);
3942        assert!(!objects.ends_with('\n'), "{objects:?}");
3943    }
3944
3945    #[test]
3946    fn a_printed_text_defers_layout_to_the_print_options() {
3947        // Captured from real rich 15.0.0 (#446, #447): `Text.join` drops the
3948        // text's own overflow and justify; print-level options still apply.
3949        let console = Console::builder()
3950            .force_terminal(true)
3951            .color_system(Some(crate::color::ColorSystem::Truecolor))
3952            .width(6)
3953            .highlight(false)
3954            .build();
3955        let text = Text::new("abcdefghij").overflow(Overflow::Ellipsis);
3956        assert_eq!(console.render_export(&text), "abcdef\nghij\n");
3957        let mut options = console.options();
3958        options.overflow = Some(Overflow::Ellipsis);
3959        assert_eq!(
3960            console.render_export_with(&Text::new("abcdefghij"), &options),
3961            "abcde…\n"
3962        );
3963        let wide = Console::builder()
3964            .force_terminal(true)
3965            .color_system(Some(crate::color::ColorSystem::Truecolor))
3966            .width(20)
3967            .highlight(false)
3968            .build();
3969        let tabbed = Text::new("a\tb").justify(Justify::Right);
3970        assert_eq!(wide.render_export(&tabbed), "a       b\n");
3971    }
3972}