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()andTheme::from_yaml_file(). CSS is the recommended format for all new projects.
Supported Attributes
Colors
| Attribute | CSS Property | Description |
|---|---|---|
fg | color | Foreground (text) color |
bg | background | Background 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 Property | Effect |
|---|---|
font-weight: bold | Bold text |
opacity: 0.5 | Dimmed/faint text |
font-style: italic | Italic text |
text-decoration: underline | Underlined text |
text-decoration: blink | Blinking text |
text-decoration: line-through | Strikethrough |
visibility: hidden | Hidden 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:
- Start with base attributes (
bold,gray) - Merge dark overrides (
whitereplacesgray) - 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: a → b → c → 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 Mode | Behavior |
|---|---|
Term | Unknown tags get a ? marker: [unknown?]text[/unknown?] |
Text | Tags stripped (plain text) |
TermDebug | Tags 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 name | Purpose |
|---|---|
table_row_even | Even rows — no background (transparent) |
table_row_odd | Odd rows — subtle gray background shift |
table_row_even_gray | Alias for table_row_even |
table_row_odd_gray | Alias for table_row_odd |
table_row_even_blue | Even rows for blue tint |
table_row_odd_blue | Odd rows — dark navy / lavender bg |
table_row_even_red | Even rows for red tint |
table_row_odd_red | Odd rows — dark crimson / blush bg |
table_row_even_green | Even rows for green tint |
table_row_odd_green | Odd rows — dark forest / mint bg |
table_row_even_purple | Even rows for purple tint |
table_row_odd_purple | Odd 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); }