Skip to main content

imp_tui/
terminal.rs

1use std::io::{self, Write};
2
3use crossterm::cursor::Show;
4use crossterm::event::{
5    DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
6    KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
7};
8use crossterm::terminal::{
9    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
10};
11use ratatui::backend::CrosstermBackend;
12use ratatui::Terminal;
13
14pub type InteractiveTerminal = Terminal<CrosstermBackend<io::Stdout>>;
15
16pub fn set_window_title(title: &str) -> io::Result<()> {
17    let mut stdout = io::stdout();
18    write!(stdout, "\x1b]0;{title}\x07")?;
19    stdout.flush()?;
20    Ok(())
21}
22
23pub fn ring_terminal_bell() -> io::Result<()> {
24    #[cfg(test)]
25    {
26        return Ok(());
27    }
28
29    #[cfg(not(test))]
30    {
31        let mut stdout = io::stdout();
32        write!(stdout, "\x07")?;
33        stdout.flush()?;
34        Ok(())
35    }
36}
37
38fn restore_terminal<W: Write>(writer: &mut W) -> io::Result<()> {
39    let _ = disable_raw_mode();
40    write!(writer, "\x1b[0m")?;
41    #[cfg(unix)]
42    crossterm::execute!(
43        writer,
44        Show,
45        LeaveAlternateScreen,
46        DisableMouseCapture,
47        DisableBracketedPaste,
48        PopKeyboardEnhancementFlags
49    )?;
50    #[cfg(not(unix))]
51    crossterm::execute!(writer, Show, LeaveAlternateScreen, DisableMouseCapture)?;
52    writer.flush()?;
53    Ok(())
54}
55
56fn restore_terminal_if_needed<W: Write>(writer: &mut W, restored: &mut bool) -> io::Result<()> {
57    if *restored {
58        return Ok(());
59    }
60    *restored = true;
61    restore_terminal(writer)
62}
63
64pub struct TerminalSession {
65    terminal: InteractiveTerminal,
66    last_title: Option<String>,
67    restored: bool,
68}
69
70impl TerminalSession {
71    pub fn enter() -> io::Result<Self> {
72        enable_raw_mode()?;
73        let mut stdout = io::stdout();
74        #[cfg(unix)]
75        crossterm::execute!(
76            stdout,
77            EnterAlternateScreen,
78            EnableMouseCapture,
79            EnableBracketedPaste,
80            PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
81        )?;
82        #[cfg(not(unix))]
83        crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
84        let backend = CrosstermBackend::new(stdout);
85        let terminal = Terminal::new(backend)?;
86        Ok(Self {
87            terminal,
88            last_title: None,
89            restored: false,
90        })
91    }
92
93    pub fn terminal_mut(&mut self) -> &mut InteractiveTerminal {
94        &mut self.terminal
95    }
96
97    pub fn restore(&mut self) -> io::Result<()> {
98        restore_terminal_if_needed(self.terminal.backend_mut(), &mut self.restored)
99    }
100
101    pub fn set_window_title(&mut self, title: &str) -> io::Result<()> {
102        if self.last_title.as_deref() == Some(title) {
103            return Ok(());
104        }
105
106        let mut stdout = io::stdout();
107        write!(stdout, "\x1b]0;{title}\x07")?;
108        stdout.flush()?;
109        self.last_title = Some(title.to_string());
110        Ok(())
111    }
112}
113
114impl Drop for TerminalSession {
115    fn drop(&mut self) {
116        let _ = self.restore();
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn restore_terminal_writes_reset_and_exit_sequences() {
126        let mut output = Vec::new();
127        restore_terminal(&mut output).unwrap();
128        let text = String::from_utf8(output).unwrap();
129
130        assert!(text.contains("\u{1b}[0m"));
131        assert!(text.contains("\u{1b}[?25h"));
132        assert!(text.contains("\u{1b}[?1049l"));
133    }
134
135    #[test]
136    fn restore_terminal_if_needed_is_idempotent() {
137        let mut output = Vec::new();
138        let mut restored = false;
139
140        restore_terminal_if_needed(&mut output, &mut restored).unwrap();
141        let first = output.clone();
142        restore_terminal_if_needed(&mut output, &mut restored).unwrap();
143
144        assert!(restored);
145        assert_eq!(output, first);
146    }
147}