An abstract table model written in Rust, with customisable text formatting and renderers.
- Feature-complete: Stanza supports a broad range of styling features — various text formatting controls, foreground/background/fill colours, border styles, multiple horizontal and vertical headers and separators, and even nested tables, to name a few.
- Pluggable renderers: The clean separation of the table model from the render implementation lets you switch between output formats. For example, the table that you might output to the terminal can be switched to produce a Markdown document instead. You can also add your own render; e.g., to output HTML or paint a TUI/Curses screen.
- Ease of use: Simple things are easy to do and hard things are possible. Stanza offers both a fluid API for building "static" tables and an API for building a table programmatically.
- No standard library needed: Stanza is
no_std, meaning it can be used in embedded devices.
- Performance: It takes ~10 µs to build the table model used in the screenshot above and ~200 µs to render it. (Markdown takes roughly half that time.) Efficiency mightn't sound like a concern in desktop and server use cases, but it makes a difference in low-powered devices.
A basic table
Stanza intentionally separates the
Table model from the set of optional
Style modifiers and, finally, the
Renderer that is used to turn the table into a printable
There are four main types of elements within the model:
Table holds both
Row objects. Tables in Stanza are row-oriented; i.e., a
Col is used purely for assigning styles;
Row is where the data lives.
Row contains a vector of
Cells. In turn, a
Cell houses a
Content enum. The simplest and most common content type that you will use is a
Let's start by creating a most basic, 2x3 table.
use Console; use Renderer; use Table; // build a table model let table = default .with_row .with_row .with_row; // configure a renderer that will later turn the model into a string let renderer = default; // render the table, outputting to stdout println!;
The printing of the table here is clearly split into two distinct steps:
- Building the table model.
- Invoking a renderer on the model to produce the output.
The resulting output:
╔═══════════╤══════╗ ║Department │Budget║ ╟───────────┼──────╢ ║Sales │90000 ║ ╟───────────┼──────╢ ║Engineering│270000║ ╚═══════════╧══════╝
Not bad for a half-dozen lines. We used all the concepts above without specifying them explicitly. For example, we didn't refer to
Content types at all. Stanza offers a highly abridged syntax for building tables where additional flexibility mightn't be needed. Still, it's worth understanding what happens under the hood. The same table model can be produced using the fully explicit syntax below.
use Styles; use ; with_styles .with_cols .with_row .with_row .with_row;
We've gone from 4 lines to over 30, but that's what it takes to specify this model using the fully explicit syntax. You'll be pleased to know that Stanza's syntax is not a binary either-or: it lets you progressively use more explicit constructs as you need to refine your styles or layouts. Note also that we didn't need to specify columns in the first cut — the table model will autogenerate the column definitions based on the number of cells.
Our table lacks a few niceties, however. Ideally, we would like to top row to act as a header. The context is also a little cramped. And finally, the budget figures should ideally be right-aligned. Lets a build a more stylish table.
use Console; use Renderer; use ; use ; let table = default .with_cols .with_row .with_row .with_row; let renderer = default; println!;
╔════════════════════╤═══════════════╗ ║Department │ Budget║ ╠════════════════════╪═══════════════╣ ║Sales │ 90000║ ╟────────────────────┼───────────────╢ ║Engineering │ 270000║ ╚════════════════════╧═══════════════╝
Earlier, it was promised that you could output the same table model into a variety of formats. To switch to Markdown, simply replace the renderer:
use Markdown; use Renderer; // build the model ... let renderer = default; // render ...
|Department | Budget| |:-------------------|--------------:| |Sales | 90000| |Engineering | 270000|
Voilà, we have Markdown!
This is a good segue into styles. Stanza is built on the philosophy of separating models from renderers. While this offers phenomenal flexibility, it does create a problem. Every renderer is different, which limits the portability of styles. For example, the
Console renderer supports a lot richer output than, say,
When forming the table model, we almost always have a particular output format in mind. Call it the "preferred" format. It's normal to decorate the table for the preferred format. What we'd also like is to render the table in some alternate format without breaking the output or, worse, causing a
panic because the alternate render mightn't support some style modifier.
Styles are always optional; it is up to the renderer to make use of a style if it can do so meaningfully. Renderers ignore styles they don't understand and may downgrade a style that isn't fully supported while preserving the legibility of the content. For example,
Markdown is oblivious to the
Blink style, but it will still render the text — albeit without the blinking effect. You might even create your own renderer someday and a bunch of styles that only that renderer understands. This won't affect the existing renderers in the slightest.
To organise styles, Stanza posits two basic rules: specificity and assignability.
Styles cascade, much like their CSS counterparts. A style assigned at some higher-level element will automatically be applied to all elements in the layers below it. The specificity hierarchy is shown below.
Table └─> Col └─> Row └─> Cell
Generally, one element is considered to be higher than another if the former intersects with more elements than the latter. As an example, the table intersects all other elements, hence it is at the top of the specificity ordering. Conversely, a cell only intersects itself, one row, one column and one table, placing it firmly at the bottom.
Rows and columns aren't so cut and dried, because a table could be very tall or very wide. However, tall tables are far more frequent. In fact, that's how one generally scales a table — by adding rows, not columns. A column will intersect more elements in more cases; therefore, it is higher in the specificity hierarchy.
The specificity hierarchy is used to resolve conflicting styles. Say, we assigned a
TextFg::Magenta style to the table and
TextFg::Cyan to the cell. Which style should the cell render with?
Cyan, of course. This is how we'd expect a normal spreadsheet to behave. Although the cell inherits styles from the table, the column and the row, its own style overrides any of its parents'.
A style defined at the table level will apply to the table and everything contained within, and may be overridden by any lower-level style. A style defined at the column level will apply to the column and all cells intersected by the column, and may be overridden by a cell style. Similarly, a style defined at the row level will apply to the row and all of its cells, and it may be overridden by the cells equivalently. But what happens when a cell inherits a conflicting style from both the column and the row, but does not have an overriding style of its own? The row style takes precedence, as it is more specific.
Although styles cascade in a top-down manner, it doesn't mean that a style may be assigned to any element. Take the
Header style, for example. It may be assigned to a row or to a column. (Stanza supports vertical headers.) Might a
Header be assigned to the table as a whole? Yes, in which case all rows and columns would be treated as headers. But a header cell makes no sense. Whether a style may be assigned to a particular element can be determined by invoking the
S::assignability() static trait method for some
S: Style, returning a variant of the
Assignability enum. The assignability hierarchy is shown below.
Cell ├───> Col │ └─┐ └─> Row │ └────┴─> Table
A style that can be assigned to a cell can also be assigned to a row, a column and a table. Notable examples include text formatting styles —
HAlign, as well as some colouring styles —
Above the cell, the hierarchy is disjoint at the column and row elements. Any style that can be assigned to a row can also be assigned to a table. Similarly, any style that can be assigned to a column can also be assigned to a table. These include
A style that can be assigned to a row is not necessarily assignable to a column, and vice versa. For example,
MaxWidth may only be assigned to a column — they make no sense at the row level. (And certainly not at the cell level.)
Finally, some styles may only be assigned to a table. These include
BorderBg — used to alter the colour of all borders in the table.
The assignability rule is enforced at runtime. Attempting to assign a nonassignable style will fail with a
panic. As such, changing the returned
Assignability value of a style to a more restrictive variant would constitute a breaking change.
With all this in mind, let's create another table that demonstrates overriding styles.
We didn't have to work much to get our text laid out well. Often, all that's needed is a
MinWidth column style and possibly a
HAlign. Sometimes we might have lots of text, which pushes our column size out:
use Console; use Renderer; use ; use ; let table = default .with_cols .with_row .with_row .with_row; println!;
╔═══════════════╤═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗ ║Poem │Extract ║ ╠═══════════════╪═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣ ║Antigonish │Yesterday, upon the stair, I met a man who wasn't there! He wasn't there again today, Oh how I wish he'd go away! ║ ╟───────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╢ ║The Raven │Ah, distinctly I remember it was in the bleak December; And each separate dying ember wrought its ghost upon the floor.║ ╚═══════════════╧═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝
As we expected, the "Extract" column is too wide. A simple way of dealing with this is to set
MaxWidth style on the column. Simply append
.with(MaxWidth(40)) to the existing styles:
╔═══════════════╤════════════════════════════════════════╗ ║Poem │Extract ║ ╠═══════════════╪════════════════════════════════════════╣ ║Antigonish │Yesterday, upon the stair, I met a man ║ ║ │who wasn't there! He wasn't there again ║ ║ │today, Oh how I wish he'd go away! ║ ╟───────────────┼────────────────────────────────────────╢ ║The Raven │Ah, distinctly I remember it was in the ║ ║ │bleak December; And each separate dying ║ ║ │ember wrought its ghost upon the floor. ║ ╚═══════════════╧════════════════════════════════════════╝
That's better! But since we're dealing with a poem, we should probably respect the author's choice of line breaks. Stanza supports newline characters, leading to line breaks exactly where you need them. Best of all, you can combine newline characters with
MaxWidth, resulting in something like this:
╔═══════════════╤════════════════════════════════════════╗ ║Poem │Extract ║ ╠═══════════════╪════════════════════════════════════════╣ ║Antigonish │Yesterday, upon the stair, ║ ║ │I met a man who wasn't there! ║ ║ │He wasn't there again today, ║ ║ │Oh how I wish he'd go away! ║ ╟───────────────┼────────────────────────────────────────╢ ║The Raven │Ah, distinctly I remember it was in the ║ ║ │bleak December; ║ ║ │And each separate dying ember wrought ║ ║ │its ghost upon the floor. ║ ╚═══════════════╧════════════════════════════════════════╝
The ability to support multiple row and column headers is a feature unique to Stanza. Let's draw a multiplication table to illustrate. This will also double as an example of building tables programmatically.
use Console; use Renderer; use ; use ; const NUMS: i8 = 6; // the table will multiply from 1 to NUMS // builds just the header row -- there should be two (top and bottom) // builds all body rows (each row also contains a pair column header cells) let mut table = with_styles .with_cols; table.push_row; table.push_rows; table.push_row; println!;
╔═════╦═════╤═════╤═════╤═════╤═════╤═════╦═════╗ ║ ║ 1│ 2│ 3│ 4│ 5│ 6║ ║ ╠═════╬═════╪═════╪═════╪═════╪═════╪═════╬═════╣ ║ 1║ 1│ 2│ 3│ 4│ 5│ 6║ 1║ ╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢ ║ 2║ 2│ 4│ 6│ 8│ 10│ 12║ 2║ ╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢ ║ 3║ 3│ 6│ 9│ 12│ 15│ 18║ 3║ ╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢ ║ 4║ 4│ 8│ 12│ 16│ 20│ 24║ 4║ ╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢ ║ 5║ 5│ 10│ 15│ 20│ 25│ 30║ 5║ ╟─────╫─────┼─────┼─────┼─────┼─────┼─────╫─────╢ ║ 6║ 6│ 12│ 18│ 24│ 30│ 36║ 6║ ╠═════╬═════╪═════╪═════╪═════╪═════╪═════╬═════╣ ║ ║ 1│ 2│ 3│ 4│ 5│ 6║ ║ ╚═════╩═════╧═════╧═════╧═════╧═════╧═════╩═════╝
The handling of row and column headers is entirely renderer-specific.
Markdown, for example, supports at most one header row, being the first row in the table. It nonetheless renders the content correctly, albeit not as nicely as
| | 1| 2| 3| 4| 5| 6| | |----:|----:|----:|----:|----:|----:|----:|----:| | 1| 1| 2| 3| 4| 5| 6| 1| | 2| 2| 4| 6| 8| 10| 12| 2| | 3| 3| 6| 9| 12| 15| 18| 3| | 4| 4| 8| 12| 16| 20| 24| 4| | 5| 5| 10| 15| 20| 25| 30| 5| | 6| 6| 12| 18| 24| 30| 36| 6| | | 1| 2| 3| 4| 5| 6| |
Separators are a means of subdividing a table into logical sections. There could be multiple such sections, and the division might be horizontal, vertical, or both. A separator is created by either assigning a
Separator style to a
Col, or instantiating the
Col using the
separator() convenience method. The latter is generally preferred due to brevity; the former allows you to specify additional styles for added flexibility. Let's try this on a basic Sudoku puzzle:
use Console; use Renderer; use ; use ; let table = with_styles .with_cols .with_row .with_row .with_row .with_row .with_row .with_row .with_row .with_row .with_row .with_row .with_row; println!;
╔═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╤═══╗ ║ 5 │ 3 │ │ │ │ 7 │ │ │ │ │ ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ 6 │ │ │ │ 1 │ 9 │ 5 │ │ │ │ ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ │ 9 │ 8 │ │ │ │ │ │ │ 6 │ ║ ╟───┴───┴───┘ └───┴───┴───┘ └───┴───┴───╢ ║ ║ ╟───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───╢ ║ 8 │ │ │ │ │ 6 │ │ │ │ │ 3 ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ 4 │ │ │ │ 8 │ │ 3 │ │ │ │ 1 ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ 7 │ │ │ │ │ 2 │ │ │ │ │ 6 ║ ╟───┴───┴───┘ └───┴───┴───┘ └───┴───┴───╢ ║ ║ ╟───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───╢ ║ │ 6 │ │ │ │ │ │ │ │ 2 │ 8 ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ │ │ │ │ 4 │ 1 │ 9 │ │ │ │ 5 ║ ╟───┼───┼───┤ ├───┼───┼───┤ ├───┼───┼───╢ ║ │ │ │ │ │ 8 │ │ │ │ 7 │ 9 ║ ╚═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╝
The data we've been tabulating thus far has been determined at the point of
Table creation, using
Content::Label under the hood. More often than not, the table model is built as the last step in some process, once all the necessary data is available, and is rendered immediately thereafter.
There is another way, wherein the table model is used purely as a placeholder layout, with maybe a handful of static labels (e.g., headers). The rest of the data can be computed at the point of rendering. This "late bound" approach is made possible by
The following example shows the difference between an early-bound cell value and a late-bound one. The value in the bottom-left cell is taken by calling
current_time() at model build-time. The bottom-right cell uses
Content::Computed to embed a closure, which is evaluated when
render() is called. The example purposely injects a 2-second wait time so that the two values differ.
use thread; use Duration; use Console; use Renderer; use ; use ; // build the table model let table = default .with_row .with_row; // wait a little sleep; // render the table println!;
╔═══════════╤══════════╗ ║Early-bound│Late-bound║ ╠═══════════╪══════════╣ ║07:40:41 │07:40:43 ║ ╚═══════════╧══════════╝
The greatest layout flexibility comes from nested tables. Nesting essentially lets you combine differently structured content in the same overarching table.
The next example combines two different data sets and adds a vertical separator for neatness.
use Console; use Renderer; use ; use ; let table = with_styles .with_cols .with_row .with_row; println!;
╔════════════╤═════╤═════════════╗ ║ Sensors │ │ Stocks ║ ╟────────────┤ ├─────────────╢ ║╔═════╤════╗│ │╔════╤══════╗║ ║║Water│19.3║│ │║AAPL│138.20║║ ║╟─────┼────╢│ │╟────┼──────╢║ ║║Oil │65.1║│ │║AMZN│113.20║║ ║╚═════╧════╝│ │╟────┼──────╢║ ║ │ │║IBM │118.81║║ ║ │ │╚════╧══════╝║ ╚════════════╧═════╧═════════════╝
Note, nesting uses the
Content::Nested behind the scenes, although we didn't have to use this enum variant explicitly in the example. That's because the abridged syntax uses
into() to convert a
Table into a
Content::Nested(Table) for us. This is much the same as calling
into() on any value that implements
ToString — the value will be converted to a
Content::Label(String) for us.
A notable limitation of nested tables is that all character formatting of the inner table will be replaced with the format of the outer table cell. You may still use any of the layout styles (
Header, etc.), it's just the character formatting styles (
TextFg, etc.) that will be ignored.
So far we employed various
Content enum variants to assign content of different types — plain text, computed values and nested tables — to any given cell. What if we needed to combine content of several distinct types into a single cell? This is accomplished using the
Let's take the "nested tables" example above. Currently, it displays the labels "Sensor temps" and "Stock prices" in a separate row above the nested tables. Let's merge them into a single cell.
use Console; use Renderer; use ; use ; let table = with_styles .with_cols .with_row; println!;
╔════════════╤═════╤═════════════╗ ║ Sensors │ │ Stocks ║ ║╔═════╤════╗│ │╔════╤══════╗║ ║║Water│19.3║│ │║AAPL│138.20║║ ║╟─────┼────╢│ │╟────┼──────╢║ ║║Oil │65.1║│ │║AMZN│113.20║║ ║╚═════╧════╝│ │╟────┼──────╢║ ║ │ │║IBM │118.81║║ ║ │ │╚════╧══════╝║ ╚════════════╧═════╧═════════════╝
There is a more elaborate alternative to the
render() method —
render_with_hints(), which takes an immutable reference to the
Table and a slice of
RenderHints. Hints offer advanced control over the renderer's behaviour. They are generally not needed for most use cases — we flew through the previous examples while the renderer correctly did its thing.
Hints are used internally within Stanza to feed additional context to the render that it mightn't be aware of. For example, when you use
Content::Nested, the table will be rendered recursively. This presents a problem for inner tables that might be using character formatting styles, which output special ANSI escape sequences (when using the
Console renderer). These escape sequences interfere with those generated in the course of styling the outer table cell. To solve this problem, the outer rendering routine passes in
RenderHint::Nested during recursion, telling the inner rendering routine to suppress these escape sequences.
Why use hints when the renderer takes care of this automatically? Take nested tables. Although the
Content::Nested enum variant is convenient, it is highly inflexible. Using it implies that the nested table will be drawn using the same renderer that is used for the outer table. This is why in our previous examples, the inner tables and the outer table shared the overall look and feel.
To gain full control over the table styles, we need to pre-render the inner tables before embedding the output into the outer table. This way we can control all aspects of the inner render. In fact, one might use a different type of renderer altogether. In the following example, we use two different
Console renderers: the outer renderer maintains the default configuration, while the inner renderer is stripped of the outer borders.
use ; use ; use ; use ; let inner_renderer = Console; let sensors = default .with_row .with_row; let stocks = default .with_row .with_row .with_row; let outer = with_styles .with_cols .with_row .with_row; println!;
╔══════════╤═════╤═══════════╗ ║ Sensors │ │ Stocks ║ ╟──────────┤ ├───────────╢ ║Water│19.3│ │AAPL│138.20║ ║─────┼────│ │────┼──────║ ║Oil │65.1│ │AMZN│113.20║ ║ │ │────┼──────║ ║ │ │IBM │118.81║ ╚══════════╧═════╧═══════════╝