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}