199 lines
8.7 KiB
Markdown
199 lines
8.7 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/Users/ruohki/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/Users/ruohki/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/01-safety-foundation/01-RESEARCH.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Implement terminal init/restore and panic hook</name>
|
|
<files>src/terminal.rs</files>
|
|
<action>
|
|
Create `src/terminal.rs` with:
|
|
|
|
1. **`init_terminal() -> std::io::Result<Terminal<CrosstermBackend<std::io::Stdout>>>`**
|
|
- 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<CrosstermBackend<std::io::Stdout>>;
|
|
```
|
|
</action>
|
|
<verify>
|
|
`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.
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Implement signal handling and file logging stub</name>
|
|
<files>src/signals.rs</files>
|
|
<action>
|
|
Create `src/signals.rs` with:
|
|
|
|
1. **`SignalFlags` struct:**
|
|
```rust
|
|
pub struct SignalFlags {
|
|
pub terminate: Arc<AtomicBool>,
|
|
}
|
|
```
|
|
|
|
2. **`register_signals() -> std::io::Result<SignalFlags>`:**
|
|
- 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;
|
|
```
|
|
</action>
|
|
<verify>
|
|
`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`.
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-safety-foundation/01-02-SUMMARY.md`
|
|
</output>
|