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