Skip to content

Exporting

Anything you can print you can also save: as plain text, as an HTML page, or as an SVG picture of a terminal window. Exports are self-contained — no external CSS, fonts or images — and deterministic, so they diff cleanly and work as test fixtures.

The examples use these imports and this small report:

use rich::r#box::ROUNDED;
use rich::{
    export, svg, ColorSystem, Console, Justify, Table, DEFAULT_TERMINAL_THEME, MONOKAI,
    NIGHT_OWLISH, SVG_EXPORT_THEME,
};
fn report(console: &Console) {
    console.print_str("[bold]Build report[/] :package:");
    let mut table = Table::new().box_set(ROUNDED);
    table.add_column("Crate");
    table.add_column_justify("Tests", Justify::Right);
    table.add_column("Result");
    table.add_row(&["rs-rich", "312", "pass"]);
    table.add_row(&["rs-rich-ext", "187", "pass"]);
    console.print(&table);
    console.print_str("[green]✓[/] all green in [bold]41[/] s");
}

Exporting a render

Each export method takes a closure, runs it with output recorded instead of written, and returns the document:

fn exports(out: &Path) -> std::io::Result<()> {
    // Pin the console so the export does not depend on the terminal.
    let console = Console::builder()
        .width(60)
        .force_terminal(true)
        .color_system(Some(ColorSystem::Truecolor))
        .build();

    // Each export renders the closure, records it, and returns a document.
    // Nothing is written to the terminal.
    let text = console.export_text(report); // plain text
    let html = console.export_html(report); // inline style="…" spans
    let html_classes = console.export_html_classes(report); // <style> + classes
    let svg = console.export_svg("Build report", "build-report", report);

    std::fs::write(out.join("report.txt"), &text)?;
    std::fs::write(out.join("report.html"), &html)?;
    std::fs::write(out.join("report-classes.html"), &html_classes)?;
    std::fs::write(out.join("report.svg"), &svg)?;

    assert!(text.starts_with("Build report"));
    assert!(html.contains("<pre"));
    assert!(svg.starts_with("<svg"));
    Ok(())
}
Method Produces Upstream
export_text(f) plain text, styles stripped export_text()
capture(f) the ANSI text a terminal would receive capture()
export_html(f) HTML with inline style="…" on each span export_html(inline_styles=True)
export_html_classes(f) HTML with a <style> block and .r1, .r2 … classes export_html()
export_svg(title, unique_id, f) an SVG of a terminal window with title in its title bar export_svg(title=…, unique_id=…)

Pin the console first

An export captures whatever the console renders at the console's width and with its theme. Styles are kept even when stdout is piped (HTML and SVG do not depend on the colour system), but the width does depend on the terminal. Build the console with an explicit width — plus force_terminal(true) and a colour system if you also compare capture output — so the file is the same wherever the program runs.

unique_id is required

SVG class names and ids are prefixed with unique_id, so several SVGs can share one HTML page. Upstream derives a default by hashing Python repr() output, which Rust cannot reproduce, so here you pass it. Given the same id and the same render, the bytes are identical to upstream's (divergence #15).

Terminal themes

Styles name abstract colours ("red", "the default foreground"). A TerminalTheme decides what RGB those become in the exported file. Every export has a _themed variant:

fn themed(out: &Path) -> std::io::Result<()> {
    let console = Console::builder().width(60).build();

    // Choose the palette the abstract colours map to.
    let dark_html = console.export_html_themed(&MONOKAI, report);
    let light_svg = console.export_svg_themed(&DEFAULT_TERMINAL_THEME, "Report", "light", report);

    std::fs::write(out.join("report-monokai.html"), dark_html)?;
    std::fs::write(out.join("report-light.svg"), light_svg)?;
    Ok(())
}

The same report under three of the bundled palettes:

The report under SVG_EXPORT_THEME

The report under MONOKAI

The report under NIGHT_OWLISH

Theme Default for
SVG_EXPORT_THEME export_svg
DEFAULT_TERMINAL_THEME export_html (black on white)
MONOKAI, DIMMED_MONOKAI, NIGHT_OWLISH —

A TerminalTheme is a plain struct (background, foreground, and the 16 ansi colours as ColorTriplets), so you can define your own. Truecolor and 256-colour styles are exported as-is; only the 16 standard colours and the defaults go through the theme.

Render once, export many

The closure-based methods render once per call. To produce the terminal output and several files from a single render — which matters when the render consumes something, like standard input — record the segments and export them yourself:

fn record_once(out: &Path) -> std::io::Result<()> {
    let console = Console::builder()
        .width(60)
        .force_terminal(true)
        .color_system(Some(ColorSystem::Truecolor))
        .build();

    // Render once, keep the segments, produce every format from them.
    let segments = console.record_output(report);

    let ansi = console.segments_to_string(&segments); // what the terminal gets
    let html = export::export_html_classes(&segments, &DEFAULT_TERMINAL_THEME);
    let image = svg::export_svg(
        &segments,
        &SVG_EXPORT_THEME,
        "Report",
        "rpt",
        console.width(),
    );

    print!("{ansi}"); // show it, too
    std::fs::write(out.join("once.html"), html)?;
    std::fs::write(out.join("once.svg"), image)?;
    Ok(())
}

export::export_html_inline, export::export_html_classes and svg::export_svg take the segments directly. This is upstream's Console(record=True) plus save_html(clear=False), with the buffer handed to you instead of kept on the console. It is also how rich --export-html … --export-svg … works in the CLI.

How the guide's screenshots are made

Every picture in this guide is an export_svg of the example program that contains the snippet next to it. Each program takes an optional --svg DIR; with it, each "shot" is rendered on a pinned console:

/// A console that renders the same bytes on every machine.
fn pinned_builder(width: usize) -> ConsoleBuilder {
    Console::builder()
        .width(width)
        .force_terminal(true)
        .color_system(Some(ColorSystem::Truecolor))
        .no_color(false) // ignore NO_COLOR in the environment
}

and written to DIR/guide_<topic>-<shot>.svg, with the file name as the unique id so a regenerated image only changes when the rendering does:

/// Render `f` on `console` and write it to `dir/<stem>.svg`.
fn export_shot(dir: &Path, stem: &str, console: &Console, f: impl FnOnce(&Console)) {
    // The stem doubles as the SVG's unique id: same input, same bytes.
    let svg = console.export_svg(stem, stem, f);
    let path = dir.join(format!("{stem}.svg"));
    std::fs::write(&path, svg).expect("write the screenshot");
    eprintln!("wrote {}", path.display());
}

Regenerate them all with:

for ex in console text tables layout tree progress code logging prompts export; do
    cargo run -p rs-rich --example guide_$ex -- --svg docs/media/guide
done

Not yet ported

  • save_text, save_html, save_svg — write the returned string with std::fs::write.
  • export_html(code_format=…) and export_svg(code_format=…) custom templates, and the SVG font_aspect_ratio option.

See also