feat(tui): polish batch — async storage, smart dirty, event bus wiring #130

Merged
charles merged 5 commits from tui/polish-batch into main 2026-04-14 20:51:07 +00:00
Collaborator

Resolves five infra-level TUI issues in one batch.

Commits

Commit Issue Title
bb13b8a #124 smart dirty tracking — idle CPU drops to ~zero
62a9c3a #125 async storage queries for gallery, entities, presets
4b457b2 #122 gallery auto-refresh on storage mutations
a486c82 #123 settings change propagation via EventBus
683a654 #126 plugin lifecycle events on the bus

Highlights

#124 Smart dirty tracking (crates/loom-tui/src/app.rs)

  • process_event no longer marks dirty unconditionally
  • Tick events only mark dirty when a notification expires
  • Active jobs already mark dirty via CoreMsg progress events
  • load_from_storage now spawns tokio::task::spawn_blocking
  • Three new AppAction variants route results back to apply_loaded
  • Mutex held only for the SQLite query duration
  • New LoomEvent::GalleryChanged
  • Published from gallery favorite/delete + generate save-to-gallery
  • Gallery screen also listens for ImageGenerated and re-loads

#123 Settings propagation

  • New LoomEvent::SettingsChanged
  • App handler re-applies generation/browsing/sharing/prompt config to bridge

#126 Plugin lifecycle events

  • New LoomEvent::PluginLoaded / PluginUnloaded
  • PluginBridge::with_event_bus(bus) opt-in attachment
  • Model browser invalidates cached results on plugin set change

Test plan

  • Gallery → generate → save → switch back to gallery: new image appears without r
  • Settings → change generation backend → next generation uses the new backend without restart
  • Idle CPU near 0% (was redrawing 4×/s)
  • Large gallery (1000+ items) → no visible freeze on initial load; "(loading…)" placeholder visible briefly
  • All cargo test pass

Closes charles/loom#122
Closes charles/loom#123
Closes charles/loom#124
Closes charles/loom#125
Closes charles/loom#126

🤖 Generated with Claude Code

Resolves five infra-level TUI issues in one batch. ## Commits | Commit | Issue | Title | |---|---|---| | `bb13b8a` | #124 | smart dirty tracking — idle CPU drops to ~zero | | `62a9c3a` | #125 | async storage queries for gallery, entities, presets | | `4b457b2` | #122 | gallery auto-refresh on storage mutations | | `a486c82` | #123 | settings change propagation via EventBus | | `683a654` | #126 | plugin lifecycle events on the bus | ## Highlights ### #124 Smart dirty tracking (`crates/loom-tui/src/app.rs`) - `process_event` no longer marks dirty unconditionally - Tick events only mark dirty when a notification expires - Active jobs already mark dirty via `CoreMsg` progress events ### #125 Async storage (`gallery.rs`, `entities.rs`, `presets.rs`) - `load_from_storage` now spawns `tokio::task::spawn_blocking` - Three new `AppAction` variants route results back to `apply_loaded` - Mutex held only for the SQLite query duration ### #122 Gallery auto-refresh (event bus) - New `LoomEvent::GalleryChanged` - Published from gallery favorite/delete + generate save-to-gallery - Gallery screen also listens for `ImageGenerated` and re-loads ### #123 Settings propagation - New `LoomEvent::SettingsChanged` - App handler re-applies generation/browsing/sharing/prompt config to bridge ### #126 Plugin lifecycle events - New `LoomEvent::PluginLoaded` / `PluginUnloaded` - `PluginBridge::with_event_bus(bus)` opt-in attachment - Model browser invalidates cached results on plugin set change ## Test plan - [ ] Gallery → generate → save → switch back to gallery: new image appears without `r` - [ ] Settings → change generation backend → next generation uses the new backend without restart - [ ] Idle CPU near 0% (was redrawing 4×/s) - [ ] Large gallery (1000+ items) → no visible freeze on initial load; "(loading…)" placeholder visible briefly - [ ] All `cargo test` pass Closes charles/loom#122 Closes charles/loom#123 Closes charles/loom#124 Closes charles/loom#125 Closes charles/loom#126 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replaces the blanket ``self.dirty = true`` at the top of process_event
with per-variant matching:

- Key/Mouse/Paste/Resize/CoreMsg/CoreLag/Focus*/Quit always mark dirty
- Tick only marks dirty when a notification actually expires

Active jobs already mark dirty via ``CoreMsg`` progress events, so the
generate screen still updates its progress bar / spinner in real time.

``Notifications::tick`` now returns whether anything was popped so the
caller can decide whether to repaint.

Closes charles/loom#124

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The screens' ``load_from_storage`` previously held a sync mutex on
GalleryStorage and blocked the tokio event loop, causing perceptible
freezes on large galleries.

Each screen now spawns a ``tokio::task::spawn_blocking`` task and
reports back through the action channel via three new variants:
``GalleryItemsLoaded``, ``EntitiesLoaded``, ``PresetsLoaded``. The
app routes the result to the corresponding screen's ``apply_loaded``.

The mutex is held only for the duration of the SQLite query, never
across an await point.

Closes charles/loom#125

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds three new ``LoomEvent`` variants — ``GalleryChanged``,
``SettingsChanged``, ``PluginLoaded``, ``PluginUnloaded`` — wiring
up the first one for gallery cross-screen sync.

Gallery screen:
- Listens for ``LoomEvent::GalleryChanged`` and
  ``LoomEvent::ImageGenerated`` via ``Event::CoreMsg``; either
  triggers ``needs_load = true`` for an automatic refresh.

Publishers:
- ``GalleryScreen::toggle_favorite`` and ``delete_selected``
  publish ``GalleryChanged`` after a successful storage write.
- ``GenerateScreen::save_to_gallery`` publishes ``GalleryChanged``
  on successful insert so the gallery view picks up new images
  without manual reload.

D-Bus signal emitter routes the new internal-only events to a
``continue`` arm — they don't need to leak outside the process.

Closes charles/loom#122

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Settings screen publishes ``LoomEvent::SettingsChanged`` after each
successful save (both from ``apply_field_edit`` and ``toggle_bool``).

The app's ``process_event`` handler observes the event and re-applies
the current settings snapshot to the plugin bridge — generation
plugin/backend, configure_backend, browsing/sharing plugins, prompt
plugin — so changes take effect without restart.

Closes charles/loom#123

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat(tui): plugin lifecycle events on the bus
All checks were successful
qa / qa (pull_request) Successful in 28m23s
683a6541c8
PluginBridge gains an optional ``Arc<EventBus>`` (set via
``with_event_bus``) and publishes ``LoomEvent::PluginLoaded`` /
``PluginUnloaded`` on successful ``load_plugin`` / ``uninstall_plugin``.

Wiring:
- main.rs creates the event bus before the bridge so plugin lifecycle
  events from the bootstrap phase are also broadcast.
- Model browser screen listens for both events; clears cached results
  and posts a status hint so the next search re-pulls fresh data.

Closes charles/loom#126

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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!130
No description provided.