Tabular Layout

Standout helps you create aligned, readable output for lists, logs, and tabular data.

Choose your path:

Quick Start: The col Filter

For simple alignment, use the col filter directly in templates:

{% for entry in entries %}
{{ entry.id | col(8) }}  {{ entry.name | col(20) }}  {{ entry.status | col(10) }}
{% endfor %}

Output:

abc123    Alice Johnson         active
def456    Bob Smith             pending
ghi789    Carol Williams        done

The col filter:

  • Pads short values to the specified width
  • Truncates long values with
  • Handles Unicode correctly (CJK characters count as 2 columns)

Alignment

{{ value | col(10) }}                 {# Left-aligned (default) #}
{{ value | col(10, align="right") }}  {# Right-aligned #}
{{ value | col(10, align="center") }} {# Centered #}
left......
....right.
..center..

Truncation Position

When content is too long, choose where to cut:

{{ path | col(15) }}                        {# "Very long pa…" (default: end) #}
{{ path | col(15, truncate="start") }}      {# "…ng/path/file" #}
{{ path | col(15, truncate="middle") }}     {# "Very l…h/file" #}

Truncate middle is useful for paths where both start and end matter.

Custom Ellipsis

{{ value | col(10, ellipsis="...") }}   {# "Hello W..." instead of "Hello W…" #}
{{ value | col(10, ellipsis="→") }}     {# "Hello Wor→" #}

Structured Layout

When you need consistent column widths across complex output, define a layout spec.

Defining Columns in Templates

Use the tabular() function to create a formatter:

{% set t = tabular([
    {"name": "id", "width": 8},
    {"name": "author", "width": 20},
    {"name": "message", "width": "fill"},
    {"name": "date", "width": 10, "align": "right"}
]) %}

{% for commit in commits %}
{{ t.row([commit.id, commit.author, commit.message, commit.date]) }}
{% endfor %}

Output:

a1b2c3d4  Alice Johnson         Add new login feature             2024-01-15
e5f6g7h8  Bob Smith             Fix authentication bug            2024-01-14
i9j0k1l2  Carol Williams        Update dependencies               2024-01-13

Width Options

WidthMeaningExample
8Exactly 8 columnsIDs, short codes
{"min": 10}At least 10, grows to fitNames, titles
{"min": 10, "max": 30}Between 10 and 30Bounded growth
"fill"Takes remaining spaceDescriptions
"2fr"2 parts of remaining (vs 1fr)Proportional
{% set t = tabular([
    {"name": "id", "width": 8},                    {# Fixed #}
    {"name": "name", "width": {"min": 10}},        {# Grows to fit #}
    {"name": "desc", "width": "fill"},             {# Takes the rest #}
]) %}

Anchoring Columns

Put columns at the right edge:

{% set t = tabular([
    {"name": "name", "width": 20},
    {"name": "path", "width": "fill"},
    {"name": "size", "width": 8, "anchor": "right"},  {# Stays at right edge #}
]) %}

Output:

document.txt          /home/user/docs/                    1.2 MB
image.png             /home/user/photos/vacation/         4.5 MB

The size column is anchored to the right edge. The path column fills the gap.

Handling Long Content

Choose what happens when content exceeds the column width:

{% set t = tabular([
    {"name": "path", "width": 30, "overflow": "truncate"},  {# Default: "Very long…" #}
    {"name": "desc", "width": 30, "overflow": "wrap"},      {# Wrap to multiple lines #}
]) %}

Truncate (default)

{"overflow": "truncate"}                      {# Truncate at end #}
{"overflow": {"truncate": {"at": "middle"}}}  {# Keep start and end #}
{"overflow": {"truncate": {"marker": "..."}}} {# Custom ellipsis #}

Wrap

Content wraps to multiple lines:

abc123  This is a very long       active
        description that wraps
        to multiple lines
def456  Short description         done
{"name": "desc", "width": 25, "overflow": "wrap"}
{"name": "desc", "width": 25, "overflow": {"wrap": {"indent": 2}}}  {# Continuation indent #}

Extracting Fields from Objects

When column names match struct fields, use row_from():

{% set t = tabular([
    {"name": "id", "width": 8},
    {"name": "title", "width": 30},
    {"name": "status", "width": 10}
]) %}

{% for item in items %}
{{ t.row_from(item) }}  {# Automatically extracts item.id, item.title, item.status #}
{% endfor %}

For nested fields, use key:

{% set t = tabular([
    {"name": "Author", "key": "author.name", "width": 20},
    {"name": "Email", "key": "author.email", "width": 30}
]) %}

Column Styles

Apply styles to entire columns:

{% set t = tabular([
    {"name": "id", "width": 8, "style": "muted"},
    {"name": "name", "width": 20, "style": "bold"},
    {"name": "status", "width": 10}  {# No automatic style #}
]) %}

The style value wraps content in style tags: [muted]abc123[/muted]

For dynamic styles (style based on value):

{% for item in items %}
{{ t.row([item.id, item.name, item.status | style_as(item.status)]) }}
{% endfor %}

This applies [pending]pending[/pending] or [done]done[/done] based on the actual status value.


Defining Layout in Rust

For reusable layouts or when you need full control:

#![allow(unused)]
fn main() {
use standout::tabular::{TabularSpec, Col};

let spec = TabularSpec::builder()
    .column(Col::fixed(8).named("id"))
    .column(Col::min(10).named("name").style("author"))
    .column(Col::fill().named("description").wrap())
    .column(Col::fixed(10).named("status").anchor_right().right())
    .separator("  ")
    .build();
}

Pass to template context:

#![allow(unused)]
fn main() {
let formatter = TabularFormatter::new(&spec, 80);
ctx.insert("table", formatter);
}

Shorthand Column Constructors

#![allow(unused)]
fn main() {
Col::fixed(8)          // Exactly 8 columns
Col::min(10)           // At least 10, grows to fit
Col::bounded(10, 30)   // Between 10 and 30
Col::fill()            // Takes remaining space
Col::fraction(2)       // 2 parts of remaining (2fr)

// Chained modifiers
Col::fixed(10)
    .named("status")       // Column name
    .right()               // Align right
    .center()              // Align center
    .anchor_right()        // Position at right edge
    .wrap()                // Overflow: wrap
    .clip()                // Overflow: hard cut
    .truncate_middle()     // Truncate in middle
    .style("pending")      // Apply style
    .null_repr("N/A")      // Display for missing values
}

Tables: Headers and Borders

For output with explicit headers, separators, and borders:

{% set t = table([
    {"name": "ID", "key": "id", "width": 8},
    {"name": "Author", "key": "author", "width": 20},
    {"name": "Message", "key": "message", "width": "fill"}
], border="rounded", header_style="bold") %}

{{ t.header_row() }}
{{ t.separator_row() }}
{% for commit in commits %}
{{ t.row([commit.id, commit.author, commit.message]) }}
{% endfor %}
{{ t.bottom_border() }}

Output:

╭──────────┬──────────────────────┬────────────────────────────────╮
│ ID       │ Author               │ Message                        │
├──────────┼──────────────────────┼────────────────────────────────┤
│ a1b2c3d4 │ Alice Johnson        │ Add new login feature          │
│ e5f6g7h8 │ Bob Smith            │ Fix authentication bug         │
│ i9j0k1l2 │ Carol Williams       │ Update dependencies            │
╰──────────┴──────────────────────┴────────────────────────────────╯

Table Border Styles

border="none"     {# No borders #}
border="ascii"    {# +--+--+  ASCII compatible #}
border="light"    {# ┌──┬──┐  Light box drawing #}
border="heavy"    {# ┏━━┳━━┓  Heavy box drawing #}
border="double"   {# ╔══╦══╗  Double lines #}
border="rounded"  {# ╭──┬──╮  Rounded corners #}

Row Separators

Add lines between data rows:

{% set t = table(columns, border="light", row_separator=true) %}
┌──────────┬──────────────────────┐
│ ID       │ Name                 │
├──────────┼──────────────────────┤
│ abc123   │ Alice                │
├──────────┼──────────────────────┤
│ def456   │ Bob                  │
└──────────┴──────────────────────┘

Simple Table Rendering

For simple cases, render everything in one call using render_all():

{% set t = table([
    {"width": 8, "header": "ID"},
    {"width": 20, "header": "Author"}
], border="light", header=["ID", "Author"]) %}
{{ t.render_all(commits) }}

In Rust: Full Table API

#![allow(unused)]
fn main() {
use standout::tabular::{Table, TabularSpec, Col, BorderStyle};

let spec = TabularSpec::builder()
    .column(Col::fixed(8).header("ID"))
    .column(Col::min(10).header("Author"))
    .column(Col::fill().named("Message"))
    .build();

let table = Table::new(spec, 80)
    .header_from_columns()        // Use column headers/names as headers
    .header_style("table-header")
    .border(BorderStyle::Rounded);

// Render full table
let data = vec![
    vec!["a1b2c3d4", "Alice", "Add login"],
    vec!["e5f6g7h8", "Bob", "Fix bug"],
];
let output = table.render(&data);
println!("{}", output);

// Or render parts manually
println!("{}", table.header_row());
println!("{}", table.separator_row());
for row in &data {
    println!("{}", table.row(row));
}
println!("{}", table.bottom_border());
}

Terminal Width

By default, Standout auto-detects terminal width. Override for testing or fixed-width output:

{% set t = tabular(columns, width=80) %}  {# Fixed 80 columns #}
#![allow(unused)]
fn main() {
let formatter = TabularFormatter::new(&spec, 80);  // Fixed width
}

Helper Filters

For simpler use cases, these filters work standalone:

Padding

{{ value | pad_right(10) }}   {# "hello     " - left align #}
{{ value | pad_left(10) }}    {# "     hello" - right align #}
{{ value | pad_center(10) }}  {# "  hello   " - center #}

Truncation

{{ path | truncate_at(20) }}                  {# End: "/very/long/path…" #}
{{ path | truncate_at(20, "middle") }}        {# Middle: "/very…/file" #}
{{ path | truncate_at(20, "start") }}         {# Start: "…long/path/file" #}
{{ path | truncate_at(20, "end", "...") }}    {# Custom marker #}

Display Width

Check visual width (handles Unicode):

{% if name | display_width > 20 %}
  {{ name | truncate_at(20) }}
{% else %}
  {{ name }}
{% endif %}

Unicode and ANSI

The tabular system correctly handles:

  • CJK characters: 日本語 counts as 6 columns (2 each)
  • Combining marks: café is 4 columns (é combines)
  • ANSI codes: Preserved in output, not counted in width
{{ "Hello 日本" | col(12) }}  →  "Hello 日本  " (10 display columns + 2 padding)

Styled text maintains styles through truncation:

{{ "[red]very long red text[/red]" | col(10) }}  →  "[red]very lon…[/red]"

Missing Values

Set what displays for null/empty values:

{% set t = tabular([
    {"name": "email", "width": 30, "null_repr": "N/A"}
]) %}

Or in templates with Jinja's default filter:

{{ entry.email | default("N/A") | col(30) }}

Complete Example

A git log-style output:

{% set t = tabular([
    {"name": "hash", "width": 8, "style": "muted"},
    {"name": "author", "width": {"min": 15, "max": 25}, "style": "author"},
    {"name": "message", "width": "fill"},
    {"name": "date", "width": 10, "anchor": "right", "align": "right", "style": "date"}
], separator=" │ ") %}

{% for commit in commits %}
{{ t.row([commit.hash[:8], commit.author, commit.message, commit.date]) }}
{% endfor %}

Output (80 columns):

a1b2c3d4 │ Alice Johnson   │ Add new login feature with OAuth    │ 2024-01-15
e5f6g7h8 │ Bob Smith       │ Fix authentication bug              │ 2024-01-14
i9j0k1l2 │ Carol Williams  │ Update dependencies and refactor    │ 2024-01-13

With styling (in terminal):

[muted]a1b2c3d4[/muted] │ [author]Alice Johnson[/author]   │ Add new login feature with OAuth    │ [date]2024-01-15[/date]

Summary

NeedSolution
Simple column alignment{{ value | col(width) }}
Multiple columns, same widthstabular([...]) with t.row([...])
Auto field extractiont.row_from(object)
Headers and borderstable([...])
Right-edge columnsanchor: "right"
Long content wrappingoverflow: "wrap"
Proportional widthswidth: "2fr"

Reference

col Filter

{{ value | col(width, align=?, truncate=?, ellipsis=?) }}
ParamValuesDefault
widthintegerrequired
align"left", "right", "center""left"
truncate"end", "start", "middle""end"
ellipsisstring"…"

Column Spec

{
  "name": "string",
  "key": "field.path",
  "width": 8 | {"min": 5} | {"min": 5, "max": 20} | "fill" | "2fr",
  "align": "left" | "right" | "center",
  "anchor": "left" | "right",
  "overflow": "truncate" | "wrap" | "clip" | {"truncate": {...}} | {"wrap": {...}},
  "style": "style-name",
  "null_repr": "-"
}

tabular() Function

{% set t = tabular(columns, separator=?, width=?) %}
{{ t.row([values]) }}
{{ t.row_from(object) }}

table() Function

{% set t = table(columns, border=?, header=?, header_style=?, row_separator=?, width=?) %}
{{ t.header_row() }}
{{ t.separator_row() }}
{{ t.row([values]) }}
{{ t.row_from(object) }}
{{ t.top_border() }}
{{ t.bottom_border() }}
{{ t.render_all(rows) }}

Border Styles

ValueExample
"none"No borders
"ascii"+--+--+
"light"┌──┬──┐
"heavy"┏━━┳━━┓
"double"╔══╦══╗
"rounded"╭──┬──╮