polyc-tui 2026.9.0

Operator cockpit TUI (pc-tui) for the polychrome control plane: fleet, transcript, approvals, and tools panes.
//! Terminal lifecycle + the four safety patterns:
//! (1) alt-screen enter/leave, (2) panic-safe restore (panic hook + RAII Drop),
//! (3) `SIGWINCH`/resize redraw, (4) `SIGTSTP` suspend / `SIGCONT` resume.
//!
//! `Tui` is the single seam through which the program touches the terminal.

use std::io::{Stdout, stdout};
use std::ops::{Deref, DerefMut};

use color_eyre::Result;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
// crossterm is imported via the `ratatui::crossterm` re-export so its version
// can never drift from the one ratatui itself uses.
use ratatui::crossterm::{
    event::{DisableMouseCapture, EnableMouseCapture},
    execute,
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};

/// The crossterm backend the whole app renders through.
pub(crate) type Backend = CrosstermBackend<Stdout>;

/// Owns the terminal. Deref-es to the inner [`Terminal`] so callers can
/// `tui.draw(...)` directly. Restoring is idempotent and happens on `Drop`.
pub(crate) struct Tui {
    terminal: Terminal<Backend>,
}

impl Tui {
    /// Build a terminal over stdout.
    ///
    /// # Errors
    /// Returns an error if the backing terminal cannot be constructed.
    pub(crate) fn new() -> Result<Self> {
        let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
        Ok(Self { terminal })
    }

    /// Pattern 1: enter the alternate screen + raw mode + mouse capture.
    /// Pattern 2 (install side): register a panic hook that restores the
    /// terminal before the default hook prints, so a panic never leaves the
    /// user in a broken raw-mode alt-screen.
    ///
    /// # Errors
    /// Returns an error if enabling raw mode or entering the alt-screen fails.
    pub(crate) fn enter(&mut self) -> Result<()> {
        enable_raw_mode()?;
        execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?;
        Self::install_panic_hook();
        self.terminal.clear()?;
        Ok(())
    }

    /// Pattern 1: leave the alternate screen + restore cooked mode. Infallible
    /// (the underlying restore is best-effort) and an associated function so it
    /// can run from `Drop`/the panic hook as well as an explicit exit.
    pub(crate) fn exit() {
        Self::restore();
    }

    /// Idempotent, best-effort low-level restore. Safe to call multiple times
    /// (Drop + panic hook + explicit exit may all fire); each step is `let _`d,
    /// so there is nothing to fail.
    fn restore() {
        // Order matters: leave alt-screen first, then disable raw mode.
        let _ = execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture);
        let _ = disable_raw_mode();
    }

    /// Pattern 2 (hook side): chain our restore in front of the existing hook.
    fn install_panic_hook() {
        let hook = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |info| {
            Self::restore();
            hook(info);
        }));
    }

    /// Pattern 4: suspend on Ctrl-Z. Restore the terminal, raise `SIGTSTP` so
    /// the job-control shell stops us, and — once `fg` continues us (the
    /// `raise` call returns after `SIGCONT`) — re-grab the terminal and repaint.
    ///
    /// `signal_hook::low_level::raise` is a safe wrapper, so this needs no
    /// crate-level `unsafe`. The panic hook installed by [`Self::enter`] stays
    /// in force across the stop, so re-grabbing does not reinstall it.
    ///
    /// # Errors
    /// Returns an error if restoring or re-entering the terminal fails.
    pub(crate) fn suspend(&mut self) -> Result<()> {
        Self::restore();
        #[cfg(unix)]
        signal_hook::low_level::raise(signal_hook::consts::SIGTSTP)?;
        // Resumed: re-enter raw mode + the alternate screen and repaint.
        enable_raw_mode()?;
        execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?;
        self.terminal.clear()?;
        Ok(())
    }
}

impl Deref for Tui {
    type Target = Terminal<Backend>;
    fn deref(&self) -> &Self::Target {
        &self.terminal
    }
}

impl DerefMut for Tui {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.terminal
    }
}

/// Pattern 2 (RAII side): even on a normal `?`-bail / early return that never
/// reaches `exit()`, `Drop` restores the terminal. Redundant with the panic
/// hook on purpose — `restore` is idempotent.
impl Drop for Tui {
    fn drop(&mut self) {
        Self::restore();
    }
}