Skip to content

Console and printing

A Console is where output goes. It knows how wide the terminal is, which colours it supports and which theme to use, and it turns anything renderable into styled text. Most programs create one console and print everything through it.

use rich::Console;

fn main() {
    let console = Console::new();
    console.print_str("[bold magenta]Hello[/], world");
}

The examples on this page use these imports:

use rich::measure::Measurement;
use rich::{
    ColorSystem, Console, ConsoleOptions, Justify, Overflow, Panel, Renderable, Rule, Segment,
    Style, Text,
};

Printing

Method Takes Use it for
print_str(&str) console markup Strings: parses [tags], expands :emoji:, applies highlighting
try_print_str(&str) console markup The same, but returns an error for malformed markup instead of printing it raw
print(&dyn Renderable) any renderable Text, Table, Panel, your own types
print_justified(&str, Justify) console markup A line aligned left, centre, right or fully justified
print_with(&dyn Renderable, &ConsoleOptions) any renderable Overriding width, justify, overflow or wrapping for one print

Every print ends with a newline.

fn printing(console: &Console) {
    // Markup: parsed, emoji-expanded and highlighted.
    console.print_str("[bold magenta]Hello[/], [italic]world[/] :wave:");
    // Automatic highlighting of numbers, strings, paths, URLs…
    console.print_str("Loaded 3 files from /etc/app in 250 ms");

    // Any Renderable goes through `print`.
    console.print(&Text::styled("A styled Text value", "bold green"));
    console.print(&Rule::new("[b]a rule[/]"));

    // A blank line is an empty print.
    console.print_str("");
    console.print_justified("[reverse] centred [/]", Justify::Center);
    console.print_justified("right →", Justify::Right);
}

Printing markup, Text, a rule and justified lines

Upstream's console.rule() and console.line() are spelled as plain prints here: console.print(&Rule::new("title")) and console.print_str(""). Upstream's console.log() has no method on the console; print a LogRecord instead.

Escape text you did not write

