Skip to content

Theme & Design System

The design system in fireside-tui is a layered stack that translates an abstract theme configuration into every color and layout decision made at render time. The stack flows from user-facing configuration (ThemeFile / .itermcolors) through semantic roles (DesignTokens) into responsive layout helpers (Breakpoint, Spacing, NodeTemplate).

User config / CLI flag
└─► Theme resolution order (CLI > doc meta > user config > defaults)
└─► Theme struct (concrete Color values + syntect theme name)
└─► DesignTokens::from_theme() (semantic roles)
├─► renderer leaf functions (fg/bg per element)
└─► NodeTemplate::compute_areas() (responsive layout)
└─► Breakpoint + Spacing constants

Theme — the concrete color struct

Theme is the lowest-level runtime color carrier. It holds one ratatui::style::Color field per UI role and one syntax_theme: String naming the syntect theme for code blocks.

pub struct Theme {
pub background: Color,
pub foreground: Color,
pub heading_h1: Color,
pub heading_h2: Color,
pub heading_h3: Color,
pub code_background: Color,
pub code_foreground: Color,
pub code_border: Color,
pub block_quote: Color,
pub footer: Color,
pub syntax_theme: String,
}

Theme::default() uses named ratatui colors (Color::Cyan, Color::Reset, etc.) for portability across 16-color and 256-color terminals. The rich One Dark RGB defaults live in DesignTokens::default() instead — they are used when the full design token path is active.

ThemeFile — the JSON overlay

ThemeFile is the serde target for user-authored JSON theme files. Every field is Option<String> annotated with #[serde(default)]. String values are parsed into Color via parse_color():

  • Named colors: "red", "darkgray", "lightcyan" (and snake_case variants)
  • Hex: "#ff0000" (6-digit only)
  • "reset" or "" for Color::Reset (terminal default)

ThemeFile::apply_to(base: &Theme) -> Theme merges only the fields that are Some, leaving unspecified fields at their baseline values. This means a minimal theme file can override just heading_h1 with only one line of JSON and inherit everything else.

Theme resolution order

App::resolve_theme() checks sources from highest to lowest priority and returns the first non-trivial result:

  1. --theme <name|path> CLI flag (resolved against ~/.config/fireside/themes/)
  2. Document meta.theme field in the .json graph file
  3. ~/.config/fireside/config.json theme key
  4. Theme::default()

Hot-reload (triggered by Action::ReloadTheme or filesystem watch) re-runs this resolution and then calls DesignTokens::from_theme on the new result. The render immediately reflects the new palette on the next frame.

DesignTokens — the semantic color layer

DesignTokens expands the 11 concrete fields of Theme into a richer set of 35+ semantic roles organized into four groups.

Base palette (7 roles)

TokenDefault (One Dark)Semantic use
backgroundColor::ResetTerminal base background
surface#282c34Code blocks, panels, elevated cards
primary#61afef (blue)Headings, active borders, selected items
accent#c678dd (purple)Links, interactive elements
muted#5c6370 (dim gray)Borders, separators, dimmed text
error#e06c75 (red)Errors, warnings, destructive actions
success#98c379 (green)Confirmations, positive indicators

On-colors (3 roles)

Text that appears on a colored background needs its own token to guarantee readability. The on-color tokens are used wherever a colored surface needs legible text overlaid on it.

TokenUse
on_backgroundText on the base background
on_surfaceText inside code blocks and panels
on_primaryLabel text on primary-colored elements (e.g., active selection)

Typography tokens (8 roles)

TokenUse
heading_h1H1 color ( underrule)
heading_h2H2 color ( underrule)
heading_h3H3–H6 color
bodyBody prose text
code_fgCode base foreground (overridden per-span by syntect)
code_bgCode block background
quoteblockquote border and text

Chrome tokens (5 roles)

TokenUse
footerProgress bar and status line
border_activeFocused panel border
border_inactiveUnfocused panel border, divider rules
toolbar_bgEditor toolbar background
toolbar_fgEditor toolbar text

syntax_theme: String

The name of the syntect theme to use for code blocks. This is passed through from Theme and consumed by render/code.rs when calling highlight_code. The two-face library provides an extended theme set; the default is "base16-ocean.dark".

Round-trip path

DesignTokens::to_theme() and DesignTokens::from_theme() form a round-trip. Some token fields (e.g., accent, muted, on_primary) have no direct counterpart in the compact Theme struct and are filled in from DesignTokens::default() during the from_theme direction. The test theme_roundtrip in tokens.rs verifies that heading and code tokens survive the round-trip without loss.

Accessibility: WCAG contrast helpers

tokens.rs provides two public functions for contrast checking:

pub fn contrast_ratio(c1: Color, c2: Color) -> f64
pub fn meets_contrast_aa(fg: Color, bg: Color) -> bool // threshold: 4.5:1

Both use the WCAG 2.1 relative luminance formula with correct gamma linearization:

