The Styling System

standout-render uses a theme-based styling system where named styles are applied to content through bracket notation tags. Instead of embedding ANSI codes in your templates, you define semantic style names (error, title, muted) and let the theme decide the visual representation.

This separation provides several benefits:

  • Readability: Templates use meaningful names, not escape codes
  • Maintainability: Change colors in one place, update everywhere
  • Adaptability: Themes can respond to light/dark mode automatically
  • Consistency: Enforce visual hierarchy across your application

Themes

A Theme is a named collection of styles. Each style maps a name (like title or error) to visual attributes (bold cyan, dim red, etc.).

CSS Themes

Define styles in standard CSS syntax — a subset of CSS Level 3 tailored for terminals:

/* theme.css */
.title {
    color: cyan;
    font-weight: bold;
}

.error {
    color: red;
    font-weight: bold;
}

.muted {
    opacity: 0.5;  /* maps to dim */
}

.success {
    color: green;
}

/* Shorthand works too */
.warning { color: yellow; }

Load CSS themes:

#![allow(unused)]
fn main() {
use standout_render::Theme;

let theme = Theme::from_css(css_content)?;
let theme = Theme::from_css_file("styles/theme.css")?;
}

CSS gives you syntax highlighting in editors, linting tools, and familiarity for web developers.

Programmatic Themes

Build themes in code using the builder pattern:

#![allow(unused)]
fn main() {
use standout_render::Theme;
use console::Style;

let theme = Theme::new()
    .add("title", Style::new().bold().cyan())
    .add("error", Style::new().red().bold())
    .add("muted", Style::new().dim())
    .add("success", Style::new().green());
}

Legacy format: YAML themes are still supported via Theme::from_yaml() and Theme::from_yaml_file(). CSS is the recommended format for all new projects.


Supported Attributes

Colors

AttributeCSS PropertyDescription
fgcolorForeground (text) color
bgbackgroundBackground color

Color Formats

/* Named colors (16 ANSI colors) */
.example { color: red; }
.example { color: green; }
.example { color: cyan; }
.example { color: magenta; }
.example { color: yellow; }
.example { color: white; }
.example { color: black; }

/* Bright variants */
.example { color: bright_red; }
.example { color: bright_green; }

/* 256-color palette (0-255) */
.example { color: 208; }