print_str parses markup, so user-supplied text containing [ can change your formatting. Pass it through rich::markup::escape first, or build a Text::new(untrusted) and print that — Text::new never parses markup. See markup.

Per-print options

print_with takes a ConsoleOptions. Start from console.options() and change the fields you need: max_width, height, justify, overflow, no_wrap.

fn print_with(console: &Console) {
    let long = "Pneumonoultramicroscopicsilicovolcanoconiosis-is-a-very-long-word";

    let mut options = console.options();
    options.max_width = 30; // render into 30 cells
    options.overflow = Some(Overflow::Ellipsis);
    options.no_wrap = Some(true);
    console.print_with(&Text::new(long), &options);

    options.overflow = Some(Overflow::Fold);
    options.no_wrap = None;
    console.print_with(&Text::new(long), &options);
}

The same long word rendered with ellipsis and fold overflow

A printed Text uses the print's options

When a Text is printed directly, its own justify, overflow and no_wrap are dropped in favour of the print's options — upstream does the same. Set them through print_with, or put the Text inside a container (a Panel, a table cell), where its own settings apply.

Configuring the console

Console::new() detects everything. Console::builder() lets you override it:

fn builder() {
    let console = Console::builder()
        .width(72) // ignore the detected width
        .height(20) // ignore the detected height
        .force_terminal(true) // style even when stdout is not a TTY
        .color_system(Some(ColorSystem::EightBit)) // downgrade colours to 256
        .no_color(false) // true: plain output, no colour or other styling
        .highlight(true) // automatic repr highlighting (the default)
        .emoji(true) // expand :shortcodes: (the default)
        .build();

    assert_eq!(console.width(), 72);
    assert_eq!(console.color_system(), Some(ColorSystem::EightBit));
    assert!(console.is_terminal());
}
Builder method Default Effect
width(usize) COLUMNS, else the terminal width, else 80 The width everything is rendered into
height(usize) LINES, else the terminal height, else 25 Used by height-filling renderables such as Layout
color_system(Option<ColorSystem>) detected from COLORTERM/TERM; None when not a terminal Standard (16), EightBit (256), Truecolor, or None for no colour at all
force_terminal(bool) detected Treat stdout as a terminal: keep styles and control codes when piped
no_color(bool) on when NO_COLOR is set and non-empty Remove colours from output, keeping bold, italic and underline (see the note below)
highlight(bool) true Automatic highlighting of numbers, strings, paths, URLs…
emoji(bool) true Expand :rocket:-style shortcodes in markup
theme(Theme) Theme::default_theme() The style names markup and renderables look up (themes)
legacy_windows(bool), safe_box(bool) false, true On a legacy Windows console, swap ROUNDED/HEAVY boxes for SQUARE
ascii_only(bool) false Draw every box with ASCII characters

Read the result back with console.width(), height(), color_system(), is_terminal() and no_color().

no_color removes only colour

As upstream's does, no_color strips colours when output is written and keeps bold, italic and underline. color_system() still reports the detected colour system, so to ask "will colour reach the terminal?", also check no_color(). Exports read the recording, so they keep their colours. For no styling at all, use color_system(None).

Pin the console in tests

Output depends on the terminal. For snapshot tests and generated docs, set width, force_terminal(true) and color_system(...) so the same code produces the same bytes everywhere. Every screenshot in this guide is made that way (how).

Upstream's Console(record=True) has no builder flag here: recording is scoped to a closure instead — see capturing.

Capturing output

Instead of printing, you can capture what would be printed. Each method runs a closure that receives the same console; everything the closure prints is collected rather than written, and captures nest.

fn capturing() {
    let console = Console::builder()
        .width(40)
        .force_terminal(true)
        .color_system(Some(ColorSystem::Standard))
        .build();

    // Everything printed inside the closure is returned instead of written.
    let ansi = console.capture(|c| c.print_str("[bold]hi[/]"));
    assert_eq!(ansi, "\x1b[1mhi\x1b[0m\n");

    // The same, with styles stripped.
    let plain = console.export_text(|c| c.print_str("[bold]hi[/]"));
    assert_eq!(plain, "hi\n");

    // Render one value without printing it.
    let rule = Rule::new("x");
    let with_newline = console.render_export(&rule); // exactly what print writes
    let without = console.render_to_string(&rule); // no trailing newline
    assert_eq!(with_newline, format!("{without}\n"));

    // The raw segments, for producing several outputs from one render.
    let segments: Vec<Segment> = console.record_output(|c| c.print_str("[red]a[/] b"));
    assert_eq!(segments[0].text, "a");
    assert_eq!(
        console.segments_to_string(&segments),
        "\x1b[31ma\x1b[0m b\n"
    );
}
Method Returns
capture(f) the ANSI string f would have written
export_text(f) the same, styles stripped
record_output(f) the raw Vec<Segment>, to render into several formats from one pass
render_to_string(&r) one renderable as ANSI, without the trailing newline
render_export(&r) one renderable exactly as print writes it, newline included
render_str_to_string(&str) one markup string, as print_str would render it
export_html(f), export_svg(…) documents — see Exporting

build_text(&str) gives you the styled Text that print_str would print, so you can wrap markup in another renderable.

Measuring

A Measurement is the minimum and maximum number of cells a renderable needs. Containers use it to lay out their children; you rarely need it directly, but it explains a lot of layout behaviour.

fn measure(console: &Console) {
    let options = console.options();
    let text = Text::new("the quick brown fox");
    let m = Measurement::get(console, &options, &text);
    // minimum = the longest word, maximum = the whole line.
    console.print_str(&format!("Text:  min={} max={}", m.minimum, m.maximum));

    // A panel measures its content plus border and padding, as upstream's
    // does, though it still expands to fill the width when rendered.
    let panel = Panel::new(Box::new(Text::new("hi")));
    let m = Measurement::get(console, &options, &panel);
    console.print_str(&format!("Panel: min={} max={}", m.minimum, m.maximum));

    // Clamp a measurement into bounds.
    let m = Measurement::new(5, 19).clamp(Some(8), Some(12));
    console.print_str(&format!("clamped: min={} max={}", m.minimum, m.maximum));
}

Measurements of a Text, a Panel and a clamped range

  • Text measures from its content: the minimum is its longest word, the maximum its longest line.
  • Table, Tree, Padding, Align and Constrain measure their content, as upstream's do, so they size to it in a table cell or under Align.
  • Panel fills the width unless built with Panel::fit (or given a width); a fitted panel measures its content plus its border and padding.
  • Everything else — Columns, Layout, and any Renderable that does not override measure — reports the full available width. Wrap it in Constrain to make it narrower.
  • Syntax, Json, Pretty, ProgressBar and Styled measure their content.
  • Measurement::get normalizes and caps a renderable's answer at options.max_width; clamp and with_maximum adjust one.

The Renderable trait

Renderable is the Rust form of upstream's __rich_console__. One method is required:

fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment>;

measure is optional (the default asks for the whole width).

Writing your own renderable

This one draws a dotted leader between a key and a value, filling whatever width it is given — so it works at the top level and inside a panel:

/// A key/value line that pushes its value to the right edge with dots:
/// `name ........ value`. It adapts to whatever width it is given.
struct Leader {
    key: String,
    value: String,
}

impl Renderable for Leader {
    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
        let used = self.key.chars().count() + self.value.chars().count() + 2;
        let dots = options.max_width.saturating_sub(used).max(1);
        vec![
            Segment::new(self.key.clone(), Some(Style::parse("bold").unwrap())),
            Segment::new(" ", None),
            Segment::new(".".repeat(dots), Some(Style::parse("dim").unwrap())),
            Segment::new(" ", None),
            Segment::new(self.value.clone(), Some(Style::parse("cyan").unwrap())),
        ]
    }

    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
        // At least "key . value"; happy to take the whole width.
        let minimum = self.key.chars().count() + self.value.chars().count() + 3;
        Measurement::new(minimum.min(options.max_width), options.max_width)
    }
}

