Skip to main content

steer_tui/tui/
terminal.rs

1use ratatui::crossterm::{
2    event::{
3        DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableFocusChange,
4        EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
5        PushKeyboardEnhancementFlags,
6    },
7    execute,
8    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
9};
10use std::io::{self, Write};
11use std::sync::atomic::{AtomicBool, Ordering};
12
13/// Global terminal state to make cleanup idempotent across exit, panic and signal paths
14pub struct TerminalState {
15    pub(crate) raw: AtomicBool,
16    pub(crate) alt_screen: AtomicBool,
17    pub(crate) bracketed_paste: AtomicBool,
18    pub(crate) keyboard_flags_pushed: AtomicBool,
19    pub(crate) mouse_capture: AtomicBool,
20    pub(crate) focus_change: AtomicBool,
21}
22
23impl Default for TerminalState {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl TerminalState {
30    pub const fn new() -> Self {
31        Self {
32            raw: AtomicBool::new(false),
33            alt_screen: AtomicBool::new(false),
34            bracketed_paste: AtomicBool::new(false),
35            keyboard_flags_pushed: AtomicBool::new(false),
36            mouse_capture: AtomicBool::new(false),
37            focus_change: AtomicBool::new(false),
38        }
39    }
40}
41
42pub static TERMINAL_STATE: TerminalState = TerminalState::new();
43
44/// Set up terminal modes and features. Flags are updated only after each successful step.
45pub fn setup<W: Write>(w: &mut W) -> io::Result<()> {
46    // raw mode
47    enable_raw_mode()?;
48    TERMINAL_STATE.raw.store(true, Ordering::Relaxed);
49
50    // alt screen
51    execute!(w, EnterAlternateScreen)?;
52    TERMINAL_STATE.alt_screen.store(true, Ordering::Relaxed);
53
54    // bracketed paste
55    execute!(w, ratatui::crossterm::event::EnableBracketedPaste)?;
56    TERMINAL_STATE
57        .bracketed_paste
58        .store(true, Ordering::Relaxed);
59
60    // keyboard enhancement flags
61    execute!(
62        w,
63        PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
64    )?;
65    TERMINAL_STATE
66        .keyboard_flags_pushed
67        .store(true, Ordering::Relaxed);
68
69    // mouse capture
70    execute!(w, EnableMouseCapture)?;
71    TERMINAL_STATE.mouse_capture.store(true, Ordering::Relaxed);
72
73    // focus change
74    execute!(w, EnableFocusChange)?;
75    TERMINAL_STATE.focus_change.store(true, Ordering::Relaxed);
76
77    Ok(())
78}
79
80/// Cleanup helper that writes escape sequences to the provided writer.
81/// Uses global flags to avoid double-disabling.
82pub fn cleanup_with_writer<W: Write>(writer: &mut W) {
83    if TERMINAL_STATE
84        .keyboard_flags_pushed
85        .swap(false, Ordering::Relaxed)
86    {
87        let _ = execute!(writer, PopKeyboardEnhancementFlags);
88    }
89    if TERMINAL_STATE.focus_change.swap(false, Ordering::Relaxed) {
90        let _ = execute!(writer, DisableFocusChange);
91    }
92    if TERMINAL_STATE.mouse_capture.swap(false, Ordering::Relaxed) {
93        let _ = execute!(writer, DisableMouseCapture);
94    }
95    if TERMINAL_STATE
96        .bracketed_paste
97        .swap(false, Ordering::Relaxed)
98    {
99        let _ = execute!(writer, DisableBracketedPaste);
100    }
101    if TERMINAL_STATE.alt_screen.swap(false, Ordering::Relaxed) {
102        let _ = execute!(writer, LeaveAlternateScreen);
103    }
104    if TERMINAL_STATE.raw.swap(false, Ordering::Relaxed) {
105        let _ = disable_raw_mode();
106    }
107    let _ = writer.flush();
108}
109
110/// Best-effort cleanup across common output streams.
111pub fn cleanup() {
112    {
113        let mut out = io::stdout();
114        cleanup_with_writer(&mut out);
115        let _ = out.flush();
116    }
117    {
118        let mut err = io::stderr();
119        cleanup_with_writer(&mut err);
120        let _ = err.flush();
121    }
122    #[cfg(not(windows))]
123    if let Ok(mut tty) = std::fs::OpenOptions::new().write(true).open("/dev/tty") {
124        cleanup_with_writer(&mut tty);
125        let _ = tty.flush();
126    }
127}
128
129/// RAII guard used during terminal setup to ensure cleanup on early-return paths.
130/// It does not track per-step state; it relies on the global TERMINAL_STATE flags.
131pub struct SetupGuard {
132    armed: bool,
133}
134
135impl Default for SetupGuard {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl SetupGuard {
142    pub fn new() -> Self {
143        Self { armed: true }
144    }
145
146    pub fn disarm(&mut self) {
147        self.armed = false;
148    }
149}
150
151impl Drop for SetupGuard {
152    fn drop(&mut self) {
153        if self.armed {
154            cleanup();
155        }
156    }
157}