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}