---
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)