From b258b6d262168898c5411ba74b88d666117bc432 Mon Sep 17 00:00:00 2001 From: ruohki Date: Sat, 28 Feb 2026 21:12:04 +0100 Subject: [PATCH] feat(01-02): implement terminal init/restore and panic hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - init_terminal() enables raw mode and clears main screen via Viewport::Fullscreen - No alternate screen buffer used (deliberate BBS immersive approach) - restore_terminal() disables raw mode and shows cursor using let _ for all calls - install_panic_hook() restores terminal, prints BBS-themed message, delegates to original hook - Use ratatui::crossterm re-export (crossterm is transitive dep only) - All panic hook operations use let _ or eprintln! — no unwrap() or ? inside hook --- src/main.rs | 25 +++++++++++++ src/terminal.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/main.rs create mode 100644 src/terminal.rs diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8d34b7e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,25 @@ +mod config; +mod terminal; + +fn main() { + // 1. Detect login shell BEFORE stripping argv[0] + let is_login_shell = config::detect_login_shell(); + + // 2. Parse CLI (strips dash from argv[0] internally) + let cli = config::parse_cli(); + + // 3. Resolve config path and load config + let config_path = config::resolve_config_path(cli.config.as_deref()); + let app_config = match config::load_config(&config_path) { + Ok(c) => c, + Err(e) => { + config::print_config_error(&e); + std::process::exit(1); + } + }; + + // Phase 1 placeholder: print loaded config for verification + // This will be replaced by terminal init + event loop in Plan 03 + println!("Config loaded: {:?}", app_config); + println!("Login shell: {}", is_login_shell); +} diff --git a/src/terminal.rs b/src/terminal.rs new file mode 100644 index 0000000..60a1c90 --- /dev/null +++ b/src/terminal.rs @@ -0,0 +1,93 @@ +//! 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); + })); +}