How Themes Are Persisted and Applied Across TUI Sessions in llmfit-tui

llmfit-tui persists color themes by writing the selected variant label to a file in the OS-specific configuration directory (e.g., $XDG_CONFIG_HOME/llmfit/theme on Linux), then reloads this value on startup via Theme::load() to restore the user's visual preferences across sessions.

The AlexsJones/llmfit repository implements a stateful theming system for its terminal user interface that survives application restarts without external dependencies. Understanding how themes are persisted and applied across TUI sessions in llmfit-tui requires examining the Theme enum's serialization logic in llmfit-tui/src/theme.rs and its integration with the App state manager in llmfit-tui/src/tui_app.rs.

Theme Persistence Architecture

The persistence layer resides entirely within llmfit-tui/src/theme.rs, where the Theme enum implements filesystem-backed storage using simple text serialization.

Configuration Path and File Storage

The config_path() helper constructs the storage location using the dirs crate to locate the appropriate configuration directory for the operating system. On Linux, this resolves to $XDG_CONFIG_HOME/llmfit/theme; on Windows, %APPDATA%\llmfit\theme (lines 66-69 in theme.rs). This path ensures the theme file respects platform conventions and user environment variables.

The Save and Load Lifecycle

Three core methods handle the persistence lifecycle:

  • save(&self) (lines 71-78): Creates the parent llmfit directory if missing using fs::create_dir_all(), then writes the theme's string label (e.g., "Dracula", "Nord") to the config file via fs::write().
  • load() (lines 81-87): Attempts to read the configuration file, trims whitespace, and passes the content to from_label(). Returns Theme::Default if the file is absent or unreadable.
  • from_label(s) (lines 89-101): Matches string labels against enum variants using a match expression, providing a safe fallback to Theme::Default for unknown or corrupted values.

This design ensures that theme selection is atomic and crash-resistant; the file either contains a valid label or the application gracefully defaults to the base theme.

Initializing Themes on Application Startup

When the TUI launches, the App struct defined in llmfit-tui/src/tui_app.rs instantiates the theme state before entering the main event loop. During App::new() or equivalent initialization, the code invokes Theme::load() to read the persisted preference from disk:

// In llmfit-tui/src/tui_app.rs
use crate::theme::Theme;

impl App {
    fn new() -> Self {
        let theme = Theme::load(); // Reads from <config_dir>/llmfit/theme
        
        Self {
            // ... other fields ...
            theme,
            // ...
        }
    }
}

The loaded Theme value is stored in the app.theme field, making it accessible to the entire application lifecycle. This single source of truth ensures consistent styling from the first rendered frame.

Runtime Theme Application

Once initialized, the theme influences both user interaction and visual rendering through a cyclic selection mechanism and a structured color palette.

Cycling Through Available Themes

User input triggers theme changes via keybindings handled in llmfit-tui/src/tui_events.rs. When the user presses the theme toggle key (typically t), the application advances to the next variant and immediately persists the choice:

// In llmfit-tui/src/tui_events.rs (key handling logic)
if key == Key::Char('t') {
    app.theme = app.theme.next(); // Cyclic iteration through variants (lines 36-48)
    app.theme.save();             // Writes new label to disk immediately
}

The next() method (lines 36-48 in theme.rs) implements a cyclic iterator over the enum variants, returning the first theme after the last to create an infinite rotation.

Rendering UI Components with ThemeColors

The rendering pipeline in llmfit-tui/src/tui_ui.rs accesses the current theme's color palette through the colors() method, which returns a ThemeColors struct containing concrete ratatui::style::Color values:

// In llmfit-tui/src/tui_ui.rs
use ratatui::{
    style::{Color, Style},
    widgets::{Block, Borders, Table},
    Frame,
};

fn draw_dashboard(f: &mut Frame, app: &App) {
    let colors = app.theme.colors(); // Obtain ThemeColors struct
    
    let header_style = Style::default()
        .fg(colors.title)
        .bg(colors.bg);
        
    let status_style = Style::default()
        .fg(colors.accent);
        
    // Apply styles to widgets using colors.fg, colors.bg, 
    // colors.fit_colors, colors.status_colors, etc.
    let block = Block::default()
        .borders(Borders::ALL)
        .border_style(Style::default().fg(colors.accent));
        
    // ... render table cells and status indicators ...
}

The ThemeColors struct aggregates all necessary color definitions—background, foreground, accents, fit-level indicators, and run-mode states—allowing the UI layer to remain agnostic of specific theme variants while maintaining visual consistency.

Summary

  • Storage Location: Themes persist as plain-text labels in <config_dir>/llmfit/theme, determined by Theme::config_path() in theme.rs.
  • Startup Loading: The App struct calls Theme::load() during initialization to restore the previous session's selection.
  • Safe Fallbacks: The load() and from_label() methods default to Theme::Default if the configuration file is missing or contains invalid data.
  • Immediate Persistence: Theme changes trigger save() instantly, ensuring no state loss on application crash or forced termination.
  • Runtime Application: The colors() method provides a structured palette to tui_ui.rs, while next() enables cyclic theme rotation via keybindings.

Frequently Asked Questions

Where does llmfit-tui store the active theme configuration?

The application stores the theme label in a file named theme inside the OS-specific configuration directory. On Linux systems, this is $XDG_CONFIG_HOME/llmfit/theme; on macOS, ~/Library/Application Support/llmfit/theme; and on Windows, %APPDATA%\llmfit\theme. The Theme::config_path() method in llmfit-tui/src/theme.rs constructs this path dynamically using the dirs crate.

What happens if the theme configuration file is corrupted or deleted?

If Theme::load() encounters a missing file, read error, or unknown label, it automatically falls back to Theme::Default. The from_label() match expression handles unknown strings by returning the default variant, ensuring the application always starts with a valid color scheme even when persistence fails.

How does the application cycle between different color themes?

The Theme::next() method (lines 36-48 in theme.rs) implements a cyclic iterator over all available theme variants. When the user triggers a theme change, the application assigns app.theme = app.theme.next() to advance to the next variant, wrapping back to the first theme after reaching the last. This new value is immediately written to disk via save().

Which source files handle theme persistence versus theme rendering?

Theme persistence logic resides in llmfit-tui/src/theme.rs, containing the load(), save(), and config_path() implementations. Theme rendering and application logic is split between llmfit-tui/src/tui_app.rs (which holds the App.theme state) and llmfit-tui/src/tui_ui.rs (which calls app.theme.colors() to style widgets). Input handling for theme switching typically occurs in llmfit-tui/src/tui_events.rs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →