Goal
Ship `quadraui::tui::run<A: AppLogic>(app)` — the actual TUI runner, blocked on the `TuiBackend` lift.
Scope
```rust
// quadraui/src/tui/run.rs
pub fn run<A: AppLogic>(mut app: A) -> std::io::Result<()> {
// 1. Terminal init: enable_raw_mode, EnterAlternateScreen,
// EnableMouseCapture, EnableBracketedPaste,
// PushKeyboardEnhancementFlags (best-effort).
//
// 2. Construct Terminal<CrosstermBackend> and TuiBackend.
//
// 3. Install panic hook (write crash info to a known path; emergency
// swap-flush hook is app-side, not runner-side — runner doesn't
// know about engine swap files).
//
// 4. app.setup(&mut backend).
//
// 5. Loop:
// - terminal.draw(|f| backend.enter_frame_scope(f, |b| app.render(b)))
// - for event in backend.wait_events(timeout): match app.handle(event, &mut backend) {
// Reaction::Continue => {},
// Reaction::Redraw => needs_redraw = true,
// Reaction::Exit => return,
// }
//
// 6. Tear-down: pop kbd flags, disable raw mode, leave alternate screen,
// show cursor.
}
```
Open design questions surfaced during the audit (#260):
- How does the app communicate "needs redraw" between event handlers? `Reaction::Redraw` covers per-event; a handler that mutates state mid-frame needs a different path.
- Should the runner take a `RunnerOptions` struct (enable mouse?, kitty kbd protocol?, panic hook path?) or sensible defaults only?
- Single-DrawingArea / single-Frame scope — multi-area apps (vimcode) need a richer shape, separately filed.
Migration target
After this lands, kubeui's `main` collapses from ~200 lines to ~20:
```rust
struct KubeApp { state: AppState, rt: tokio::runtime::Runtime }
impl quadraui::AppLogic for KubeApp {
fn render(&self, backend: &mut dyn Backend) { /* paint primitives via backend / }
fn handle(&mut self, ev: UiEvent, _b: &mut dyn Backend) -> Reaction { / ... */ }
}
fn main() -> Result<()> {
kubeui_core::install_crypto_provider()?;
let rt = tokio::runtime::Runtime::new()?;
let state = bootstrap_state(&rt);
quadraui::tui::run(KubeApp { state, rt }).map_err(Into::into)
}
```
Depends on
- Lift TuiBackend into quadraui::tui (separate stage, filed alongside this).
Surfaced during
#261 stage A — trait design landed; runner blocked on the lift.
Goal
Ship `quadraui::tui::run<A: AppLogic>(app)` — the actual TUI runner, blocked on the `TuiBackend` lift.
Scope
```rust
// quadraui/src/tui/run.rs
pub fn run<A: AppLogic>(mut app: A) -> std::io::Result<()> {
// 1. Terminal init: enable_raw_mode, EnterAlternateScreen,
// EnableMouseCapture, EnableBracketedPaste,
// PushKeyboardEnhancementFlags (best-effort).
//
// 2. Construct Terminal<CrosstermBackend> and TuiBackend.
//
// 3. Install panic hook (write crash info to a known path; emergency
// swap-flush hook is app-side, not runner-side — runner doesn't
// know about engine swap files).
//
// 4. app.setup(&mut backend).
//
// 5. Loop:
// - terminal.draw(|f| backend.enter_frame_scope(f, |b| app.render(b)))
// - for event in backend.wait_events(timeout): match app.handle(event, &mut backend) {
// Reaction::Continue => {},
// Reaction::Redraw => needs_redraw = true,
// Reaction::Exit => return,
// }
//
// 6. Tear-down: pop kbd flags, disable raw mode, leave alternate screen,
// show cursor.
}
```
Open design questions surfaced during the audit (#260):
Migration target
After this lands, kubeui's `main` collapses from ~200 lines to ~20:
```rust
struct KubeApp { state: AppState, rt: tokio::runtime::Runtime }
impl quadraui::AppLogic for KubeApp {
fn render(&self, backend: &mut dyn Backend) { /* paint primitives via backend / }
fn handle(&mut self, ev: UiEvent, _b: &mut dyn Backend) -> Reaction { / ... */ }
}
fn main() -> Result<()> {
kubeui_core::install_crypto_provider()?;
let rt = tokio::runtime::Runtime::new()?;
let state = bootstrap_state(&rt);
quadraui::tui::run(KubeApp { state, rt }).map_err(Into::into)
}
```
Depends on
Surfaced during
#261 stage A — trait design landed; runner blocked on the lift.