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

> Learn how llmfit-tui persists and applies themes across TUI sessions by storing theme variants in config files and reloading them on startup.

- Repository: [Alex Jones/llmfit](https://github.com/AlexsJones/llmfit)
- Tags: internals
- Published: 2026-09-11

---

**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`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/theme.rs) and its integration with the `App` state manager in [`llmfit-tui/src/tui_app.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_app.rs).

## Theme Persistence Architecture

The persistence layer resides entirely within [`llmfit-tui/src/theme.rs`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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:

```rust
// 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`](https://github.com/AlexsJones/llmfit/blob/main/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:

```rust
// 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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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:

```rust
// 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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_app.rs) (which holds the `App.theme` state) and [`llmfit-tui/src/tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/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`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_events.rs).