Themes

Themes control the visual appearance of the TUI, including colors for text, borders, messages, code blocks, and interactive elements. Agent Air includes two built-in themes and supports creating custom themes for brand consistency or personal preference.

A well-designed theme improves readability and reduces eye strain during extended use. The theming system applies consistent styling across all widgets, ensuring visual coherence throughout the interface. Whether you use the built-in themes or create your own, the same properties control appearance everywhere.


Built-in Themes

Agent Air provides two built-in themes optimized for different terminal backgrounds. These themes have been carefully designed to provide good contrast and readability while looking pleasant across a variety of terminal emulators.

Both themes include complete styling for all UI elements. Switching between them demonstrates the full scope of what themes control, from subtle details like timestamp colors to prominent elements like message role prefixes.

NameDisplay NameDescription
darkDarkOptimized for dark terminal backgrounds
lightLightOptimized for light terminal backgrounds

The dark theme is the default. Both themes provide complete styling for all UI elements including chat messages, markdown rendering, tool execution, panels, and popups.


Setting the Theme

Set the initial theme when configuring the TUI using the with_theme method. The theme name must match one of the registered themes—the built-in themes are always available.

Choosing a theme at startup ensures the interface looks correct from the first frame. Users can switch themes later, but starting with the right theme avoids a flash of incorrect colors.

use agent_air::AgentAir;
use agent_air::tui::TuiRunner;

AgentAir::with_config(&config)?
    .into_tui()
    .with_theme("dark")
    .run()?;

You can also set the theme programmatically at startup using the initialization functions:

use agent_air::tui::themes::{get_theme, init_theme};

if let Some(theme) = get_theme("light") {
    init_theme("light", theme);
}

Runtime Theme Switching

Themes can be changed while the application is running, allowing users to adjust the appearance based on their environment or preference. The change takes effect on the next render cycle, providing immediate visual feedback.

Runtime switching is useful when users move between different lighting conditions or simply want to try different looks without restarting the application.

use agent_air::tui::themes::{get_theme, set_theme};

if let Some(theme) = get_theme("light") {
    set_theme("light", theme);
}

Users can also switch themes interactively using the theme picker widget, which is available via the /theme command in the default configuration.


Getting the Current Theme

Access the current theme for use in custom widgets or other code that needs to style content consistently. The theme provides all the style information needed to render themed content.

Custom widgets should always use theme properties rather than hardcoded colors. This ensures they remain visually consistent when users switch themes.

use agent_air::tui::themes::{theme, current_theme_name};

// Get the current theme
let current = theme();
// Use current.text, current.border, etc.

// Get the theme name
let name = current_theme_name();
println!("Current theme: {}", name);

The theme() function returns a clone of the current theme. If no theme has been set, it returns the default theme.


Theme Lookup Functions

The theme registry provides functions for accessing themes programmatically. These functions let you list available themes, look up specific themes by name, and access theme metadata.

Use these functions when building theme-related UI, such as a custom theme picker, or when you need to access themes before the TUI has fully initialized.

use agent_air::tui::themes::themes::{get_theme, list_themes, THEMES};

// Get a specific theme by name
if let Some(theme) = get_theme("dark") {
    // Use the theme
}

// List all available theme names
let names: Vec<&'static str> = list_themes();

// Access theme metadata
for info in THEMES {
    println!("{} ({}) - {}",
        info.name,
        info.display_name,
        if info.is_dark { "dark" } else { "light" }
    );
}

Theme Structure

The Theme struct contains approximately 50 style properties organized by category. Each property is a Style object (or Modifier for text formatting) that can specify foreground color, background color, and text modifiers.

Understanding the theme structure helps when creating custom themes or debugging styling issues. Properties are grouped logically by the UI elements they affect.

Base Styles

Base styles provide the foundation for the entire interface. The background style fills the screen, while the text style is the default for any content without a more specific style.

PropertyPurpose
backgroundBackground color for the entire UI
textDefault text color

Border Styles

Border styles define the visual boundaries between UI elements. Focused borders indicate which element has keyboard focus, helping users understand where their input will go.

PropertyPurpose
borderDefault border color
border_focusedBorder color for focused elements

Message Role Styles

Message role styles distinguish between different participants in the conversation. Clear visual differentiation helps users quickly scan conversations and identify who said what.

PropertyPurpose
user_prefixPrefix for user messages
system_prefixPrefix for system messages
assistant_prefixPrefix for assistant messages
timestampMessage timestamps

Markdown Styles

Markdown styles control how formatted text appears in assistant responses. Good markdown styling improves readability of structured content like code blocks, lists, and headings.

PropertyTypePurpose
boldModifierBold text
italicModifierItalic text
strikethroughModifierStrikethrough text
inline_codeStyleInline code snippets
link_textStyleLink display text
link_urlStyleLink URL
heading_1 through heading_4StyleHeading levels
code_blockStyleFenced code block content

Tool Execution Styles

Tool execution styles provide visual feedback about tool state. Different colors for executing, completed, and failed states help users quickly understand what happened without reading detailed output.

PropertyPurpose
tool_headerTool name and header
tool_executingTool in progress indicator
tool_completedTool completed successfully
tool_failedTool execution failed

UI Panel Styles

Panel styles apply to interactive elements like permission panels, question panels, and buttons. These styles need to work together to create a cohesive interface for user interactions.

PropertyPurpose
help_textHelp and instruction text
muted_textDe-emphasized text
focused_textCurrently focused text
selectedSelected option
unselectedUnselected option
button_confirmConfirm button
button_cancelCancel button
warningWarning messages

Creating Custom Themes

Create custom themes using the define_theme! macro or by manually constructing a Theme struct. Custom themes let you match your organization’s brand colors, optimize for specific terminal environments, or simply express personal preference.

The macro approach is quickest when you have a color palette in mind, while manual construction gives complete control over every style property.

Using the define_theme! Macro

The macro generates a complete theme from a 13-color palette. This approach ensures all theme properties are set consistently based on your chosen colors, avoiding the tedium of configuring 50+ properties individually.

Each color parameter has a semantic meaning that guides how it’s applied throughout the theme. Understanding these meanings helps you choose colors that work well together.

use agent_air::define_theme;
use agent_air::tui::themes::theme::Theme;

define_theme! {
    bg: (0x1a, 0x1b, 0x26),      // Primary background
    bg_alt: (0x16, 0x16, 0x1e),  // Secondary background
    fg: (0xc0, 0xca, 0xf5),      // Primary text
    fg_alt: (0x56, 0x5f, 0x89),  // Muted text
    base4: (0x3b, 0x40, 0x61),   // Borders
    red: (0xf7, 0x76, 0x8e),     // Errors
    orange: (0xff, 0x9e, 0x64),  // Warnings
    yellow: (0xe0, 0xaf, 0x68),  // Highlights
    green: (0x9e, 0xce, 0x6a),   // Success
    cyan: (0x7d, 0xcf, 0xff),    // Links
    blue: (0x7a, 0xa2, 0xf7),    // User messages
    magenta: (0xbb, 0x9a, 0xf7), // Assistant messages
    violet: (0x9d, 0x7c, 0xd8)   // Special elements
}

The macro maps these colors to all 50+ theme properties automatically.

Color Parameter Mapping

This table shows how each macro parameter maps to theme elements. Use this as a guide when choosing colors to ensure they work well for their intended purposes.

ParameterMaps To
bgMain background
bg_altCode blocks, selected item backgrounds
fgPrimary text, headings
fg_altMuted text, timestamps, help text
base4Borders, separators
redErrors, failed states, cancel buttons
orangeTool headers, warnings
yellowSystem messages, inline code, focus indicators
greenSuccess states, connected indicator, confirm buttons
cyanFocused elements, links, selected items
blueUser messages
magentaAssistant messages, categories
violetLink URLs, special elements

Manual Theme Construction

For complete control, construct the Theme struct directly. This approach is necessary when you need styles that the macro can’t express, such as different modifiers on certain elements or background colors that don’t fit the standard pattern.

Start with the default theme and modify specific properties to reduce boilerplate while ensuring all properties have valid values.

use ratatui::style::{Color, Modifier, Style};
use agent_air::tui::themes::theme::Theme;

