feat: complete TUI Phase 0 — Toolchain Gate

Implements the full lore-tui crate scaffold with 6 Phase 0 modules:

- message.rs: Msg (~40 variants), Screen (12), EntityKey, AppError, InputMode
- clock.rs: Clock trait with SystemClock + FakeClock for deterministic testing
- safety.rs: Terminal sanitizer (ANSI filter), URL policy, PII/secret redaction
- db.rs: DbManager with 3 reader pool (round-robin) + dedicated writer (WAL mode)
- theme.rs: Flexoki adaptive theme (19 slots), state/event colors, label styling
- app.rs: Minimal LoreApp Model trait impl proving FrankenTUI integration

68 tests, clippy clean, fmt clean. Closes bd-3ddw, bd-c9gk, bd-2lg6,
bd-3ir1, bd-2kop, bd-5ofk, bd-2emv, bd-1cj0.
This commit is contained in:
teernisse
2026-02-12 15:08:09 -05:00
parent ad4dd6e855
commit d224a88738
12 changed files with 5214 additions and 16 deletions

101
crates/lore-tui/src/app.rs Normal file
View File

@@ -0,0 +1,101 @@
#![allow(dead_code)] // Phase 0: minimal scaffold, fleshed out in bd-6pmy
//! Minimal FrankenTUI Model implementation for the lore TUI.
//!
//! This is the Phase 0 integration proof — validates that the ftui Model trait
//! compiles with our Msg type and produces basic output. The full LoreApp with
//! screen routing, navigation stack, and action dispatch comes in bd-6pmy.
use ftui::{Cmd, Frame, Model};
use crate::message::Msg;
// ---------------------------------------------------------------------------
// LoreApp
// ---------------------------------------------------------------------------
/// Root model for the lore TUI.
///
/// Phase 0: minimal scaffold that renders a placeholder and handles Quit.
/// Phase 1 (bd-6pmy) will add screen routing, DbManager, theme, and subscriptions.
pub struct LoreApp;
impl Model for LoreApp {
type Message = Msg;
fn init(&mut self) -> Cmd<Self::Message> {
Cmd::none()
}
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
match msg {
Msg::Quit => Cmd::quit(),
_ => Cmd::none(),
}
}
fn view(&self, _frame: &mut Frame) {
// Phase 0: no-op view. Phase 1 will render screens via the frame.
}
}
/// Verify that `App::fullscreen(LoreApp).run()` compiles.
///
/// This is a compile-time check — we don't actually run it because that
/// would require a real TTY. The function exists solely to prove the wiring.
#[cfg(test)]
fn _assert_app_fullscreen_compiles() {
// This function is never called — it only needs to compile.
fn _inner() {
use ftui::App;
let _app_builder = App::fullscreen(LoreApp);
// _app_builder.run() would need a TTY, so we don't call it.
}
}
/// Verify that `App::inline(LoreApp, 12).run()` compiles.
#[cfg(test)]
fn _assert_app_inline_compiles() {
fn _inner() {
use ftui::App;
let _app_builder = App::inline(LoreApp, 12);
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lore_app_init_returns_none() {
let mut app = LoreApp;
let cmd = app.init();
assert!(matches!(cmd, Cmd::None));
}
#[test]
fn test_lore_app_quit_returns_quit_cmd() {
let mut app = LoreApp;
let cmd = app.update(Msg::Quit);
assert!(matches!(cmd, Cmd::Quit));
}
#[test]
fn test_lore_app_tick_returns_none() {
let mut app = LoreApp;
let cmd = app.update(Msg::Tick);
assert!(matches!(cmd, Cmd::None));
}
#[test]
fn test_lore_app_navigate_returns_none() {
use crate::message::Screen;
let mut app = LoreApp;
let cmd = app.update(Msg::NavigateTo(Screen::Dashboard));
assert!(matches!(cmd, Cmd::None));
}
}