Skip to main content

tui_panel_select/
terminal.rs

1//! Panic-safe terminal setup for mouse capture (feature `terminal-guard`).
2//!
3//! Enabling this crate's panel-scoped drag-selection means turning on the
4//! terminal's mouse-tracking mode, so the application — not the terminal
5//! emulator — receives mouse events. That global terminal state has to be
6//! undone on exit *and* on any panic, or the user's shell is left with mouse
7//! tracking still switched on and every subsequent mouse move spews raw
8//! tracking escape sequences into the prompt (the terminal appears to fill
9//! with garbage).
10//!
11//! [`TerminalGuard`] centralises that: it enables mouse capture (and,
12//! optionally, the keyboard-enhancement protocol), wraps the current panic
13//! hook so the state is restored even on an unexpected panic — including a
14//! panic raised *inside* a dependency's own event parser — and restores
15//! everything again when the guard is dropped on the normal exit path.
16//!
17//! This lives behind the default-on `terminal-guard` feature so callers who
18//! only want the pure selection/wrapping logic can opt out and avoid the
19//! process-global panic-hook and terminal side effects entirely.
20
21use std::io;
22
23use ratatui::crossterm::event::{
24    DisableMouseCapture, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
25    PushKeyboardEnhancementFlags,
26};
27use ratatui::crossterm::execute;
28use ratatui::crossterm::terminal::supports_keyboard_enhancement;
29
30/// An RAII guard that turns on terminal mouse capture (and optionally the
31/// keyboard-enhancement protocol) for as long as it is alive, and guarantees
32/// the terminal is put back the way it was — both on the normal exit path
33/// (via [`Drop`]) and on any panic (via a wrapped panic hook).
34///
35/// Create it *after* your terminal has been put into raw mode / the alternate
36/// screen (e.g. after `ratatui::init()`), and drop it *before* you tear that
37/// down (e.g. before `ratatui::restore()`):
38///
39/// ```no_run
40/// # fn main() -> std::io::Result<()> {
41/// use tui_panel_select::TerminalGuard;
42///
43/// let mut terminal = ratatui::init();
44/// let guard = TerminalGuard::install(true)?;
45/// let enhanced = guard.keyboard_enhancement_active();
46///
47/// // ... run your event loop, using `enhanced` to decide whether modifier
48/// // combinations like Ctrl+Enter are reported distinctly ...
49///
50/// drop(guard); // restore mouse capture / keyboard flags
51/// ratatui::restore();
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Debug)]
56pub struct TerminalGuard {
57    keyboard_enhancement_active: bool,
58}
59
60impl TerminalGuard {
61    /// Enable mouse capture and install the panic-safe restore hook.
62    ///
63    /// When `keyboard_enhancement` is `true` and the terminal advertises
64    /// support, the keyboard-enhancement (disambiguate-escape-codes) protocol
65    /// is pushed as well, so modifier combinations such as Ctrl+Enter are
66    /// reported distinctly from a plain Enter. Use
67    /// [`keyboard_enhancement_active`](Self::keyboard_enhancement_active) to
68    /// learn whether it actually took effect.
69    ///
70    /// The current panic hook is taken and wrapped: on any panic the wrapper
71    /// disables mouse capture (and pops the keyboard-enhancement flags, if
72    /// they were pushed) before delegating to the previous hook.
73    pub fn install(keyboard_enhancement: bool) -> io::Result<Self> {
74        let enhanced = keyboard_enhancement && supports_keyboard_enhancement().unwrap_or(false);
75        if enhanced {
76            execute!(
77                io::stdout(),
78                PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES),
79            )?;
80        }
81        execute!(io::stdout(), EnableMouseCapture)?;
82
83        let previous_hook = std::panic::take_hook();
84        std::panic::set_hook(Box::new(move |info| {
85            let _ = execute!(io::stdout(), DisableMouseCapture);
86            if enhanced {
87                let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
88            }
89            previous_hook(info);
90        }));
91
92        Ok(Self {
93            keyboard_enhancement_active: enhanced,
94        })
95    }
96
97    /// Whether the keyboard-enhancement protocol was actually enabled (it is
98    /// only enabled when requested *and* supported by the terminal).
99    pub fn keyboard_enhancement_active(&self) -> bool {
100        self.keyboard_enhancement_active
101    }
102}
103
104impl Drop for TerminalGuard {
105    fn drop(&mut self) {
106        if self.keyboard_enhancement_active {
107            let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
108        }
109        let _ = execute!(io::stdout(), DisableMouseCapture);
110    }
111}