feat(tui): app-level event loop and state machine #49

Merged
charles merged 1 commit from tui/event-loop-16 into main 2026-04-11 20:41:19 +00:00
Owner

Summary

Second PR in the loom-tui stack. Replaces the scaffold's one-off loop with a unified event model and a top-level App state machine that screens and overlays plug into.

Stacks on #48 (scaffold). Closes charles/loom#16.

What's in

  • event::Event unifies keyboard/mouse/paste/focus/resize + Tick + CoreMsg(LoomEvent) + Quit/CoreLag
  • event::EventStream merges crossterm input, a 250 ms Tick, and an optional tokio::sync::broadcast subscription from loom_core::event_bus::EventBus into one awaitable source. Test-only new_headless skips the crossterm reader
  • app::App holds a screen map (lazy-constructed), an overlay stack, a tokio::sync::mpsc::UnboundedChannel<AppAction> for screens to enqueue navigation / overlay / quit / redraw requests, and a dirty flag so unchanged frames aren't re-drawn
  • screens::Screen trait + ScreenKind enum + StubScreen placeholders for all six top-level screens (Generate, Gallery, ModelBrowser, Entities, Presets, Settings). Later tickets replace the stubs
  • app::Overlay trait + OverlayKind::Help + a minimal HelpOverlay so the overlay dispatch chain is live end-to-end (push on ?, pop on Esc)
  • Dispatch order per spec §1/§3: overlays (top-first, can short-circuit) → current screen → global bindings (q/Ctrl+C quit, ? help, g n m e p s navigate)

Out of scope

  • Sidebar / status bar rendering (#17)
  • Configurable key map (#19) — global keys are hard-coded for now
  • tui.toml config loading (#43)
  • Real screen implementations (#20, #27, #32, #36, #38, #39)
  • PluginBridge/EventBus are NOT yet constructed and wired through AppCtx — that happens once navigation + config land. EventStream::new(None) reflects this.

Test plan

  • cargo test -p loom-tui — 8 tests pass (event tick/quit/core, app navigate/quit/overlay)
  • cargo clippy -p loom-tui --all-targets -- -D warnings clean
  • just qa green
  • Manual: cargo run -p loom-tui, verify ? opens help, Esc closes, g navigates to Gallery stub, q exits

Notes

  • AppAction::Navigate / PushOverlay / Quit / Redraw variants carry #[allow(dead_code)] in this PR because only PopOverlay is wired so far. They come online in tickets #17/#18/#42
  • Event variants carry the same allow for the same reason
  • Screen::kind() is unused in the dispatch chain but kept on the trait so #17's sidebar can ask "which screen is this trait object?" without downcasting
## Summary Second PR in the loom-tui stack. Replaces the scaffold's one-off loop with a unified event model and a top-level `App` state machine that screens and overlays plug into. Stacks on #48 (scaffold). Closes charles/loom#16. ## What's in - **`event::Event`** unifies keyboard/mouse/paste/focus/resize + `Tick` + `CoreMsg(LoomEvent)` + `Quit`/`CoreLag` - **`event::EventStream`** merges crossterm input, a 250 ms `Tick`, and an optional `tokio::sync::broadcast` subscription from `loom_core::event_bus::EventBus` into one awaitable source. Test-only `new_headless` skips the crossterm reader - **`app::App`** holds a screen map (lazy-constructed), an overlay stack, a `tokio::sync::mpsc::UnboundedChannel<AppAction>` for screens to enqueue navigation / overlay / quit / redraw requests, and a dirty flag so unchanged frames aren't re-drawn - **`screens::Screen`** trait + `ScreenKind` enum + `StubScreen` placeholders for all six top-level screens (Generate, Gallery, ModelBrowser, Entities, Presets, Settings). Later tickets replace the stubs - **`app::Overlay`** trait + `OverlayKind::Help` + a minimal `HelpOverlay` so the overlay dispatch chain is live end-to-end (push on `?`, pop on `Esc`) - **Dispatch order** per spec §1/§3: overlays (top-first, can short-circuit) → current screen → global bindings (`q`/`Ctrl+C` quit, `?` help, `g n m e p s` navigate) ## Out of scope - Sidebar / status bar rendering (#17) - Configurable key map (#19) — global keys are hard-coded for now - `tui.toml` config loading (#43) - Real screen implementations (#20, #27, #32, #36, #38, #39) - `PluginBridge`/`EventBus` are NOT yet constructed and wired through `AppCtx` — that happens once navigation + config land. `EventStream::new(None)` reflects this. ## Test plan - [x] `cargo test -p loom-tui` — 8 tests pass (event tick/quit/core, app navigate/quit/overlay) - [x] `cargo clippy -p loom-tui --all-targets -- -D warnings` clean - [x] `just qa` green - [ ] Manual: `cargo run -p loom-tui`, verify `?` opens help, `Esc` closes, `g` navigates to Gallery stub, `q` exits ## Notes - `AppAction::Navigate / PushOverlay / Quit / Redraw` variants carry `#[allow(dead_code)]` in this PR because only `PopOverlay` is wired so far. They come online in tickets #17/#18/#42 - `Event` variants carry the same allow for the same reason - `Screen::kind()` is unused in the dispatch chain but kept on the trait so #17's sidebar can ask "which screen is this trait object?" without downcasting
Replace the scaffold's one-off event loop with a unified Event enum,
an EventStream that merges crossterm input, a 250 ms tick, and an
optional loom-core broadcast subscription, plus a top-level App state
machine with screen map, overlay stack, and an AppAction channel
routed through an AppHandle. Screens implement a common trait
(Screen), stub implementations land for all six screen kinds so the
dispatch chain is fully wired end-to-end, and a minimal HelpOverlay
plumbs through ? and Esc to exercise the overlay stack.

Event dispatch order per spec: overlays (top-first) → current screen
→ global bindings. SIGTERM/SIGINT are translated into a synthetic
Event::Quit by the shutdown handler so the main loop only has one
quit path.

Closes charles/loom#16

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
charles changed target branch from tui/scaffold-11 to main 2026-04-11 20:41:12 +00:00
charles deleted branch tui/event-loop-16 2026-04-11 20:41:20 +00:00
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!49
No description provided.