Skip to main content

tvscreener/tui/
terminal.rs

1// Copyright 2026 tvscreener-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Terminal raw-mode / alternate-screen lifecycle.
5
6use std::env;
7use std::io::{self, stdout, Write};
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use crossterm::cursor;
11use crossterm::event::{KeyCode, KeyModifiers};
12use crossterm::terminal::{
13    disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen,
14};
15use crossterm::ExecutableCommand;
16
17/// Set by SIGINT/SIGTERM so the loop can restore the tty before exit.
18pub static STOP: AtomicBool = AtomicBool::new(false);
19
20/// Restores the terminal on drop (normal exit, `?`, or unwind after panic hook).
21pub struct TerminalGuard {
22    use_alt: bool,
23}
24
25impl TerminalGuard {
26    /// Enables raw mode and optionally the alternate screen.
27    ///
28    /// # Errors
29    ///
30    /// Returns an I/O error when the tty cannot enter raw mode or alt screen.
31    pub fn enter(use_alt: bool) -> io::Result<Self> {
32        enable_raw_mode()?;
33        let mut out = stdout();
34        if use_alt {
35            out.execute(EnterAlternateScreen)?;
36        }
37        hard_reset_tty();
38        out.execute(cursor::Hide)?;
39        out.flush()?;
40        Ok(Self { use_alt })
41    }
42}
43
44impl Drop for TerminalGuard {
45    fn drop(&mut self) {
46        restore_terminal(self.use_alt);
47    }
48}
49
50/// GNU screen sets `STY`. Its default config often ignores the xterm alt buffer.
51#[must_use]
52pub fn inside_gnu_screen() -> bool {
53    env::var_os("STY").is_some()
54}
55
56/// Returns true for quit keys (`q`, Esc, Ctrl-C).
57#[must_use]
58pub const fn is_quit_key(code: KeyCode, modifiers: KeyModifiers) -> bool {
59    matches!(code, KeyCode::Char('q') | KeyCode::Esc)
60        || (matches!(code, KeyCode::Char('c')) && modifiers.contains(KeyModifiers::CONTROL))
61}
62
63/// Installs Ctrl-C / SIGTERM handler that sets [`STOP`].
64pub fn install_signal_handlers() {
65    let _ = ctrlc::set_handler(|| {
66        STOP.store(true, Ordering::SeqCst);
67    });
68}
69
70/// Restores the tty on panic before the default panic hook runs.
71pub fn install_panic_hook() {
72    let use_alt = !inside_gnu_screen();
73    let prior = std::panic::take_hook();
74    std::panic::set_hook(Box::new(move |info| {
75        restore_terminal(use_alt);
76        prior(info);
77    }));
78}
79
80/// Reset SGR, cursor, and clear both alt and main tty buffers.
81pub fn hard_reset_tty() {
82    let mut out = stdout();
83    let _ = write!(
84        out,
85        "\x1b[0m\x1b[?25h\x1b[?1049l\x1b[?47l\x1b[2J\x1b[3J\x1b[H"
86    );
87    let _ = out.execute(Clear(ClearType::All));
88    let _ = out.execute(Clear(ClearType::Purge));
89    let _ = out.execute(cursor::Show);
90    let _ = out.flush();
91}
92
93fn restore_terminal(use_alt: bool) {
94    let _ = disable_raw_mode();
95    let mut out = stdout();
96    if use_alt {
97        let _ = out.execute(LeaveAlternateScreen);
98    }
99    let _ = out.flush();
100    hard_reset_tty();
101}