Skip to content

Add a --output text|json flag

Define one row type with both derives

render_table needs Tabled; render_json needs Serialize. A type that feeds both carries both:

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

#[derive(Tabled, Serialize)]
pub struct RepoRow {
    #[tabled(rename = "repository")]
    #[serde(rename = "repository")]
    pub name: String,
    pub branch: String,
    pub dirty: bool,
}

The two rename attributes are independent. Set both, or the table header and the JSON key will disagree — which is exactly the sort of thing nobody notices until a script starts parsing the JSON.

Pick the renderer from the flag

Keep the choice in one place. The command produces rows; something downstream of it decides how they are written:

use rtb_tui::{render_json, render_table, RenderError};
use serde::Serialize;
use tabled::Tabled;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
    Text,
    Json,
}

pub fn render<R: Tabled + Serialize>(
    rows: &[R],
    format: OutputFormat,
) -> Result<String, RenderError> {
    match format {
        OutputFormat::Text => Ok(render_table(rows)),
        OutputFormat::Json => render_json(rows),
    }
}

Print the result with print!, not println!. Both helpers already append a trailing newline, so println! adds a blank line to every command's output.

Handle the empty case before rendering

An empty slice is not an error, and neither helper treats it as one. render_table renders the header and the rule with no body rows; render_json renders []. That asymmetry usually wants a decision at the call site rather than in the renderer:

if rows.is_empty() && format == OutputFormat::Text {
    eprintln!("no repositories matched");
    return Ok(());
}

Note the eprintln!. A "nothing found" message on stdout ends up in whatever is parsing the output; on stderr it reaches the human and leaves the pipe clean. And leave the JSON branch alone — [] is the correct answer to "give me the list" and a script can handle it.

Deal with the one type that will not render as a table

Serialize is happy with nested structs; Tabled is not. A row type with a Vec<String> field, or a field whose type is another struct, will not derive Tabled without a per-field tabled attribute telling it how to flatten the value into a cell.

When that happens you have two honest options, and inventing a third usually ends in a table nobody can read:

  • flatten for the table (tags: String holding a comma-joined list) and accept that the JSON is flat too; or
  • keep two types — a rich one for JSON and a flat projection for the table — and write the From impl between them.

What this does not give you

There is no third format. rtb-tui renders text and JSON, and nothing else; there is no YAML, CSV or template renderer, and adding one means writing it in your own crate. The table style is fixed as well — see Why the table style is not configurable.

Next