fn custom(console: &Console) {
    let row = |key: &str, value: &str| Leader {
        key: key.into(),
        value: value.into(),
    };
    console.print(&row("version", "0.0.7"));
    console.print(&row("licence", "MIT"));
    // Custom renderables nest inside the built-in containers.
    console.print(&Panel::new(Box::new(row("inside", "a panel"))).title("Leader"));
}

A custom Leader renderable, alone and inside a panel

Rules for a well-behaved renderable:

  • Stay inside options.max_width. The console crops lines that run past the terminal edge, but a container gives you less than the full width and expects you to respect it.
  • Separate lines with newline segments (Segment::line()), and do not end with one: the console adds the final newline.
  • Measure honestly if you want to sit in a table column or be centred: Measurement::new(min, max) with the narrowest and widest you can render.
  • Reuse the built-ins. Build a Text, Table or Panel and return its rich_render(console, options) rather than drawing boxes by hand.

Segments

A Segment is a piece of text with an optional Style and a control flag (for cursor movement and other terminal control codes). It is the unit every renderable produces and every export consumes.

fn segments_demo(console: &Console) {
    let text = Text::from_markup("[bold]Hello[/] world").unwrap();
    for segment in console.record_output(|c| c.print(&text)) {
        println!(
            "{:?} {:?}",
            segment.text,
            segment.style.map(|s| s.definition())
        );
    }
    // "Hello" Some("bold")
    // " world" Some("none")
    // "\n" None
}

Useful helpers on Segment: line(), cell_length(), split_lines, apply_style, adjust_line_length (pad or crop a line to a width), simplify (merge neighbours with the same style), crop_lines. For whole lines at a fixed width, console.render_lines(&r, &options, pad) returns Vec<Vec<Segment>>, one entry per line — that is what Panel and Layout use internally.

Gotchas

  • Piped output is plain. When stdout is not a terminal, Console::new() drops colour and control codes. That is usually right (> out.txt gives clean text); use force_terminal(true) when it is not.
  • Malformed markup prints raw. print_str("[/oops]") prints the text as-is rather than failing, which is friendlier than upstream's exception but can hide mistakes. Use try_print_str / try_build_text for markup that comes from users or config (divergence #2).
  • Adding highlighters and themes needs &mut. add_highlighter, push_theme and use_theme take &mut self; configure the console before sharing it.

See also