feat(tui): notification bar, confirmation dialogs & text input basics #54

Merged
charles merged 1 commit from tui/notifications-42 into main 2026-04-11 20:41:55 +00:00
Owner

Summary

Seventh PR in the loom-tui stack. Wraps up the spec §8 UX polish layer so subsequent screen tickets can rely on real notifications, confirmations, and text-input primitives.

Stacks on #53. Closes charles/loom#42. Completes Phase 1 of the loom-tui roadmap (#47).

What's in

  • components::notificationLevel (info/success/warn/error), Notification { level, message, created_at }, NotificationQueue with push/tick/dismiss_current/current/render. Lifetime is Duration::from_secs(5), driven off Instant deltas so tests can forge past-timestamps
  • components::confirmConfirmRequest (title, body, confirm/cancel labels, default_confirm), ConfirmOutcome::{Confirmed, Cancelled}, render(frame, area, &request). Helper ConfirmRequest::destructive forces default_confirm = false
  • components::text_input::TextInput — single-line editor with Ctrl+A (cursor start), Ctrl+E (cursor end), Ctrl+U (clear), Ctrl+W (delete word left), Backspace/Delete/Left/Right/Home/End/printable chars. Unicode-safe char/byte indexing
  • AppAction::Notify(Notification) / ConfirmOutcome(ConfirmOutcome) — screens push notifications via ctx.notify(n); confirm overlay writes its outcome via AppAction::ConfirmOutcome which App stores in last_confirm
  • OverlayKind::Confirm(ConfirmRequest)App::push_overlay constructs a ConfirmOverlay around the request
  • ConfirmOverlay in app.rsy/Y → Confirmed, n/N/Esc → Cancelled, Enter → default answer, every other key swallowed (modal)
  • App::render reserves a 1-row notification slot above the status bar when the queue has a current entry; compute_layout grew a fourth show_notification parameter and returns AppLayout { sidebar, main, notification?, status }
  • Auto-dismiss: process_event calls notifications.tick(Instant::now()) on Event::Tick and notifications.dismiss_current() on any key press, matching spec "auto-dismiss after 5 s or on next keypress"

Tests (12 new, 44 total)

  • Push/dismiss through the queue; tick expires old entries
  • Sidebar/layout grew a new test for the notification-row variant
  • AppAction::Notify routed through drain_actions lands in the queue
  • A key press dismisses the current notification
  • Confirm overlay reports Confirmed on y and Cancelled on Esc/default
  • Destructive confirm with Enter returns Cancelled
  • TextInput: insert/backspace roundtrip, Ctrl+U clear, Ctrl+W word delete, Ctrl+A cursor start, Left/Right bounds

Notes

  • components::notification, components::confirm, components::text_input are #![allow(dead_code)] at module level — they're library surfaces consumed by later screen tickets (Notification::success, success/error levels, ConfirmRequest::destructive, TextInput::set_text). Module-level allow keeps reviews focused instead of peppering the file with per-item attributes
  • The last_confirm poll-based API is intentionally minimal. A subscription model (one-shot channel per prompt) can grow later once multiple concurrent confirmations become a thing
  • Layout always computes the notification row when any notification is visible. When the queue is empty the row is omitted entirely so screens get the full body area

Phase 1 complete

