Content
The Content system separates what you display from how it renders.
F-strings (f"...") produce plain strings. Content strings (c"...") produce
structured ContentNode values that carry styling, layout, and semantic
information. A ContentNode can be rendered to a terminal with ANSI codes, to
HTML with <span> tags, or to plain text with no formatting at all — from the
same source.
Three levels of usage:
- Inline — ad-hoc styling in
c"..."literals - Trait — implement
Contenton a type for reusable rendering - Builder — compose tables, charts, and fragments with
Content.*helpers
Content Strings
Section titled “Content Strings”Content strings use the c prefix and support the same interpolation modes
as f-strings:
| Form | Interpolation | Use case |
|---|---|---|
c"..." | {expr} | General purpose |
c$"..." | ${expr} | Templates with literal braces |
c#"..." | #{expr} | Shell-like templates |
Basic interpolation:
let name = "Alice"print(c"Name: {name}")Inline Styling
Section titled “Inline Styling”Inside c"..." interpolation, append styling hints after a colon:
let score = 95.5print(c"{score: fg(green), fixed(2)}") // green, two decimals
let msg = "ALERT: threshold exceeded"print(c"Alert: {msg: fg(red), bold}") // red bold textStyling hints are comma-separated. Available hints:
fg(color)— foreground colorbg(color)— background colorbold,italic,underline,dim— text decorationfixed(n)— numeric precision (same as f-string format spec)
When the interpolated value implements Content, its render() method runs
first, then inline styles are applied on top.
Content Trait
Section titled “Content Trait”Implement Content to define how any type renders as structured output:
trait Content { method render() -> ContentNode;}Example for a score type:
type Score = number
impl Content for Score { method render() { let sign = if self < 0 { "-" } else { "+" } Content.text(f"{sign}{abs(self):fixed(1)}") .fg(if self < 0 { Color.red } else { Color.green }) }}Now any Score value automatically gets colored output in c"..." strings
and when passed to print(...):
let delta: Score = -12.5print(c"Change: {delta}") // renders as red "-12.5"Custom Types with Charts
Section titled “Custom Types with Charts”Any user-defined type can implement Content to produce rich output, including
charts:
type Sensor { name: string, readings: Array<number>,}
impl Content for Sensor { method render() { Content.chart("line") .add(self.name, self.readings.enumerate()) .title(f"Sensor: {self.name}") }}
// Now print() automatically renders as a chart:let s = Sensor { name: "temp", readings: [20, 22, 21, 23] }print(s) // → renders line chart in terminalThe Content trait dispatch checks for user-defined implementations before falling back to built-in rendering for standard types (strings, numbers, arrays, tables, etc.).
Auto-Table for Collections
Section titled “Auto-Table for Collections”When you print a Vec<T> where T is a struct, Shape automatically renders
it as a table. Each field becomes a column. If a field’s type implements
Content, the trait controls that column’s styling.
type TestResult { name: string, status: string, duration_ms: int, delta: Score}
let results = [ TestResult { name: "auth", status: "PASS", duration_ms: 120, delta: 3.2 }, TestResult { name: "db", status: "FAIL", duration_ms: 450, delta: -8.7 },]
print(c"{results}")Output (terminal):
╭──────┬────────┬─────────────┬────────╮│ name │ status │ duration_ms │ delta │├──────┼────────┼─────────────┼────────┤│ auth │ PASS │ 120 │ +3.2 │ ← green│ db │ FAIL │ 450 │ -8.7 │ ← red╰──────┴────────┴─────────────┴────────╯The delta column is colored because Score implements Content.
Styled Text
Section titled “Styled Text”Colors
Section titled “Colors”The Color namespace provides named colors and RGB:
Color.redColor.greenColor.blueColor.yellowColor.magentaColor.cyanColor.whiteColor.defaultColor.rgb(r, g, b) // 0-255 per channelStyle Methods
Section titled “Style Methods”Style methods chain on any ContentNode:
Content.text("Error") .fg(Color.red) .bg(Color.rgb(40, 0, 0)) .bold() .underline()Available methods:
| Method | Effect |
|---|---|
.fg(color) | Set foreground color |
.bg(color) | Set background color |
.bold() | Bold weight |
.italic() | Italic style |
.underline() | Underline decoration |
.dim() | Dimmed / faint text |
Tables
Section titled “Tables”Build a table explicitly with Content.table():
let records = load_records()Content.table(records) .border(Border.rounded) .max_rows(20)Border Styles
Section titled “Border Styles”| Style | Example |
|---|---|
Border.rounded | ╭──┬──╮ corners with │ ─ lines |
Border.sharp | ┌──┬──┐ corners with │ ─ lines |
Border.heavy | ┏━━┳━━┓ thick lines |
Border.double | ╔══╦══╗ double lines |
Border.minimal | No outer border, ─ row separators only |
Border.none | No borders at all |
Visual reference for Border.rounded (default):
╭──────┬───────╮│ name │ value │├──────┼───────┤│ test │ 142.5 ││ prod │ 378.2 │╰──────┴───────╯Border.sharp:
┌──────┬───────┐│ name │ value │├──────┼───────┤│ test │ 142.5 ││ prod │ 378.2 │└──────┴───────┘Border.heavy:
┏━━━━━━┳━━━━━━━┓┃ name ┃ value ┃┣━━━━━━╋━━━━━━━┫┃ test ┃ 142.5 ┃┃ prod ┃ 378.2 ┃┗━━━━━━┻━━━━━━━┛Border.double:
╔══════╦═══════╗║ name ║ value ║╠══════╬═══════╣║ test ║ 142.5 ║║ prod ║ 378.2 ║╚══════╩═══════╝Table Methods
Section titled “Table Methods”| Method | Description |
|---|---|
.border(style) | Set border style |
.max_rows(n) | Truncate display after n rows |
Column styling comes from field types. If a field type implements Content,
that implementation controls the column’s colors and formatting.
Charts
Section titled “Charts”Build charts with Content.chart():
Content.chart("line") .add("Revenue", [[1, 100], [2, 200], [3, 350]]) .title("Quarterly Revenue") .x_label("Quarter") .y_label("USD")The .add(label, data) method adds a named data channel. Each data point is
an [x, y] pair. Multiple data sets can be added to the same chart:
Content.chart("line") .add("Revenue", [[1, 100], [2, 200], [3, 350]]) .add("Costs", [[1, 80], [2, 90], [3, 120]]) .title("Revenue vs Costs")Chart Types
Section titled “Chart Types”| Type | Use case |
|---|---|
ChartType.line | Time series, trends |
ChartType.bar | Categorical comparison |
ChartType.scatter | Correlation plots |
ChartType.area | Cumulative / stacked values |
ChartType.histogram | Distribution of values |
ChartType.candlestick | Financial OHLC data |
ChartType.boxplot | Statistical distributions |
ChartType.heatmap | Matrix / density data |
ChartType.bubble | Scatter with size dimension |
You can pass a type as a string ("line") or use the ChartType namespace
(ChartType.line):
Content.chart("candlestick") // string formContent.chart(ChartType.line) // namespace formChart Methods
Section titled “Chart Methods”| Method | Description |
|---|---|
.add(label, data) | Add a named data series ([[x, y], ...]) |
.title(text) | Set chart title |
.x_label(text) | Label the x-axis |
.y_label(text) | Label the y-axis |
.width(n) | Width hint in columns |
.height(n) | Height hint in rows |
Channel-Based Data Model
Section titled “Channel-Based Data Model”Internally, chart data is stored as channels — named arrays of numeric
values with roles like "x", "y", "open", "high", "low", "close".
The .add() method creates "x" and "y" channels from [x, y] pairs.
Each chart type requires specific channels:
| Chart Type | Required Channels |
|---|---|
| Line, Area, Scatter | x, y |
| Bar | y |
| Candlestick | x, open, high, low, close |
| Box Plot | x, min, q1, median, q3, max |
| Histogram | values |
| Heatmap | x, y, value |
| Bubble | x, y, size |
Multiple chart nodes are supported:
let fast = Content.chart("line") .add("SMA 20", sma_20_data) .title("Window 20")
let slow = Content.chart("line") .add("SMA 50", sma_50_data) .title("Window 50")
Content.fragment([fast, slow])Terminal Rendering
Section titled “Terminal Rendering”Charts render directly in the terminal using Unicode Braille patterns (U+2800..U+28FF) for line, scatter, and area charts, and block characters (U+2581..U+2588) for bar charts and histograms. This works in any terminal that supports Unicode — no image protocol required.
Revenue vs Costs 350 ┤ ⡠⠊ 275 ┤ ⡠⠤⠤⠤⠤⠤⠤⠔⠉ 200 ┤ ⡠⠤⠤⠤⠤⠤⠔⠉ 125 ┤⠤⠤⠔⠉ 50 ┤ └────────────────────────────In terminals with Kitty image protocol support, charts can render as high-resolution inline images via the shape-viz GPU renderer.
Composition with Fragment
Section titled “Composition with Fragment”Combine multiple content nodes into a single output with Content.fragment():
let summary = Content.text(f"Report as of {today()}") .bold()
let table = Content.table(records) .border(Border.rounded)
let chart = Content.chart(ChartType.line) .title("Trend")
let dashboard = Content.fragment([summary, table, chart])print(dashboard)Fragments render each part sequentially — text, then table, then chart — making it straightforward to build dashboard-style output.
ContentFor<Adapter>
Section titled “ContentFor<Adapter>”For adapter-specific rendering, implement ContentFor<Adapter>:
trait ContentFor<Adapter> { fn render(self, caps: RendererCapabilities): ContentNode}The caps parameter describes what the target supports:
type RendererCapabilities { color: bool, unicode: bool, width: int, interactive: bool}Example — render a chart as SVG for HTML, but as ASCII art for terminal:
impl ContentFor<Html> for DataReport { method render(caps) { Content.fragment([ Content.text(f"<h2>{self.title}</h2>"), Content.chart(ChartType.line) .title("Trend") ]) }}
impl ContentFor<Terminal> for DataReport { method render(caps) { Content.fragment([ Content.text(self.title).bold().underline(), Content.chart(ChartType.line) .title("Trend") .width(caps.width) ]) }}Available Adapters
Section titled “Available Adapters”Terminal— ANSI escape codesHtml— HTML tags and inline stylesMarkdown— GitHub-flavored markdownJson— Structured JSON outputPlain— No formatting
Adapter Behavior Matrix
Section titled “Adapter Behavior Matrix”| Adapter | Text | Table | Chart | Code |
|---|---|---|---|---|
| Terminal | ANSI codes | Unicode box-drawing | Braille/block Unicode | Indented |
| HTML | <span> with styles | <table> | ECharts (interactive) | <pre> |
| Markdown | Plain text | GFM pipe tables | Omitted | Fenced blocks |
| JSON | String field | Object array | ChartSpec JSON | String |
| Plain | No formatting | ASCII borders | Text description | Indented |
When no ContentFor<Adapter> impl exists, the default Content trait
implementation is used with the adapter’s default rendering rules.
Wire Serialization
Section titled “Wire Serialization”ContentNode is a first-class variant of WireValue, the universal wire
format. It is serialized as compact binary (MessagePack) alongside all other
Shape values — no JSON intermediate. The structure looks like this
conceptually:
WireValue::Content(ContentNode::Chart { chart_type: "line", channels: [ { name: "x", label: "x", values: [1, 2, 3] }, { name: "y", label: "Revenue", values: [100, 200, 350] }, ], title: "Quarterly Revenue",})Any consumer that receives a WireValue::Content can re-render the
ContentNode for its own target format. The web playground deserializes
the content node and converts ChartSpec into interactive ECharts
visualizations.