altui-core 0.2.0

A library to build rich terminal user interfaces or dashboards
Documentation
//! # AltuiInit
//!
//! `AltuiInit` is a small helper around a Crossterm-based terminal that provides
//! a safe and ergonomic way to:
//!
//! - initialize the terminal (raw mode, alternate screen, optional mouse capture)
//! - restore the original terminal state on exit
//! - recover the terminal state even if the application panics
//!
//! The goal of `AltuiInit` is **not** to hide Crossterm or `Terminal`, but to
//! eliminate repetitive and error-prone boilerplate code commonly found in
//! TUI applications.
//!
//! ## Basic usage
//!
//! ```rust,no_run
//! use altui_core::{AltuiInit, widgets::{Block, Borders}};
//!
//! fn main() -> std::io::Result<()> {
//!     AltuiInit::new(true)?
//!         .set_panic_hook()
//!         .run(|terminal| {
//!             terminal.draw(|f| {
//!                 let size = f.size();
//!                 let mut block = Block::default()
//!                     .title("Block")
//!                     .borders(Borders::ALL);
//!                 f.render_widget(&mut block, size);
//!             })?;
//!
//!             Ok(())
//!         })
//! }
//! ```
//!
//! This example is functionally equivalent to a much more verbose setup using
//! raw Crossterm primitives (see below), but guarantees that the terminal will
//! be restored correctly even in the presence of errors or panics.
//!
//! ## What does `AltuiInit` replace?
//!
//! The example above replaces the following boilerplate code:
//!
//! ```rust,no_run
//! use std::{io, thread, time::Duration};
//! use altui_core::{
//!     backend::CrosstermBackend,
//!     widgets::{Widget, Block, Borders},
//!     Terminal,
//! };
//! use crossterm::{
//!     event::{DisableMouseCapture, EnableMouseCapture},
//!     execute,
//!     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
//! };
//!
//! fn main() -> Result<(), io::Error> {
//!     enable_raw_mode()?;
//!     let mut stdout = io::stdout();
//!     execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
//!
//!     let backend = CrosstermBackend::new(stdout);
//!     let mut terminal = Terminal::new(backend)?;
//!
//!     terminal.draw(|f| {
//!         let size = f.size();
//!         let mut block = Block::default()
//!             .title("Block")
//!             .borders(Borders::ALL);
//!         f.render_widget(&mut block, size);
//!     })?;
//!
//!     thread::sleep(Duration::from_millis(5000));
//!
//!     disable_raw_mode()?;
//!     execute!(
//!         terminal.backend_mut(),
//!         LeaveAlternateScreen,
//!         DisableMouseCapture
//!     )?;
//!     terminal.show_cursor()?;
//!
//!     Ok(())
//! }
//! ```
//!
//! `AltuiInit` encapsulates this setup and teardown logic in a single RAII type,
//! reducing the chance of leaving the terminal in a broken state.
//!
//! ## Panic handling
//!
//! Terminal-based applications modify global terminal state
//! (raw mode, alternate screen, cursor visibility, mouse capture).
//! If a panic occurs, this state must still be restored.
//!
//! `AltuiInit` addresses this problem on two levels:
//!
//! 1. **RAII cleanup**
//!    The terminal state is restored in `Drop`, which guarantees cleanup
//!    when execution leaves the `AltuiInit` scope normally or due to unwinding.
//!
//! 2. **Optional panic hook**
//!    Calling [`AltuiInit::set_panic_hook`] installs a default panic hook that
//!    performs a best-effort terminal reset before delegating to the original
//!    panic handler.
//!
//! ```rust,ignore
//! use altui_core::AltuiInit;
//!
//! AltuiInit::new(true)?
//!     .set_panic_hook()
//!     .run(|terminal| {
//!         /* application code */
//!         Ok(())
//!     });
//! ```
//!
//! This hook is global and will also trigger if a panic occurs in another
//! thread or outside the immediate control flow of `AltuiInit`.
use std::io::Stdout;

use crate::{backend::CrosstermBackend, Terminal};

/// Installs the default panic hook used by [`AltuiInit::set_panic_hook`].
///
/// This hook performs a best-effort restoration of the terminal state
/// before delegating to the previously installed panic hook.
///
/// Specifically, it attempts to:
///
/// - disable raw mode
/// - leave the alternate screen
/// - disable mouse capture
///
/// The hook is **global** and applies to panics from all threads.
///
/// # Notes
///
/// - This function does **not** replace RAII-based cleanup.
///   It is intended as a safety net for panics occurring outside the normal
///   control flow (e.g. in background threads).
///
/// - Calling this function is optional. Advanced users may prefer to install
///   their own panic hook or handle panics manually.
///
/// - The hook does not suppress the panic; it only ensures terminal recovery.
///
/// # See also
///
/// - [`AltuiInit::set_panic_hook`]
pub fn install_panic_hook() {
    let default = std::panic::take_hook();

    std::panic::set_hook(Box::new(move |info| {
        let _ = crossterm::terminal::disable_raw_mode();
        let _ = crossterm::execute!(
            std::io::stdout(),
            crossterm::terminal::LeaveAlternateScreen,
            crossterm::event::DisableMouseCapture
        );

        let _ = default(info);
    }));
}

