Skip to main content

altui_core/
init.rs

1//! # AltuiInit
2//!
3//! `AltuiInit` is a small helper around a Crossterm-based terminal that provides
4//! a safe and ergonomic way to:
5//!
6//! - initialize the terminal (raw mode, alternate screen, optional mouse capture)
7//! - restore the original terminal state on exit
8//! - recover the terminal state even if the application panics
9//!
10//! The goal of `AltuiInit` is **not** to hide Crossterm or `Terminal`, but to
11//! eliminate repetitive and error-prone boilerplate code commonly found in
12//! TUI applications.
13//!
14//! ## Basic usage
15//!
16//! ```rust,no_run
17//! use altui_core::{AltuiInit, widgets::{Block, Borders}};
18//!
19//! fn main() -> std::io::Result<()> {
20//!     AltuiInit::new(true)?
21//!         .set_panic_hook()
22//!         .run(|terminal| {
23//!             terminal.draw(|f| {
24//!                 let size = f.size();
25//!                 let mut block = Block::default()
26//!                     .title("Block")
27//!                     .borders(Borders::ALL);
28//!                 f.render_widget(&mut block, size);
29//!             })?;
30//!
31//!             Ok(())
32//!         })
33//! }
34//! ```
35//!
36//! This example is functionally equivalent to a much more verbose setup using
37//! raw Crossterm primitives (see below), but guarantees that the terminal will
38//! be restored correctly even in the presence of errors or panics.
39//!
40//! ## What does `AltuiInit` replace?
41//!
42//! The example above replaces the following boilerplate code:
43//!
44//! ```rust,no_run
45//! use std::{io, thread, time::Duration};
46//! use altui_core::{
47//!     backend::CrosstermBackend,
48//!     widgets::{Widget, Block, Borders},
49//!     Terminal,
50//! };
51//! use crossterm::{
52//!     event::{DisableMouseCapture, EnableMouseCapture},
53//!     execute,
54//!     terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
55//! };
56//!
57//! fn main() -> Result<(), io::Error> {
58//!     enable_raw_mode()?;
59//!     let mut stdout = io::stdout();
60//!     execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
61//!
62//!     let backend = CrosstermBackend::new(stdout);
63//!     let mut terminal = Terminal::new(backend)?;
64//!
65//!     terminal.draw(|f| {
66//!         let size = f.size();
67//!         let mut block = Block::default()
68//!             .title("Block")
69//!             .borders(Borders::ALL);
70//!         f.render_widget(&mut block, size);
71//!     })?;
72//!
73//!     thread::sleep(Duration::from_millis(5000));
74//!
75//!     disable_raw_mode()?;
76//!     execute!(
77//!         terminal.backend_mut(),
78//!         LeaveAlternateScreen,
79//!         DisableMouseCapture
80//!     )?;
81//!     terminal.show_cursor()?;
82//!
83//!     Ok(())
84//! }
85//! ```
86//!
87//! `AltuiInit` encapsulates this setup and teardown logic in a single RAII type,
88//! reducing the chance of leaving the terminal in a broken state.
89//!
90//! ## Panic handling
91//!
92//! Terminal-based applications modify global terminal state
93//! (raw mode, alternate screen, cursor visibility, mouse capture).
94//! If a panic occurs, this state must still be restored.
95//!
96//! `AltuiInit` addresses this problem on two levels:
97//!
98//! 1. **RAII cleanup**
99//!    The terminal state is restored in `Drop`, which guarantees cleanup
100//!    when execution leaves the `AltuiInit` scope normally or due to unwinding.
101//!
102//! 2. **Optional panic hook**
103//!    Calling [`AltuiInit::set_panic_hook`] installs a default panic hook that
104//!    performs a best-effort terminal reset before delegating to the original
105//!    panic handler.
106//!
107//! ```rust,ignore
108//! use altui_core::AltuiInit;
109//!
110//! AltuiInit::new(true)?
111//!     .set_panic_hook()
112//!     .run(|terminal| {
113//!         /* application code */
114//!         Ok(())
115//!     });
116//! ```
117//!
118//! This hook is global and will also trigger if a panic occurs in another
119//! thread or outside the immediate control flow of `AltuiInit`.
120use std::io::Stdout;
121
122use crate::{backend::CrosstermBackend, Terminal};
123
124/// Installs the default panic hook used by [`AltuiInit::set_panic_hook`].
125///
126/// This hook performs a best-effort restoration of the terminal state
127/// before delegating to the previously installed panic hook.
128///
129/// Specifically, it attempts to:
130///
131/// - disable raw mode
132/// - leave the alternate screen
133/// - disable mouse capture
134///
135/// The hook is **global** and applies to panics from all threads.
136///
137/// # Notes
138///
139/// - This function does **not** replace RAII-based cleanup.
140///   It is intended as a safety net for panics occurring outside the normal
141///   control flow (e.g. in background threads).
142///
143/// - Calling this function is optional. Advanced users may prefer to install
144///   their own panic hook or handle panics manually.
145///
146/// - The hook does not suppress the panic; it only ensures terminal recovery.
147///
148/// # See also
149///
150/// - [`AltuiInit::set_panic_hook`]
151pub fn install_panic_hook() {
152    let default = std::panic::take_hook();
153
154    std::panic::set_hook(Box::new(move |info| {
155        let _ = crossterm::terminal::disable_raw_mode();
156        let _ = crossterm::execute!(
157            std::io::stdout(),
158            crossterm::terminal::LeaveAlternateScreen,
159            crossterm::event::DisableMouseCapture
160        );
161
162        let _ = default(info);
163    }));
164}
165
166/// Crossterm terminal initialization helper which restores the original
167/// terminal state on drop.
168///
169/// `AltuiInit` provides a minimal RAII wrapper around a Crossterm-based
170/// [`Terminal`]. It is designed to eliminate repetitive setup and teardown
171/// code while keeping full control over rendering and event handling.
172///
173/// ## Responsibilities
174///
175/// - enable raw mode
176/// - enter the alternate screen
177/// - optionally enable mouse capture
178/// - restore the terminal state on drop
179///
180/// ## What `AltuiInit` does *not* do
181///
182/// - manage an event loop
183/// - handle input
184/// - hide the underlying `Terminal` API
185///
186/// ## Full control
187///
188/// If you need full control over terminal initialization or teardown
189/// (for example, skipping `LeaveAlternateScreen` or managing cursor state
190/// manually), you can always bypass `AltuiInit` and use Crossterm directly.
191/// `AltuiInit` is a convenience layer, not a restriction.
192pub struct AltuiInit {
193    terminal: Terminal<CrosstermBackend<Stdout>>,
194    mouse: bool,
195}
196
197impl AltuiInit {
198    /// Creates a new `AltuiInit` instance and initializes the terminal.
199    ///
200    /// This method:
201    /// - enables raw mode
202    /// - enters the alternate screen
203    /// - optionally enables mouse capture
204    /// - constructs a Crossterm-backed [`Terminal`]
205    ///
206    /// The original terminal state is restored automatically when the
207    /// returned `AltuiInit` value is dropped.
208    ///
209    /// # Parameters
210    ///
211    /// - `mouse`: enables mouse capture if `true`
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if any of the terminal initialization steps fail.
216    ///
217    /// # Notes
218    ///
219    /// This function assumes ownership of the terminal state. If your application
220    /// requires custom or partial terminal initialization, consider using
221    /// Crossterm directly instead of `AltuiInit`.
222    pub fn new(mouse: bool) -> std::io::Result<Self> {
223        use crossterm::{
224            event::EnableMouseCapture,
225            execute,
226            terminal::{enable_raw_mode, EnterAlternateScreen},
227        };
228
229        enable_raw_mode()?;
230        let mut stdout = std::io::stdout();
231        execute!(stdout, EnterAlternateScreen)?;
232        if mouse {
233            execute!(stdout, EnableMouseCapture)?;
234        }
235
236        let backend = crate::backend::CrosstermBackend::new(stdout);
237        let terminal = crate::Terminal::new(backend)?;
238
239        Ok(Self { terminal, mouse })
240    }
241
242    /// Installs the default panic hook used by `AltuiInit`.
243    ///
244    /// The installed hook performs a best-effort restoration of the terminal
245    /// state if a panic occurs, even if the panic originates from another thread.
246    ///
247    /// This method is optional. It is provided as a convenience for applications
248    /// that want a safe default panic behavior without installing a global
249    /// panic hook manually.
250    ///
251    /// # Notes
252    ///
253    /// - The panic hook is global and affects the entire process.
254    /// - Calling this method does not suppress panics.
255    /// - Terminal cleanup via `Drop` remains the primary cleanup mechanism.
256    ///
257    /// # See also
258    ///
259    /// - [`install_panic_hook`]
260    pub fn set_panic_hook(self) -> Self {
261        install_panic_hook();
262        self
263    }
264
265    /// Returns a mutable reference to the underlying [`Terminal`].
266    ///
267    /// This allows full access to the rendering API without abstraction or
268    /// restriction. `AltuiInit` does not attempt to hide or wrap the terminal
269    /// interface.
270    ///
271    /// # Notes
272    ///
273    /// The returned reference is valid for the lifetime of `AltuiInit`. Terminal
274    /// state will still be restored when `AltuiInit` is dropped.
275    pub fn terminal(&mut self) -> &mut crate::Terminal<CrosstermBackend<Stdout>> {
276        &mut self.terminal
277    }
278
279    /// Executes the provided closure with a mutable reference to the terminal.
280    ///
281    /// This method serves as a convenience entry point that scopes terminal usage
282    /// to the lifetime of the closure.
283    ///
284    /// # Parameters
285    ///
286    /// - `f`: a closure that receives a mutable reference to the terminal
287    ///
288    /// # Errors
289    ///
290    /// Propagates any error returned by the closure.
291    ///
292    /// # Notes
293    ///
294    /// - This method does not implement an event loop.
295    /// - It does not catch panics; panic handling is delegated to `Drop` and
296    ///   the optional panic hook.
297    /// - Calling this method is equivalent to manually borrowing the terminal
298    ///   via [`AltuiInit::terminal`].
299    pub fn run<F>(&mut self, mut f: F) -> std::io::Result<()>
300    where
301        F: FnMut(&mut Terminal<CrosstermBackend<Stdout>>) -> std::io::Result<()>,
302    {
303        f(&mut self.terminal)
304    }
305}
306
307impl Drop for AltuiInit {
308    fn drop(&mut self) {
309        use crossterm::{
310            event::DisableMouseCapture,
311            execute,
312            terminal::{disable_raw_mode, LeaveAlternateScreen},
313        };
314
315        let _ = disable_raw_mode();
316
317        let backend = self.terminal.backend_mut();
318        let _ = execute!(backend, LeaveAlternateScreen);
319
320        if self.mouse {
321            let _ = execute!(backend, DisableMouseCapture);
322        }
323
324        let _ = self.terminal.show_cursor();
325    }
326}