feat(01-02): implement terminal init/restore and panic hook
- 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
This commit is contained in:
+25
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<CrosstermBackend<std::io::Stdout>>;
|
||||||
|
|
||||||
|
/// 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<Term> {
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user