--- phase: 01-safety-foundation plan: 02 type: execute wave: 1 depends_on: [] files_modified: - src/terminal.rs - src/signals.rs autonomous: true requirements: - LIFE-01 - LIFE-02 - LIFE-03 - LIFE-04 must_haves: truths: - "When the app panics, raw mode is disabled and a friendly BBS message is printed before exiting" - "Panic details go to stderr for sysop; user sees only the friendly message" - "SIGHUP sets a terminate flag that the event loop can poll" - "SIGTERM sets a terminate flag that the event loop can poll" - "Writing to stdout after SSH disconnect does not panic (broken pipe handled)" - "Terminal init uses main screen buffer (no alternate screen) with raw mode and clear" - "Terminal restore disables raw mode and shows cursor without calling LeaveAlternateScreen" artifacts: - path: "src/terminal.rs" provides: "init_terminal(), restore_terminal(), install_panic_hook()" contains: "disable_raw_mode" - path: "src/signals.rs" provides: "SignalFlags, register_signals()" contains: "AtomicBool" key_links: - from: "src/terminal.rs" to: "crossterm" via: "enable_raw_mode, disable_raw_mode, Clear, cursor::Show" pattern: "enable_raw_mode" - from: "src/terminal.rs" to: "ratatui" via: "Terminal::with_options(Viewport::Fullscreen)" pattern: "Viewport::Fullscreen" - from: "src/signals.rs" to: "signal-hook" via: "flag::register for SIGHUP and SIGTERM" pattern: "flag::register" --- Implement terminal initialization (main screen, no alternate buffer), terminal restoration, panic hook with friendly BBS messaging, UNIX signal handling for SIGHUP/SIGTERM, and broken pipe safety. These are the safety primitives that all other code depends on. Purpose: The login-shell deployment context means any terminal corruption locks out SSH users. This plan establishes the safety envelope: every exit path (panic, signal, normal quit) restores the terminal correctly. Output: `src/terminal.rs` with init/restore/panic-hook, `src/signals.rs` with signal flag registration. @/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md @/Users/ruohki/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-safety-foundation/01-RESEARCH.md Task 1: Implement terminal init/restore and panic hook src/terminal.rs Create `src/terminal.rs` with: 1. **`init_terminal() -> std::io::Result>>`** - Call `crossterm::terminal::enable_raw_mode()?` - Call `crossterm::execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?` — clears main screen buffer for immersive BBS feel - Create `CrosstermBackend::new(stdout())` - Return `Terminal::with_options(backend, TerminalOptions { viewport: Viewport::Fullscreen })` - IMPORTANT: Do NOT enter alternate screen. Do NOT use `ratatui::init()`. 2. **`restore_terminal()`** (no return value — must never panic) - `let _ = crossterm::terminal::disable_raw_mode();` - `let _ = crossterm::execute!(std::io::stdout(), crossterm::cursor::Show);` - Do NOT call `ratatui::restore()` (it calls LeaveAlternateScreen which we never entered) - Every operation uses `let _ =` to suppress errors — this runs in cleanup paths including panic hook 3. **`install_panic_hook()`** - Call `std::panic::take_hook()` to capture the original hook - Call `std::panic::set_hook(Box::new(move |panic_info| { ... }))` with a closure that: a. Calls `restore_terminal()` to clean up the terminal b. Prints friendly BBS-themed message to stderr with `eprintln!()`: ``` \r\n+----------------------------------------------+ | SYSTEM ERROR: An unexpected fault occurred. | | The BBS has exited safely. | | SysOp has been notified. | +----------------------------------------------+\r ``` (Use `\r\n` and trailing `\r` for proper display after raw mode restoration) c. Calls the original hook with `panic_info` — this outputs the technical backtrace to stderr (captured by systemd journal / SSH log) - All I/O in the hook uses `let _ =` or `eprintln!()` (which silently ignores errors). Never `unwrap()` inside the hook. 4. **Broken pipe safety (LIFE-04):** At the module level, add a doc comment noting that all write operations in the TUI must use `let _ =` or handle `ErrorKind::BrokenPipe`. The actual enforcement happens in the event loop (Plan 03), but restore_terminal() already follows this pattern with `let _ =` on every call. Use these imports: ```rust use std::io::stdout; use crossterm::{ execute, terminal::{enable_raw_mode, disable_raw_mode, Clear, ClearType}, cursor::{MoveTo, Show}, }; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; ``` Export the Terminal type alias for use in app.rs: ```rust pub type Term = Terminal>; ``` `cargo build` succeeds (after adding `mod terminal;` temporarily to main.rs or just checking compilation). Review: `init_terminal` does NOT contain `EnterAlternateScreen`. `restore_terminal` does NOT contain `LeaveAlternateScreen`. Panic hook does NOT contain any `unwrap()` or `?` operator. Terminal init enters raw mode and clears main screen without alternate screen buffer. Restore disables raw mode and shows cursor with `let _ =` on every call. Panic hook restores terminal, prints friendly BBS message, then delegates to original hook for technical details. Task 2: Implement signal handling and file logging stub src/signals.rs Create `src/signals.rs` with: 1. **`SignalFlags` struct:** ```rust pub struct SignalFlags { pub terminate: Arc, } ``` 2. **`register_signals() -> std::io::Result`:** - Create `terminate = Arc::new(AtomicBool::new(false))` - Register SIGTERM: `signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&terminate))?` - Register SIGHUP: `signal_hook::flag::register(signal_hook::consts::SIGHUP, Arc::clone(&terminate))?` - Do NOT register SIGINT — Ctrl+C is handled as a crossterm key event in the event loop (avoids double-handling per research findings) - Return `Ok(SignalFlags { terminate })` 3. **`SignalFlags::should_terminate(&self) -> bool`:** - Returns `self.terminate.load(Ordering::Relaxed)` - Convenience method for polling in the event loop 4. **LIFE-03 (logging):** Add a simple `init_logging()` function stub. For Phase 1, logging is minimal — the requirement is that after TUI init, nothing writes to stdout/stderr directly. The panic hook is the exception (it restores terminal first). For now, `init_logging()` is a no-op that returns `()`. A doc comment should note: "Phase 1: no file logging needed yet. When LIFE-03 becomes relevant in Phase 2+, replace this with file-based logging. For now, the rule is simply: do not write to stdout/stderr after terminal init except through ratatui's draw cycle or after calling restore_terminal()." Use these imports: ```rust use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use signal_hook::consts::signal::{SIGHUP, SIGTERM}; use signal_hook::flag as signal_flag; ``` `cargo build` succeeds (after adding `mod signals;` temporarily to main.rs). Review: SIGINT is NOT registered (only SIGHUP and SIGTERM). `should_terminate()` uses `Ordering::Relaxed`. SIGHUP and SIGTERM set a shared AtomicBool flag. SignalFlags provides a `should_terminate()` method for polling. SIGINT is deliberately not registered (Ctrl+C handled via crossterm key events). Logging stub documents the stdout/stderr rule for LIFE-03. 1. `cargo build` compiles with both new modules 2. `init_terminal()` uses `Viewport::Fullscreen` and never enters alternate screen 3. `restore_terminal()` uses `let _ =` for every operation 4. Panic hook contains no `unwrap()` or `?` — only `let _ =` and `eprintln!()` 5. Signal registration covers SIGHUP and SIGTERM but NOT SIGINT 6. `should_terminate()` polls the AtomicBool - Terminal initializes on main screen buffer with raw mode - Panic hook restores terminal and prints BBS-friendly message - Signals set a pollable flag for clean shutdown - No alternate screen buffer is used anywhere - All cleanup paths are panic-safe (no unwrap in error paths) After completion, create `.planning/phases/01-safety-foundation/01-02-SUMMARY.md`