This PR closes out Phase 1 of the loom-tui roadmap (#47). The foundations — crate scaffold, event loop, configurable keybinds, config file, navigation chrome, command palette, notifications/confirms — are all in place. Phase 2 is image rendering (#12 #13 #14 #15), which I'll start on next stacked atop this branch.

## Summary Seventh PR in the loom-tui stack. Wraps up the spec §8 UX polish layer so subsequent screen tickets can rely on real notifications, confirmations, and text-input primitives. Stacks on #53. Closes charles/loom#42. Completes **Phase 1** of the loom-tui roadmap (#47). ## What's in - **`components::notification`** — `Level` (info/success/warn/error), `Notification { level, message, created_at }`, `NotificationQueue` with `push/tick/dismiss_current/current/render`. Lifetime is `Duration::from_secs(5)`, driven off `Instant` deltas so tests can forge past-timestamps - **`components::confirm`** — `ConfirmRequest` (title, body, confirm/cancel labels, `default_confirm`), `ConfirmOutcome::{Confirmed, Cancelled}`, `render(frame, area, &request)`. Helper `ConfirmRequest::destructive` forces `default_confirm = false` - **`components::text_input::TextInput`** — single-line editor with `Ctrl+A` (cursor start), `Ctrl+E` (cursor end), `Ctrl+U` (clear), `Ctrl+W` (delete word left), `Backspace`/`Delete`/`Left`/`Right`/`Home`/`End`/printable chars. Unicode-safe char/byte indexing - **`AppAction::Notify(Notification)` / `ConfirmOutcome(ConfirmOutcome)`** — screens push notifications via `ctx.notify(n)`; confirm overlay writes its outcome via `AppAction::ConfirmOutcome` which `App` stores in `last_confirm` - **`OverlayKind::Confirm(ConfirmRequest)`** — `App::push_overlay` constructs a `ConfirmOverlay` around the request - **`ConfirmOverlay`** in `app.rs` — `y`/`Y` → Confirmed, `n`/`N`/`Esc` → Cancelled, `Enter` → default answer, every other key swallowed (modal) - **`App::render`** reserves a 1-row notification slot above the status bar when the queue has a current entry; `compute_layout` grew a fourth `show_notification` parameter and returns `AppLayout { sidebar, main, notification?, status }` - **Auto-dismiss**: `process_event` calls `notifications.tick(Instant::now())` on `Event::Tick` and `notifications.dismiss_current()` on any key press, matching spec "auto-dismiss after 5 s or on next keypress" ## Tests (12 new, 44 total) - Push/dismiss through the queue; tick expires old entries - Sidebar/layout grew a new test for the notification-row variant - `AppAction::Notify` routed through drain_actions lands in the queue - A key press dismisses the current notification - Confirm overlay reports `Confirmed` on `y` and `Cancelled` on `Esc`/default - Destructive confirm with `Enter` returns `Cancelled` - TextInput: insert/backspace roundtrip, `Ctrl+U` clear, `Ctrl+W` word delete, `Ctrl+A` cursor start, Left/Right bounds ## Notes - `components::notification`, `components::confirm`, `components::text_input` are `#![allow(dead_code)]` at module level — they're library surfaces consumed by later screen tickets (`Notification::success`, `success`/`error` levels, `ConfirmRequest::destructive`, `TextInput::set_text`). Module-level allow keeps reviews focused instead of peppering the file with per-item attributes - The `last_confirm` poll-based API is intentionally minimal. A subscription model (one-shot channel per prompt) can grow later once multiple concurrent confirmations become a thing - Layout always computes the notification row when any notification is visible. When the queue is empty the row is omitted entirely so screens get the full body area ## Phase 1 complete This PR closes out Phase 1 of the loom-tui roadmap (#47). The foundations — crate scaffold, event loop, configurable keybinds, config file, navigation chrome, command palette, notifications/confirms — are all in place. Phase 2 is image rendering (#12 #13 #14 #15), which I'll start on next stacked atop this branch.
Adds the UX polish layer called for by spec §8:

- components::notification — NotificationQueue with info/success/warn/
  error levels, per-entry Instant lifetime (5 s), rendered above the
  status bar in a reserved row. Expiry driven by Tick events so wall-
  clock elapsed time works without user input
- components::confirm — ConfirmRequest / ConfirmOutcome + a centered
  dialog renderer. Destructive helper defaults the answer to Cancelled
- components::text_input — single-line TextInput with Ctrl+A/E/U/W
  editing, cursor movement, Unicode-safe delete
- AppAction::Notify / ConfirmOutcome and OverlayKind::Confirm variants
  so any screen can push a notification or prompt for confirmation via
  AppCtx.notify / AppAction::PushOverlay
- AppCtx.notify helper method for the common case
- Notifications auto-dismiss on any key press as well as via Tick-based
  expiry so the UI never stays obscured once the user engages

Closes charles/loom#42

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
charles changed target branch from tui/palette-18 to main 2026-04-11 20:41:48 +00:00
charles deleted branch tui/notifications-42 2026-04-11 20:41:55 +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!54
No description provided.