mod app; mod config; mod highlighter; mod renderer; mod signals; mod terminal; mod vault; fn main() { // ── PRE-TERMINAL PHASE ──────────────────────────────────────────────────── // Errors here use normal eprintln! — the terminal is not yet in raw mode. // 1. Detect login shell BEFORE stripping argv[0] // POSIX: kernel prefixes argv[0] with '-' for login shells. let is_login_shell = config::detect_login_shell(); // 2. Parse CLI (strips the leading dash from argv[0] before clap sees it) 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); } }; // 3a. Initialize syntax highlighting (one-time, loads embedded syntax definitions) // Must be called before render_markdown() — highlighter uses OnceLock statics. highlighter::init_highlighter(); // 3b. Load initial document (index.md from vault) and render it. // We query the terminal size here for initial render width. // On resize, the event loop re-renders with the updated width. let initial_width = ratatui::crossterm::terminal::size() .map(|(w, _)| w) .unwrap_or(80); let (initial_doc, raw_content) = match vault::load_document(&app_config.vault_path, "index.md") { vault::VaultDocument::Loaded { path, content } => { let filename = path .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "index.md".to_string()); let lines = renderer::render_markdown(&content, initial_width); let doc = app::DocumentState::Loaded { filename, lines }; (doc, Some(content)) } vault::VaultDocument::Missing { path } => { (app::DocumentState::Missing { path }, None) } vault::VaultDocument::ReadError { path, reason } => { (app::DocumentState::Error { path, reason }, None) } }; // ── TERMINAL PHASE ──────────────────────────────────────────────────────── // Install safety envelope BEFORE terminal init so panics during init are caught. // 4. Install panic hook first — covers panics during terminal init itself terminal::install_panic_hook(); // 5. Register signal handlers before terminal init — covers early SIGHUP // (e.g. SSH disconnect between login shell launch and first draw) let signal_flags = match signals::register_signals() { Ok(s) => s, Err(e) => { eprintln!("SYSTEM ERROR: Cannot register signal handlers: {}", e); std::process::exit(1); } }; // 6. Initialize terminal (enables raw mode, clears screen, sets Viewport::Fullscreen) let mut term = match terminal::init_terminal() { Ok(t) => t, Err(e) => { eprintln!("SYSTEM ERROR: Cannot initialize terminal: {}", e); std::process::exit(1); } }; // ── EVENT LOOP PHASE ────────────────────────────────────────────────────── // 7. Create app state and run the event loop. // raw_content is passed so the event loop can re-render on terminal resize. let mut app_state = app::App::new(is_login_shell, app_config, initial_doc, raw_content); let shutdown_reason = app_state.run_event_loop(&mut term, &signal_flags); // ── SHUTDOWN PHASE ──────────────────────────────────────────────────────── // Terminal must be restored in EVERY exit path below. // 8. Restore terminal — always, regardless of how we got here. // Must happen BEFORE show_goodbye() since goodbye prints to stdout. terminal::restore_terminal(); // 9. Handle the shutdown reason match shutdown_reason { Ok(app::ShutdownReason::UserQuit) => { // User deliberately exited — show BBS goodbye message app::show_goodbye(); } Ok(app::ShutdownReason::Signal) => { // SIGHUP or SIGTERM — SSH disconnect or graceful OS shutdown. // Exit silently: there may be nobody on the other end to see a message. } Err(e) => { // I/O error from the event loop if e.kind() == std::io::ErrorKind::BrokenPipe { // SSH connection closed while we were writing — silent exit (LIFE-04). // Terminal is already restored above; just exit quietly. } else { // Unexpected I/O error — log to stderr after terminal restore eprintln!("SYSTEM ERROR: {}", e); std::process::exit(1); } } } }