--- phase: 01-safety-foundation plan: 03 subsystem: app tags: [rust, ratatui, crossterm, tui, event-loop, signal-polling, double-ctrl-c, login-shell, goodbye-message] # Dependency graph requires: - phase: 01-01 provides: "Config struct, load_config(), detect_login_shell(), parse_cli(), terminal.rs scaffold" - phase: 01-02 provides: "init_terminal(), restore_terminal(), install_panic_hook(), SignalFlags, register_signals()" provides: - "src/app.rs: App struct, run_event_loop(), handle_key(), draw(), show_goodbye()" - "src/app.rs: ShutdownReason enum (UserQuit, Signal)" - "src/app.rs: DOUBLE_PRESS_WINDOW const = 2 seconds" - "src/main.rs: complete startup -> event loop -> shutdown pipeline" - "Complete runnable Phase 1 application: cargo run produces working BBS TUI" affects: - "02-content-rendering (event loop receives content model in Phase 2)" - "all future phases (event loop is the central integration point)" # Tech tracking tech-stack: added: [] patterns: - "Signal polling before draw: check SignalFlags.should_terminate() first each loop iteration" - "Double-press window: ctrl_c_pressed_at Option compared with elapsed() < DOUBLE_PRESS_WINDOW" - "Login shell suppression: is_login_shell flag disables 'q' key shortcut in handle_key()" - "ratatui::crossterm re-export path: all event polling uses ratatui::crossterm::event" - "BrokenPipe silent exit: event loop propagates io::Error; main catches ErrorKind::BrokenPipe" - "Goodbye after restore: restore_terminal() called first, then show_goodbye() prints to stdout" key-files: created: - src/app.rs modified: - src/main.rs key-decisions: - "show_goodbye() called AFTER restore_terminal() — terminal must be in cooked mode before println! is safe" - "app_config stored in App struct even unused in Phase 1 — avoids dead_code suppression anti-patterns and prepares Phase 2 access" - "quit prompt cleared on any non-Ctrl+C key — pressing Enter/arrow/letter dismisses the double-press window" patterns-established: - "Signal-first event loop: signals.should_terminate() is always the first check before draw" - "Layered shutdown: restore_terminal() unconditional, then match shutdown_reason for post-restore actions" - "Login shell mode: is_login_shell propagated from main -> App::new, controls key behavior throughout" requirements-completed: [SHEL-01] # Metrics duration: 2min completed: 2026-02-28 --- # Phase 1 Plan 03: App Event Loop and Wiring Summary **Double-press Ctrl+C state machine with login-shell 'q' suppression, signal-polling event loop, BBS goodbye message, and complete startup-to-shutdown pipeline wired in main.rs** ## Performance - **Duration:** 2 min - **Started:** 2026-02-28T20:15:54Z - **Completed:** 2026-02-28T20:17:54Z - **Tasks:** 2 - **Files modified:** 2 ## Accomplishments - App struct implements double-press Ctrl+C with a 2-second window using Option elapsed comparison - Login shell mode suppresses the 'q' key shortcut — only double Ctrl+C exits, enforcing SHEL-01 - Event loop checks SignalFlags.should_terminate() before each draw — fast path for SSH disconnect (SIGHUP/SIGTERM) - Phase 1 placeholder TUI renders a centered BBS-MD block with quit prompt in yellow when Ctrl+C is first pressed - show_goodbye() prints BBS-style "CARRIER LOST" message and sleeps 500ms after terminal is restored - main.rs wires the full pipeline: config load -> panic hook -> signal registration -> terminal init -> event loop -> restore -> goodbye ## Task Commits Each task was committed atomically: 1. **Task 1: Implement App struct and event loop with exit behavior** - `bad8fba` (feat) 2. **Task 2: Wire complete startup-to-shutdown pipeline in main.rs** - `771c9a8` (feat) **Plan metadata:** (recorded after final commit) ## Files Created/Modified - `src/app.rs` - App struct with run_event_loop(), handle_key(), draw(); ShutdownReason enum; show_goodbye(); DOUBLE_PRESS_WINDOW const - `src/main.rs` - Complete startup-to-shutdown pipeline with all four modules declared and wired ## Decisions Made - Called `restore_terminal()` unconditionally before the `match shutdown_reason` block. This ensures terminal is always in cooked mode before `show_goodbye()` prints to stdout, and before any `eprintln!()` on unexpected I/O errors. - Stored `app_config` in the `App` struct (via `App::new(is_login_shell, app_config)`) rather than using `let _app_config`. Phase 2 will read `config.vault_path` from inside the event loop, so putting it on App now avoids a refactor later. - Quit prompt is cleared on any non-Ctrl+C key press. This means pressing Enter, arrow keys, or typing any character will dismiss the "Press Ctrl+C again" prompt — matches natural user expectations. ## Deviations from Plan None - plan executed exactly as written. The only note: the plan listed bare `crossterm::event` in the imports example. Per the 01-02 deviation (crossterm is a transitive dep only), all imports use `ratatui::crossterm::event` instead. This was the established pattern from Plan 02 and applied automatically without needing a new deviation entry. ## Issues Encountered None. Build compiled cleanly on first attempt. All key verification checks passed: - No `unwrap()` in terminal.rs or app.rs - No `LeaveAlternateScreen` in executable code (only in doc comments) - DOUBLE_PRESS_WINDOW constant present and used in event loop - All key link patterns confirmed (should_terminate, restore_terminal, run_event_loop, load_config, init_terminal) ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - Phase 1 is complete: all safety requirements (LIFE-01 through LIFE-04, CONF-01, SHEL-01, SHEL-02) are implemented - The event loop in app.rs is the integration point for Phase 2 content rendering — add content model to App struct and call draw functions from draw() - `app_config.vault_path` is already accessible inside App for Phase 2 vault loading - Blocker noted for Phase 2: verify ratatui 0.30 Widget trait signature (changed between 0.28 and 0.29) before building BBS widgets --- *Phase: 01-safety-foundation* *Completed: 2026-02-28* ## Self-Check: PASSED - src/app.rs: FOUND - src/main.rs: FOUND - 01-03-SUMMARY.md: FOUND - Commit bad8fba (Task 1): FOUND - Commit 771c9a8 (Task 2): FOUND