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