Skip to content

Render helpers

Two functions that turn a slice of rows into a string. They exist as a pair so one --output text|json flag can pick between them without the calling command formatting anything itself.

#[must_use]
pub fn render_table<R: Tabled>(rows: &[R]) -> String;

pub fn render_json<R: Serialize>(rows: &[R]) -> Result<String, RenderError>;

Both take a slice, so both accept &vec and &[..]. Neither prints anything. They return the string and leave the writing to you.

Which Tabled to derive

rtb-tui re-exports its tabled dependency as rtb_tui::tabled. Derive through it:

use rtb_tui::tabled::Tabled;

#[derive(Tabled)]
#[tabled(crate = "rtb_tui::tabled")]
struct Row {
    name: &'static str,
}

The crate attribute matters. Without it the derive expands to ::tabled::Tabled, which only resolves if you also depend on tabled directly, and then it resolves to your tabled. When that is a different version from the one rtb-tui uses, it is a different trait, and render_table rejects the row type with E0277 ("the trait bound R: tabled::tabled::Tabled is not satisfied", with a note that there are multiple versions of tabled in the graph).

rtb-tui 0.6.4 moved from tabled 0.21 to 0.22, which is how rtb-cli 0.9.1 hit exactly that error.

render_table output shape

The style is tabled's Style::psql(), applied unconditionally. There is no parameter, no builder and no configuration key that changes it. See Why the table style is fixed.

For two rows of struct Row { name: &'static str, count: u32 }:

 name  | count 
-------+-------
 alpha | 1     
 beta  | 2     

Columns are padded to the widest cell, so the rule width follows the data.

Column headers come from the field names of the Tabled derive, in declaration order. Rename them with #[tabled(rename = "...")] on the field; that is a tabled attribute, not an rtb-tui one.

The returned string always ends with a newline. If tabled did not produce one, render_table appends it, so print! is the right macro and println! will add a blank line.

render_table is infallible: there is no error type and no panic path for a type that implements Tabled.

What render_table does with an empty slice

An empty slice still renders the header and the separator rule, because the headers come from the type rather than from the data:

 name | count 
------+-------

It does not return an empty string, and it does not return a "no rows" message. If you want one, check rows.is_empty() before calling.

render_json output shape

serde_json::to_string_pretty, which is a two-space indent, plus a trailing newline appended by render_json. The top level is always a JSON array, including for one row and for zero rows:

[
  {
    "name": "alpha",
    "count": 1
  }
]

An empty slice renders as [] followed by a newline. Field names come from serde, so #[serde(rename = "...")] and #[serde(skip)] apply in the usual way. Note that serde and tabled have separate rename attributes, so a field renamed for one is not renamed for the other.

When render_json returns an error

render_json returns RenderError::Json(String) when serde_json::to_string_pretty fails on the rows. The payload is the underlying serde_json::Error already converted to a string.

In practice serde_json fails on very little. Measured against this crate at 0.6.3, and the first three rows re-checked at 0.6.4 with serde_json 1.0.151:

Input Result
HashMap<i32, _> (integer map keys) Ok, keys are stringified: {"7": ...}
f64::NAN, f64::INFINITY Ok, both serialise as null
HashMap<(u8, u8), _> (tuple map keys) Err(RenderError::Json("key must be a string"))
A hand-written Serialize impl that returns an error Err(RenderError::Json(_)) with that impl's message

So the realistic triggers are a map keyed by something that is not string-like, and a hand-written Serialize impl that refuses a value. Both are programmer mistakes in the row type rather than anything a user typed, which is why the error carries a string rather than the original serde_json::Error.

Using both from one row type

A type that feeds both helpers needs both derives, and they are independent:

use rtb_tui::tabled::Tabled;
use serde::Serialize;

#[derive(Tabled, Serialize)]
#[tabled(crate = "rtb_tui::tabled")]
struct Row {
    name: &'static str,
    count: u32,
}

Tabled renders each field through Display unless you tell it otherwise, which rules out nesting: a field whose type is another struct will not derive Tabled without a per-field tabled attribute, even though Serialize handles it happily. A nested shape is the one case where the text and JSON views of the same type genuinely diverge: the table flattens or drops what the JSON keeps.