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 parentllmfitdirectory if missing usingfs::create_dir_all(), then writes the theme's string label (e.g.,"Dracula","Nord") to the config file viafs::write().load()(lines 81-87): Attempts to read the configuration file, trims whitespace, and passes the content tofrom_label(). ReturnsTheme::Defaultif the file is absent or unreadable.from_label(s)(lines 89-101): Matches string labels against enum variants using amatchexpression, providing a safe fallback toTheme::Defaultfor 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 byTheme::config_path()intheme.rs. - Startup Loading: The
Appstruct callsTheme::load()during initialization to restore the previous session's selection. - Safe Fallbacks: The
load()andfrom_label()methods default toTheme::Defaultif 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 totui_ui.rs, whilenext()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →