Skip to main content

tess/
terminal.rs

1use std::io;
2use std::sync::Arc;
3use std::sync::atomic::AtomicBool;
4
5use crossterm::cursor::{Hide, Show};
6use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
7
8/// RAII guard that enables raw mode + (optionally) alt screen on construction
9/// and restores the terminal on drop (including during panic unwind).
10///
11/// `with_alt_screen = false` is the `less -X` / `--no-init` mode: stay on
12/// the primary screen so content remains in scrollback after exit. Raw
13/// mode is still enabled because we need keystroke capture either way.
14pub struct TerminalGuard {
15    mouse: bool,
16    alt_screen: bool,
17}
18
19impl TerminalGuard {
20    pub fn enter(mouse: bool, with_alt_screen: bool) -> io::Result<Self> {
21        enable_raw_mode()?;
22        if with_alt_screen {
23            crossterm::execute!(io::stdout(), EnterAlternateScreen, Hide)?;
24        } else {
25            crossterm::execute!(io::stdout(), Hide)?;
26        }
27        if mouse {
28            crossterm::execute!(io::stdout(), crossterm::event::EnableMouseCapture)?;
29        }
30        Ok(TerminalGuard { mouse, alt_screen: with_alt_screen })
31    }
32}
33
34impl Drop for TerminalGuard {
35    fn drop(&mut self) {
36        if self.mouse {
37            let _ = crossterm::execute!(io::stdout(), crossterm::event::DisableMouseCapture);
38        }
39        if self.alt_screen {
40            let _ = crossterm::execute!(io::stdout(), Show, LeaveAlternateScreen);
41        } else {
42            let _ = crossterm::execute!(io::stdout(), Show);
43        }
44        let _ = disable_raw_mode();
45    }
46}
47
48/// Restore terminal manually (for panic hook). Idempotent and best-effort.
49pub fn restore_terminal_best_effort() {
50    let _ = crossterm::execute!(io::stdout(), Show, LeaveAlternateScreen);
51    let _ = disable_raw_mode();
52}
53
54pub fn install_panic_hook() {
55    let prev = std::panic::take_hook();
56    std::panic::set_hook(Box::new(move |info| {
57        restore_terminal_best_effort();
58        prev(info);
59    }));
60}
61
62/// Returns a flag that becomes `true` on SIGTERM or SIGHUP.
63pub fn install_signal_flag() -> Arc<AtomicBool> {
64    let flag = Arc::new(AtomicBool::new(false));
65    let _ = signal_hook::flag::register(signal_hook::consts::SIGTERM, Arc::clone(&flag));
66    let _ = signal_hook::flag::register(signal_hook::consts::SIGHUP, Arc::clone(&flag));
67    flag
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::sync::atomic::Ordering;
74
75    #[test]
76    fn signal_flag_starts_false() {
77        let f = install_signal_flag();
78        assert!(!f.load(Ordering::SeqCst));
79    }
80
81    #[test]
82    fn restore_is_idempotent() {
83        // Should not panic when raw mode was never enabled.
84        restore_terminal_best_effort();
85        restore_terminal_best_effort();
86    }
87}