mermaid_cli/app/
terminal.rs1use std::io::{self, Stdout, Write};
12use std::sync::atomic::{AtomicBool, Ordering};
13
14use anyhow::{Context, Result};
15use crossterm::cursor::Show;
16use crossterm::event::{
17 DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
18 EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
19 PushKeyboardEnhancementFlags,
20};
21use crossterm::execute;
22use crossterm::terminal::{
23 EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
24 supports_keyboard_enhancement,
25};
26use ratatui::Terminal;
27use ratatui::backend::CrosstermBackend;
28
29static TERMINAL_NEEDS_RESTORE: AtomicBool = AtomicBool::new(false);
30
31static KEYBOARD_ENHANCED: AtomicBool = AtomicBool::new(false);
35
36pub struct TerminalGuard {
43 inner: Terminal<CrosstermBackend<Stdout>>,
44 restored: bool,
45}
46
47impl TerminalGuard {
48 pub fn setup() -> Result<Self> {
49 enable_raw_mode().context("failed to enable raw mode")?;
50 TERMINAL_NEEDS_RESTORE.store(true, Ordering::SeqCst);
51 let mut stdout = io::stdout();
52 if let Err(error) = execute!(
53 stdout,
54 EnterAlternateScreen,
55 EnableMouseCapture,
56 EnableBracketedPaste,
57 EnableFocusChange,
58 ) {
59 restore_terminal_once();
60 return Err(error).context(
61 "failed to enter alternate screen / enable mouse / enable bracketed paste",
62 );
63 }
64
65 if matches!(supports_keyboard_enhancement(), Ok(true))
73 && execute!(
74 io::stdout(),
75 PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)
76 )
77 .is_ok()
78 {
79 KEYBOARD_ENHANCED.store(true, Ordering::SeqCst);
80 }
81
82 let backend = CrosstermBackend::new(stdout);
83 let terminal = match Terminal::new(backend).context("failed to create terminal") {
84 Ok(terminal) => terminal,
85 Err(error) => {
86 restore_terminal_once();
87 return Err(error);
88 },
89 };
90
91 install_panic_hook();
92
93 Ok(Self {
94 inner: terminal,
95 restored: false,
96 })
97 }
98
99 pub fn inner_mut(&mut self) -> &mut Terminal<CrosstermBackend<Stdout>> {
101 &mut self.inner
102 }
103
104 pub fn restore_now(&mut self) {
107 if self.restored {
108 return;
109 }
110 restore_terminal_once();
111 let _ = self.inner.show_cursor();
112 self.restored = true;
113 }
114}
115
116impl Drop for TerminalGuard {
117 fn drop(&mut self) {
118 self.restore_now();
119 }
120}
121
122fn restore_terminal() {
129 let mut stdout = io::stdout();
130 if KEYBOARD_ENHANCED.swap(false, Ordering::SeqCst) {
135 let _ = execute!(stdout, PopKeyboardEnhancementFlags);
136 }
137 let _ = execute!(
138 stdout,
139 DisableMouseCapture,
140 DisableBracketedPaste,
141 DisableFocusChange,
142 LeaveAlternateScreen,
143 Show,
144 );
145 let _ = stdout.write_all(
149 b"\x1b[<u\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1004l\x1b[?1005l\x1b[?1006l\x1b[?1015l\x1b[?2004l\x1b[?1049l\x1b[?25h\x1b[0m",
150 );
151 let _ = stdout.flush();
152 let _ = disable_raw_mode();
153}
154
155fn restore_terminal_once() {
156 if !TERMINAL_NEEDS_RESTORE.swap(false, Ordering::SeqCst) {
157 return;
158 }
159 restore_terminal();
160}
161
162fn install_panic_hook() {
167 static HOOK_INSTALLED: AtomicBool = AtomicBool::new(false);
168 if HOOK_INSTALLED
169 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
170 .is_err()
171 {
172 return;
173 }
174
175 let original = std::panic::take_hook();
176 std::panic::set_hook(Box::new(move |info| {
177 restore_terminal_once();
178 original(info);
179 }));
180}