# How llmfit Enforces Stateless TUI Rendering While Handling Events in Rust

> Discover how llmfit enforces stateless TUI rendering in Rust by separating UI drawing from event handling. Learn to manage state effectively for cleaner applications.

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

---

**The llmfit terminal interface separates concerns by restricting all mutations to [`tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/tui_events.rs) while [`tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/tui_ui.rs) draws each frame using temporary local state, ensuring the rendering layer remains completely stateless.**

The `AlexsJones/llmfit` repository implements a terminal user interface (TUI) that strictly enforces stateless rendering patterns essential for predictable UI behavior. By partitioning the codebase into distinct event-handling and rendering layers, the project ensures that drawing operations never mutate application state. This article examines the architectural decisions in [`llmfit-tui/src/tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_ui.rs) and [`llmfit-tui/src/tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_events.rs) that enable stateless TUI rendering while maintaining responsive event-driven interactions.

## The Architecture: Separating Event Handling from Rendering

The llmfit TUI implements a strict two-layer architecture that isolates side effects:

| Layer | File | Responsibility |
|-------|------|--------------|
| **Event Layer** | [`llmfit-tui/src/tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_events.rs) | Polls input, updates persistent `App` state, manages navigation |
| **Render Layer** | [`llmfit-tui/src/tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_ui.rs) | Draws UI frames without persisting changes |

All mutable state resides in the `App` struct, which serves as the single source of truth for selection indices, scroll offsets, and view flags. Mutations occur only through event handlers like `handle_normal_mode` and `update_model_viewport`, while the rendering code consumes this state without modification.

## How tui_ui.rs Maintains Stateless Rendering

The `draw` function in [`llmfit-tui/src/tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_ui.rs) accepts `&mut App` but explicitly avoids writing persistent state changes. Instead, it constructs temporary rendering artifacts that exist only for the duration of the current frame.

### Creating a Local TableState for Each Frame

Rather than mutating `app.table_state` directly, the rendering code instantiates a disposable `TableState` for visual selection:

```rust
// llmfit-tui/src/tui_ui.rs
pub fn draw(frame: &mut Frame, app: &mut App) {
    // ... layout calculations ...
    let mut visible_state = TableState::default();
    visible_state.select(
        viewport
            .contains(&app.selected_row)
            .then(|| app.selected_row - viewport.start),
    );
    frame.render_stateful_widget(table, area, &mut visible_state);
}

```

The `visible_state` variable is never stored back into `app.table_state`. The source code explicitly documents this intent: "Widget selection is local to this frame. Persistent navigation state is updated by `tui_events`, never by drawing the table."

This design guarantees that calling `draw` multiple times produces identical results without accumulating side effects on scroll positions or selection indices.

## How tui_events.rs Handles State Mutations

All state modifications occur exclusively in [`llmfit-tui/src/tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/llmfit-tui/src/tui_events.rs). Event handlers mutate the `App` struct before each draw call, ensuring the UI reflects the latest user input while maintaining strict boundaries.

### Updating the Persistent Viewport

The `update_model_viewport` function recalculates scroll positions and persists them to the `App` struct:

```rust
// llmfit-tui/src/tui_events.rs
pub fn update_model_viewport(app: &mut App, terminal_area: Rect) {
    // ... compute new viewport ...
    app.table_state.select((!app.filtered_fits.is_empty()).then_some(app.selected_row));
    *app.table_state.offset_mut() = viewport.start;
}

```

This function runs on every event loop iteration and during startup, guaranteeing that persistent navigation state updates occur only through the event layer. Mode-specific handlers like `handle_search_mode` and `handle_normal_mode` similarly restrict mutations to this file, creating a clear authority for state changes.

## Verifying Statelessness with Tests

The project includes explicit tests confirming that rendering produces no side effects. The test `model_viewport_updates_on_events_and_draws_leave_it_unchanged` draws multiple frames and asserts state immutability:

```rust
// llmfit-tui/src/tui_events.rs
terminal.draw(|f| crate::tui_ui::draw(f, &mut app)).expect("first frame");
terminal.draw(|f| crate::tui_ui::draw(f, &mut app)).expect("second frame");
assert_eq!(app.table_state, state); // unchanged after draw

```

This verification ensures that repeated calls to `draw` cannot accidentally modify `selected_row`, scroll offsets, or other persistent fields. The test acts as a regression guard against developers inadvertently introducing state mutations into the rendering pipeline.

## Summary

- **State isolation**: [`tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/tui_events.rs) contains all mutation logic while [`tui_ui.rs`](https://github.com/AlexsJones/llmfit/blob/main/tui_ui.rs) remains read-only for persistent state.
- **Temporary rendering state**: The `draw` function creates local `visible_state` instances rather than modifying `app.table_state`.
- **Explicit boundaries**: Event handlers like `handle_normal_mode` and `update_model_viewport` manage the `App` lifecycle, while rendering functions only consume state.
- **Test guarantees**: Unit tests verify that rendering operations leave `table_state` and navigation indices unchanged.

## Frequently Asked Questions

### Why does the draw function take &mut App if it doesn't mutate state?

The `&mut App` parameter accommodates internal conveniences like temporary borrowing, but the implementation deliberately avoids persisting changes. The function could theoretically use `&App`, but the mutable reference allows internal widget construction APIs that require mutable access while the discipline of never writing back to persistent fields maintains statelessness.

### What prevents developers from accidentally mutating state during rendering?

The architectural separation enforced by module boundaries and explicit code comments guides contributors toward the event layer for mutations. Additionally, the test suite detects unintended side effects by asserting state equality before and after draw operations, providing automated regression protection.

### How does this pattern improve TUI performance or reliability?

Stateless rendering eliminates an entire class of bugs where visual updates corrupt application logic. By ensuring that drawing frames cannot alter navigation state or scroll positions, the system guarantees that user inputs processed by [`tui_events.rs`](https://github.com/AlexsJones/llmfit/blob/main/tui_events.rs) remain the sole authority on state transitions, creating predictable, reproducible UI behavior even during rapid event processing.

### Can this pattern be adapted to other Rust TUI frameworks?

Yes, this architectural pattern applies broadly to `ratatui` and similar immediate-mode terminal interfaces. Any TUI can enforce stateless rendering by restricting persistent state mutations to event handlers and constructing temporary widget states during the draw phase, though the specific implementation details will vary based on the framework's ownership model.