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