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}