/// Crossterm terminal initialization helper which restores the original
/// terminal state on drop.
///
/// `AltuiInit` provides a minimal RAII wrapper around a Crossterm-based
/// [`Terminal`]. It is designed to eliminate repetitive setup and teardown
/// code while keeping full control over rendering and event handling.
///
/// ## Responsibilities
///
/// - enable raw mode
/// - enter the alternate screen
/// - optionally enable mouse capture
/// - restore the terminal state on drop
///
/// ## What `AltuiInit` does *not* do
///
/// - manage an event loop
/// - handle input
/// - hide the underlying `Terminal` API
///
/// ## Full control
///
/// If you need full control over terminal initialization or teardown
/// (for example, skipping `LeaveAlternateScreen` or managing cursor state
/// manually), you can always bypass `AltuiInit` and use Crossterm directly.
/// `AltuiInit` is a convenience layer, not a restriction.
pub struct AltuiInit {
    terminal: Terminal<CrosstermBackend<Stdout>>,
    mouse: bool,
}

impl AltuiInit {
    /// Creates a new `AltuiInit` instance and initializes the terminal.
    ///
    /// This method:
    /// - enables raw mode
    /// - enters the alternate screen
    /// - optionally enables mouse capture
    /// - constructs a Crossterm-backed [`Terminal`]
    ///
    /// The original terminal state is restored automatically when the
    /// returned `AltuiInit` value is dropped.
    ///
    /// # Parameters
    ///
    /// - `mouse`: enables mouse capture if `true`
    ///
    /// # Errors
    ///
    /// Returns an error if any of the terminal initialization steps fail.
    ///
    /// # Notes
    ///
    /// This function assumes ownership of the terminal state. If your application
    /// requires custom or partial terminal initialization, consider using
    /// Crossterm directly instead of `AltuiInit`.
    pub fn new(mouse: bool) -> std::io::Result<Self> {
        use crossterm::{
            event::EnableMouseCapture,
            execute,
            terminal::{enable_raw_mode, EnterAlternateScreen},
        };

        enable_raw_mode()?;
        let mut stdout = std::io::stdout();
        execute!(stdout, EnterAlternateScreen)?;
        if mouse {
            execute!(stdout, EnableMouseCapture)?;
        }

        let backend = crate::backend::CrosstermBackend::new(stdout);
        let terminal = crate::Terminal::new(backend)?;

        Ok(Self { terminal, mouse })
    }

    /// Installs the default panic hook used by `AltuiInit`.
    ///
    /// The installed hook performs a best-effort restoration of the terminal
    /// state if a panic occurs, even if the panic originates from another thread.
    ///
    /// This method is optional. It is provided as a convenience for applications
    /// that want a safe default panic behavior without installing a global
    /// panic hook manually.
    ///
    /// # Notes
    ///
    /// - The panic hook is global and affects the entire process.
    /// - Calling this method does not suppress panics.
    /// - Terminal cleanup via `Drop` remains the primary cleanup mechanism.
    ///
    /// # See also
    ///
    /// - [`install_panic_hook`]
    pub fn set_panic_hook(self) -> Self {
        install_panic_hook();
        self
    }

    /// Returns a mutable reference to the underlying [`Terminal`].
    ///
    /// This allows full access to the rendering API without abstraction or
    /// restriction. `AltuiInit` does not attempt to hide or wrap the terminal
    /// interface.
    ///
    /// # Notes
    ///
    /// The returned reference is valid for the lifetime of `AltuiInit`. Terminal
    /// state will still be restored when `AltuiInit` is dropped.
    pub fn terminal(&mut self) -> &mut crate::Terminal<CrosstermBackend<Stdout>> {
        &mut self.terminal
    }

    /// Executes the provided closure with a mutable reference to the terminal.
    ///
    /// This method serves as a convenience entry point that scopes terminal usage
    /// to the lifetime of the closure.
    ///
    /// # Parameters
    ///
    /// - `f`: a closure that receives a mutable reference to the terminal
    ///
    /// # Errors
    ///
    /// Propagates any error returned by the closure.
    ///
    /// # Notes
    ///
    /// - This method does not implement an event loop.
    /// - It does not catch panics; panic handling is delegated to `Drop` and
    ///   the optional panic hook.
    /// - Calling this method is equivalent to manually borrowing the terminal
    ///   via [`AltuiInit::terminal`].
    pub fn run<F>(&mut self, mut f: F) -> std::io::Result<()>
    where
        F: FnMut(&mut Terminal<CrosstermBackend<Stdout>>) -> std::io::Result<()>,
    {
        f(&mut self.terminal)
    }
}

impl Drop for AltuiInit {
    fn drop(&mut self) {
        use crossterm::{
            event::DisableMouseCapture,
            execute,
            terminal::{disable_raw_mode, LeaveAlternateScreen},
        };

        let _ = disable_raw_mode();

        let backend = self.terminal.backend_mut();
        let _ = execute!(backend, LeaveAlternateScreen);

        if self.mouse {
            let _ = execute!(backend, DisableMouseCapture);
        }

        let _ = self.terminal.show_cursor();
    }
}