tui: smart dirty tracking — avoid constant full-screen redraws #124

Closed
opened 2026-04-12 22:02:56 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a user, I want the TUI to be snappy and low-CPU even when idle, so that it doesn't waste resources redrawing unchanged content every 250ms.

Problem

App::process_event() sets self.dirty = true on every event (line 403 in app.rs), including every 250ms tick. This causes a full terminal redraw 4 times per second even when the UI hasn't changed. On slow terminals or over SSH, this causes visible flicker and unnecessary bandwidth.

ratatui's Terminal::draw() does diff-based rendering (only changed cells are written), but the diff computation itself has a cost, and protocol-level image renderers (Kitty/Sixel) may re-transmit image placements.

Acceptance criteria

  • Tick events do NOT set dirty by default — only events that actually change state do
  • Event::Key, Event::Mouse with state changes set dirty = true
  • Event::CoreMsg sets dirty = true (backend state changed)
  • Event::Resize sets dirty = true
  • Screens can request a redraw by sending AppAction::Redraw (already exists)
  • Notification expiry (on tick) sets dirty only when a notification was actually dismissed
  • Idle CPU usage drops to near-zero when no events are pending
  • Animated elements (progress bar, spinner) use AppAction::Redraw on their update interval instead of relying on tick

Implementation approach

Replace the blanket self.dirty = true at line 403 with per-event-type logic:

match event {
    Event::Key(_) | Event::Mouse(_) | Event::CoreMsg(_) | Event::Resize(..) | Event::Quit => {
        self.dirty = true;
    }
    Event::Tick => {
        // Only dirty if notifications expired or animations need update.
        if self.notifications.tick(Instant::now()) {
            self.dirty = true;
        }
    }
}

Out of scope

  • Per-widget dirty tracking (ratatui doesn't support partial invalidation)
  • Terminal-level damage tracking

References

  • crates/loom-tui/src/app.rsprocess_event() line 403, self.dirty = true
  • crates/loom-tui/src/event.rsEvent enum, tick interval
  • ratatui diff rendering: Terminal::draw() compares previous and current buffer
## User story As a user, I want the TUI to be snappy and low-CPU even when idle, so that it doesn't waste resources redrawing unchanged content every 250ms. ## Problem `App::process_event()` sets `self.dirty = true` on **every** event (line 403 in app.rs), including every 250ms tick. This causes a full terminal redraw 4 times per second even when the UI hasn't changed. On slow terminals or over SSH, this causes visible flicker and unnecessary bandwidth. ratatui's `Terminal::draw()` does diff-based rendering (only changed cells are written), but the diff computation itself has a cost, and protocol-level image renderers (Kitty/Sixel) may re-transmit image placements. ## Acceptance criteria - [ ] Tick events do NOT set `dirty` by default — only events that actually change state do - [ ] `Event::Key`, `Event::Mouse` with state changes set `dirty = true` - [ ] `Event::CoreMsg` sets `dirty = true` (backend state changed) - [ ] `Event::Resize` sets `dirty = true` - [ ] Screens can request a redraw by sending `AppAction::Redraw` (already exists) - [ ] Notification expiry (on tick) sets dirty only when a notification was actually dismissed - [ ] Idle CPU usage drops to near-zero when no events are pending - [ ] Animated elements (progress bar, spinner) use `AppAction::Redraw` on their update interval instead of relying on tick ## Implementation approach Replace the blanket `self.dirty = true` at line 403 with per-event-type logic: ```rust match event { Event::Key(_) | Event::Mouse(_) | Event::CoreMsg(_) | Event::Resize(..) | Event::Quit => { self.dirty = true; } Event::Tick => { // Only dirty if notifications expired or animations need update. if self.notifications.tick(Instant::now()) { self.dirty = true; } } } ``` ## Out of scope - Per-widget dirty tracking (ratatui doesn't support partial invalidation) - Terminal-level damage tracking ## References - `crates/loom-tui/src/app.rs` — `process_event()` line 403, `self.dirty = true` - `crates/loom-tui/src/event.rs` — `Event` enum, tick interval - ratatui diff rendering: `Terminal::draw()` compares previous and current buffer
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/loom#124
No description provided.