$$L = 0.2126 \cdot R_{\text{lin}} + 0.7152 \cdot G_{\text{lin}} + 0.0722 \cdot B_{\text{lin}}$$

Non-RGB Color variants (named colors, Reset) return 1.0 from relative_luminance because their actual pixel values are terminal-defined and unknowable. The contrast test default_tokens_body_on_background_contrast asserts that the One Dark defaults (body on surface) achieve at least AA.

Custom themes can validate their palettes with cargo test if they wire up a similar assertion.

Breakpoint — responsive layout

Breakpoint::from_size(width, height) maps a terminal Rect to one of three responsive tiers:

BreakpointTerminal dimensions
Compact≤ 80 columns or ≤ 24 rows
Standard81–120 × 25–40
Wide> 120 columns and > 40 rows

The breakpoint is re-evaluated at every frame draw inside App::view. There is no stored breakpoint state; it is derived live from the actual terminal Rect.

Two methods on Breakpoint drive responsive decisions:

MethodCompactStandardWide
content_width_pct()96%85%75%
h_padding()1 cell2 cells4 cells

Tighter centering at wide widths prevents text from stretching across a large monitor — a common problem for terminal presentations on 4K displays.

Spacing — the scale

Spacing is a unit struct with only const values. All margins and padding in the codebase must use these constants rather than literal integers:

ConstantValue
Spacing::XS1 cell
Spacing::SM2 cells
Spacing::MD3 cells
Spacing::LG4 cells
Spacing::XL6 cells

Using a scale rather than ad-hoc numbers keeps spacing harmonious and makes global density changes (e.g., reducing all padding for compact mode) a single-site edit.

NodeTemplate — layout archetypes

NodeTemplate maps a protocol Layout enum variant to a named presentation archetype. The mapping is many-to-one: multiple Layout values that share a visual intent map to the same template.

TemplateMaps from Layout
TitleLayout::Title
TwoColumnLayout::SplitHorizontal
CodeFocusLayout::CodeFocus, Layout::Fullscreen
QuoteLayout::Center
BulletListDefault, Top, SplitVertical, AlignLeft, AlignRight, Blank

TitleSubtitle, ImageCaption, and SpeakerNotes are template-only archetypes reachable via NodeTemplate::from_name, not directly from a Layout variant. They are intended for use in the editor’s template chooser.

TemplateAreas — computed Rect output

NodeTemplate::compute_areas(area, bp) -> TemplateAreas returns:

pub struct TemplateAreas {
pub main: Rect,
pub secondary: Option<Rect>, // right column, caption area, notes panel
pub footer: Rect,
}

Every template calls split_footer(area) first to carve off a 1-row footer:

fn split_footer(area: Rect) -> (Rect, Rect) {
Layout::default()
.direction(Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area)
}

Area calculations per template:

Templatemain areasecondary area
Titlecenter_in(content, 70%, 50%)None
TwoColumnLeft half with gutterRight half
CodeFocusMinimal horizontal padding, full heightNone
QuoteNarrow center columnNone
SpeakerNotes~70% left (content)~30% right (notes)

center_in(area, width_pct, height_pct) positions a rect using three-constraint splits in both axes (margin | content | margin), which is the idiomatic ratatui pattern for centering without absolute pixel math.

iTerm2 color scheme import (design/iterm2.rs)

Fireside can import any of the thousands of color schemes from iterm2colorschemes.com directly into its design token system.

Parser

.itermcolors files are XML plists. Each color is a <dict> with three float keys (Red Component, Green Component, Blue Component) in 0.0–1.0 range. The parser uses the plist crate to deserialize to a HashMap<String, plist::Value>, then walks the expected keys.

Float components are converted to u8 with (value * 255.0).round() as u8.

A 1 MiB file-size guard is applied before parsing:

const MAX_ITERM2_FILE_SIZE_BYTES: u64 = 1_048_576;

Files larger than this return Iterm2Error::FileTooLarge immediately, avoiding unbounded allocation on malformed or crafted inputs.

Mapping

Each relevant iTerm2 key maps to one or more design tokens:

iTerm2 keyDesign tokens populated
Background Colorbackground
Foreground Coloron_background, body
Bold Colorheading_h1, primary
Selection Colorsurface, code_bg
Cursor Colorborder_active
Ansi 1 Color (red)error
Ansi 2 Color (green)success, heading_h2
Ansi 3 Color (yellow)heading_h3
Ansi 4 Color (blue)accent
Ansi 8 Color (bright black)muted, border_inactive

Tokens not covered by any iTerm2 mapping fall back to DesignTokens::default() values, ensuring the resulting token set is always complete and usable.

CLI integration

Terminal window
fireside theme import path/to/MyScheme.itermcolors
fireside theme import path/to/MyScheme.itermcolors --name my-scheme

The import command parses the plist, materializes the full DesignTokens via the mapping, converts to ThemeFile format (JSON), and writes it to ~/.config/fireside/themes/<name>.json. From that point the theme is available by name to all theme resolution paths.