fn custom_theme() -> Theme {
    Theme {
        background: Style::default().bg(Color::Rgb(26, 27, 38)),
        text: Style::default().fg(Color::Rgb(192, 202, 245)),
        border: Style::default().fg(Color::Rgb(59, 64, 97)),
        border_focused: Style::default().fg(Color::Rgb(125, 207, 255)),

        user_prefix: Style::default().fg(Color::Rgb(122, 162, 247)),
        assistant_prefix: Style::default().fg(Color::Rgb(187, 154, 247)),
        system_prefix: Style::default().fg(Color::Rgb(224, 175, 104)),
        timestamp: Style::default().fg(Color::Rgb(86, 95, 137)),

        bold: Modifier::BOLD,
        italic: Modifier::ITALIC,
        inline_code: Style::default().fg(Color::Rgb(224, 175, 104)),

        heading_1: Style::default()
            .fg(Color::Rgb(125, 207, 255))
            .add_modifier(Modifier::BOLD),

        // ... remaining properties
        ..Default::default()
    }
}

Registering Custom Themes

Use a custom theme by initializing it at startup. The theme becomes the active theme immediately and remains available for the duration of the application.

Register your theme early in the application lifecycle, before any rendering occurs. This ensures the theme is available when the TUI initializes.

use agent_air::tui::themes::init_theme;

fn main() {
    let my_theme = custom_theme();
    init_theme("my-custom-theme", my_theme);

    // Start agent...
}

Extending Built-in Themes

Modify an existing theme by cloning it and changing specific properties. This approach lets you make targeted adjustments without recreating the entire theme from scratch.

Extending built-in themes is useful when you like most of a theme but want to adjust specific elements, such as using your brand color for user messages or increasing contrast on certain elements.

use agent_air::tui::themes::themes::get_theme;
use ratatui::style::{Color, Style};

fn modified_dark() -> Theme {
    let mut theme = get_theme("dark").unwrap();

    // Customize specific styles
    theme.user_prefix = Style::default().fg(Color::Rgb(100, 200, 255));
    theme.heading_1 = Style::default().fg(Color::Rgb(255, 100, 100));

    theme
}

Using Themes in Widgets

Widgets receive the theme in their render method. Use theme properties for all styling to ensure visual consistency with the rest of the interface. Hardcoding colors breaks theme switching and creates visual inconsistency.

When building custom widgets, resist the temptation to use direct color values. The small additional effort of using theme properties pays off in consistent appearance and proper theme support.

fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme) {
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(theme.border);

    let text = Paragraph::new("Hello")
        .style(theme.text)
        .block(block);

    frame.render_widget(text, area);
}

Style Reference

Styles are built using Style::default() and method chaining. Understanding style construction helps when creating custom themes or troubleshooting appearance issues.

Styles combine foreground color, background color, and modifiers. You can set any combination of these properties, and unset properties inherit from the parent context.

use ratatui::style::{Color, Modifier, Style};

// Foreground color only
Style::default().fg(Color::Rgb(255, 128, 0))

// Background color only
Style::default().bg(Color::Rgb(30, 30, 30))

// Both colors
Style::default().fg(Color::White).bg(Color::Black)

// With bold modifier
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)

// Multiple modifiers
Style::default()
    .fg(Color::Yellow)
    .add_modifier(Modifier::BOLD | Modifier::ITALIC)

Available Modifiers

Modifiers change how text is rendered without affecting its color. Not all terminals support all modifiers; unsupported modifiers are typically ignored rather than causing errors.

ModifierEffect
BOLDBold text
ITALICItalic text
UNDERLINEDUnderlined text
CROSSED_OUTStrikethrough text
DIMDimmed text
REVERSEDReversed foreground/background

Thread Safety

The theme system uses RwLock for thread-safe access. Multiple widgets can read the theme simultaneously during rendering, while theme switching acquires an exclusive write lock. This design allows efficient concurrent reads while ensuring atomic updates.

In practice, you don’t need to worry about thread safety when using the theme functions. The locking is handled internally, and the API is designed to be safe in multithreaded contexts.

// Safe for concurrent reads
let theme = theme();  // Multiple widgets can call this

// Theme switching is exclusive
set_theme("light", new_theme);  // Blocks until readers complete

If a lock becomes poisoned (rare, indicates a panic), functions return the default theme rather than panicking. This ensures the application can continue operating even in unusual circumstances.


Theme Picker Widget

The built-in theme picker provides an interactive way for users to switch themes. It displays all available themes with their names and applies selections immediately, giving users instant feedback on their choice.

The picker is an overlay widget that appears on top of the main content. Users navigate with arrow keys and select with Enter. Pressing Escape closes the picker without changing the theme.

The theme picker is included by default in the standard layout’s overlay widgets:

overlay_widget_ids: vec![
    widget_ids::THEME_PICKER,
    widget_ids::SESSION_PICKER,
],

Users access it through the /theme command.