//! Terminal initialization, restoration, and panic hook for bbs-md. //! //! # Design //! //! This module uses the main screen buffer (no alternate screen) for the immersive BBS feel. //! On exit, TUI output remains visible in the terminal scroll history — the user can scroll //! up to review what they read, and the goodbye message persists after the process exits. //! //! # Broken Pipe Safety (LIFE-04) //! //! All write operations in the TUI must use `let _ =` or handle `ErrorKind::BrokenPipe`. //! SSH connections can close mid-write; Rust propagates this as `ErrorKind::BrokenPipe`. //! The actual enforcement happens in the event loop (Plan 03), but `restore_terminal()` //! already follows this pattern with `let _ =` on every call. //! //! # Important //! //! Do NOT call `ratatui::restore()` — it enters alternate screen via `LeaveAlternateScreen`, //! which we never entered. Use `restore_terminal()` from this module instead. use std::io::stdout; use ratatui::crossterm::{ execute, terminal::{enable_raw_mode, disable_raw_mode, Clear, ClearType}, cursor::{MoveTo, Show}, }; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; /// Type alias for the terminal used throughout the application. pub type Term = Terminal>; /// Initialize the terminal on the main screen buffer with raw mode enabled. /// /// This does NOT enter the alternate screen buffer. Instead it clears the main screen /// so the TUI occupies the user's actual terminal. On exit, TUI output remains in /// scroll history. /// /// # Errors /// /// Returns an error if raw mode cannot be enabled or if the screen cannot be cleared. pub fn init_terminal() -> std::io::Result { enable_raw_mode()?; execute!(stdout(), Clear(ClearType::All), MoveTo(0, 0))?; let backend = CrosstermBackend::new(stdout()); Terminal::with_options(backend, TerminalOptions { viewport: Viewport::Fullscreen, }) } /// Restore the terminal to a usable state. /// /// Disables raw mode and shows the cursor. Every operation uses `let _ =` to suppress /// errors — this function is called from cleanup paths including the panic hook, where /// panicking would hide the original error. /// /// Do NOT call `ratatui::restore()` — it unconditionally calls `LeaveAlternateScreen`, /// which corrupts the terminal since we never entered the alternate screen. pub fn restore_terminal() { let _ = disable_raw_mode(); let _ = execute!(std::io::stdout(), Show); } /// Install the custom panic hook with BBS-friendly user messaging. /// /// The hook: /// 1. Restores the terminal (disables raw mode, shows cursor) /// 2. Prints a friendly BBS-themed message to stderr (visible to the user) /// 3. Delegates to the original panic hook for technical details (captured by systemd/SSH log) /// /// Install this before terminal initialization so any panic during init also triggers cleanup. pub fn install_panic_hook() { let original_hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |panic_info| { // Restore terminal — use let _ to avoid double-panic if cleanup itself fails let _ = ratatui::crossterm::terminal::disable_raw_mode(); let _ = ratatui::crossterm::execute!( std::io::stdout(), ratatui::crossterm::cursor::Show ); // Friendly message visible to the user in their terminal. // Use \r\n because we may still be in raw mode if disable_raw_mode failed. eprintln!("\r\n+----------------------------------------------+"); eprintln!("| SYSTEM ERROR: An unexpected fault occurred. |"); eprintln!("| The BBS has exited safely. |"); eprintln!("| SysOp has been notified. |"); eprintln!("+----------------------------------------------+\r"); // Delegate to the original hook — prints backtrace to stderr, // captured by journald or available in the SSH session log. original_hook(panic_info); })); }