/* RGB hex */
.example { color: #ff6b35; }
.example { color: #f63; }     /* shorthand */

/* Theme-relative cube colors */
.example { color: cube(60%, 20%, 0%); }

Cube colors express a position in a color cube whose 8 corners are the base ANSI colors of the user's terminal theme. The same cube(60%, 20%, 0%) produces earthy tones in Gruvbox, pastels in Catppuccin, and muted shades in Solarized. Interpolation is done in CIE LAB space for perceptually uniform gradients. Attach a palette to a theme with Theme::with_palette().

Text Attributes

CSS PropertyEffect
font-weight: boldBold text
opacity: 0.5Dimmed/faint text
font-style: italicItalic text
text-decoration: underlineUnderlined text
text-decoration: blinkBlinking text
text-decoration: line-throughStrikethrough
visibility: hiddenHidden text

Adaptive Styles (Light/Dark Mode)

Terminal applications run in both light and dark environments. A color that looks great on a dark background may be illegible on a light one. standout-render solves this with adaptive styles.

How It Works

Instead of defining separate "light theme" and "dark theme" files, you define mode-specific overrides at the style level:

.panel {
    font-weight: bold;
    color: gray;        /* Default/fallback */
}

@media (prefers-color-scheme: light) {
    .panel { color: black; }   /* Override for light mode */
}

@media (prefers-color-scheme: dark) {
    .panel { color: white; }   /* Override for dark mode */
}

When resolving panel in dark mode:

  1. Start with base attributes (bold, gray)
  2. Merge dark overrides (white replaces gray)
  3. Result: bold white text

This is efficient: most styles (bold, italic, semantic colors like green/red) look fine in both modes. Only a handful need adjustment—typically foreground colors for contrast.

Programmatic API

#![allow(unused)]
fn main() {
use standout_render::Theme;
use console::{Style, Color};

let theme = Theme::new()
    .add_adaptive(
        "panel",
        Style::new().bold(),                     // Base (shared)
        Some(Style::new().fg(Color::Black)),     // Light mode
        Some(Style::new().fg(Color::White)),     // Dark mode
    );
}

Color Mode Detection

standout-render auto-detects the OS color scheme:

#![allow(unused)]
fn main() {
use standout_render::{detect_color_mode, ColorMode};

let mode = detect_color_mode();
match mode {
    ColorMode::Light => println!("Light mode"),
    ColorMode::Dark => println!("Dark mode"),
}
}

Override for testing:

#![allow(unused)]
fn main() {
use standout_render::set_theme_detector;

set_theme_detector(|| ColorMode::Dark);  // Force dark mode
}

Style Aliasing

Aliases let semantic names resolve to visual styles. This is useful when multiple concepts share the same appearance:

#![allow(unused)]
fn main() {
let theme = Theme::new()
    // Define the visual style once
    .add("title", Style::new().bold().cyan())
    // Aliases — pass a string to reference another style by name
    .add("commit-message", "title")
    .add("section-header", "title")
    .add("heading", "title");
}

Now [commit-message], [section-header], and [heading] all render identically to [title].

Benefits:

  • Templates use meaningful, context-specific names
  • Visual changes propagate automatically
  • Refactoring visual design doesn't touch templates

Aliases can chain: abc → concrete style. Cycles are detected and rejected at load time.


Unknown Style Tags

When a template references a style not defined in the theme, standout-render handles it gracefully:

Output ModeBehavior
TermUnknown tags get a ? marker: [unknown?]text[/unknown?]
TextTags stripped (plain text)
TermDebugTags preserved as-is

The ? marker helps catch typos during development without crashing production apps.

Validation

For strict checking at startup:

#![allow(unused)]
fn main() {
use standout_render::validate_template;

let errors = validate_template(template, &sample_data, &theme);
if !errors.is_empty() {
    for error in errors {
        eprintln!("Unknown style: {}", error.tag_name);
    }
    std::process::exit(1);
}
}

Built-in Styles

Theme::default() includes adaptive styles for alternating table row backgrounds. These are used automatically when you pass row_styles=true (or a tint name) to the table() template function.

Style namePurpose
table_row_evenEven rows — no background (transparent)
table_row_oddOdd rows — subtle gray background shift
table_row_even_grayAlias for table_row_even
table_row_odd_grayAlias for table_row_odd
table_row_even_blueEven rows for blue tint
table_row_odd_blueOdd rows — dark navy / lavender bg
table_row_even_redEven rows for red tint
table_row_odd_redOdd rows — dark crimson / blush bg
table_row_even_greenEven rows for green tint
table_row_odd_greenOdd rows — dark forest / mint bg
table_row_even_purpleEven rows for purple tint
table_row_odd_purpleOdd rows — dark plum / lilac bg

All odd-row styles are adaptive: they resolve to a dark variant when the terminal is in dark mode, and a light variant in light mode. You can override any of these by defining the same style name in your theme.


Best Practices

Semantic, Presentation, and Visual Layers

Organize your styles in three conceptual layers:

1. Visual primitives (low-level appearance):

._cyan-bold { color: cyan; font-weight: bold; }
._dim { opacity: 0.5; }
._red-bold { color: red; font-weight: bold; }

2. Presentation roles (UI concepts — use aliases in code):

#![allow(unused)]
fn main() {
theme.add("heading", "_cyan-bold")
     .add("secondary", "_dim")
     .add("danger", "_red-bold");
}

3. Semantic names (domain concepts — aliases to presentation):

#![allow(unused)]
fn main() {
// In templates, use these
theme.add("task-title", "heading")
     .add("task-status-done", "success")
     .add("task-status-pending", "warning")
     .add("error-message", "danger");
}

Templates use semantic names (task-title), which resolve to presentation roles (heading), which resolve to visual primitives (_cyan-bold).

This layering lets you:

  • Refactor visuals without touching templates
  • Maintain consistency across domains
  • Document the purpose of each style

Naming Conventions

/* Good: descriptive, semantic */
.error-message { ... }
.file-path { ... }
.command-name { ... }

/* Avoid: visual descriptions */
.red-text { ... }
.bold-cyan { ... }

Keep Themes Focused

One theme per "look". Don't mix concerns:

styles/
├── default.css          # your app's default look
├── colorblind.css       # accessibility variant
└── monochrome.css       # for piped output

API Reference

Theme Creation

#![allow(unused)]
fn main() {
// From CSS string
let theme = Theme::from_css(css_str)?;

// From CSS file (hot reload in debug)
let theme = Theme::from_css_file(path)?;

// Empty theme (for programmatic building)
let theme = Theme::new();

// Legacy: YAML is still supported
let theme = Theme::from_yaml(yaml_str)?;
let theme = Theme::from_yaml_file(path)?;
}

Adding Styles

#![allow(unused)]
fn main() {
// Static style
theme.add("name", Style::new().bold());

// Adaptive style
theme.add_adaptive("name", base_style, light_override, dark_override);

// Alias
theme.add("alias", "target_style");
}

Resolving Styles

#![allow(unused)]
fn main() {
// Get resolved style for current color mode
let style: Option<Style> = theme.get("title");

// Get style for specific mode
let style = theme.get_for_mode("panel", ColorMode::Dark);
}

Color Mode

#![allow(unused)]
fn main() {
use standout_render::{detect_color_mode, set_theme_detector, ColorMode};

// Auto-detect
let mode = detect_color_mode();

// Override (for testing)
set_theme_detector(|| ColorMode::Light);
}