supercode_frontend_tui/terminal/
lifecycle.rs1use std::io;
10use std::io::stdout;
11
12use crossterm::cursor::Hide;
13use crossterm::cursor::Show;
14use crossterm::event::DisableBracketedPaste;
15use crossterm::event::DisableFocusChange;
16use crossterm::event::EnableBracketedPaste;
17use crossterm::event::EnableFocusChange;
18use crossterm::execute;
19use crossterm::terminal::disable_raw_mode;
20use crossterm::terminal::enable_raw_mode;
21use crossterm::terminal::EnterAlternateScreen;
22use crossterm::terminal::LeaveAlternateScreen;
23
24pub trait TerminalOps {
26 fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()>;
27 fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()>;
28 fn set_focus_events(&mut self, enabled: bool) -> io::Result<()>;
29 fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()>;
30 fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()>;
31}
32
33#[derive(Debug, Default)]
35pub struct CrosstermTerminalOps;
36
37impl TerminalOps for CrosstermTerminalOps {
38 fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()> {
39 if enabled {
40 enable_raw_mode()
41 } else {
42 disable_raw_mode()
43 }
44 }
45
46 fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
47 if enabled {
48 execute!(stdout(), EnableBracketedPaste)
49 } else {
50 execute!(stdout(), DisableBracketedPaste)
51 }
52 }
53
54 fn set_focus_events(&mut self, enabled: bool) -> io::Result<()> {
55 if enabled {
56 execute!(stdout(), EnableFocusChange)
57 } else {
58 execute!(stdout(), DisableFocusChange)
59 }
60 }
61
62 fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()> {
63 if enabled {
64 execute!(stdout(), EnterAlternateScreen)
65 } else {
66 execute!(stdout(), LeaveAlternateScreen)
67 }
68 }
69
70 fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
71 if visible {
72 execute!(stdout(), Show)
73 } else {
74 execute!(stdout(), Hide)
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
80struct ActiveModes {
81 raw: bool,
82 bracketed_paste: bool,
83 focus_events: bool,
84 alternate_screen: bool,
85 cursor_hidden: bool,
86}
87
88pub struct TerminalGuard<O: TerminalOps> {
90 ops: O,
91 active: ActiveModes,
92}
93
94impl<O: TerminalOps> TerminalGuard<O> {
95 pub fn enter(ops: O) -> io::Result<Self> {
98 let mut guard = Self {
99 ops,
100 active: ActiveModes::default(),
101 };
102 if let Err(error) = guard.activate() {
103 let _ = guard.restore();
104 return Err(error);
105 }
106 Ok(guard)
107 }
108
109 fn activate(&mut self) -> io::Result<()> {
110 self.ops.set_raw_mode(true)?;
111 self.active.raw = true;
112
113 self.ops.set_bracketed_paste(true)?;
114 self.active.bracketed_paste = true;
115
116 self.ops.set_focus_events(true)?;
117 self.active.focus_events = true;
118
119 self.ops.set_alternate_screen(true)?;
120 self.active.alternate_screen = true;
121
122 self.ops.set_cursor_visible(false)?;
123 self.active.cursor_hidden = true;
124 Ok(())
125 }
126
127 pub fn restore(&mut self) -> io::Result<()> {
130 let mut first_error = None;
131
132 if self.active.cursor_hidden {
133 record_result(
134 &mut first_error,
135 self.ops.set_cursor_visible(true),
136 &mut self.active.cursor_hidden,
137 );
138 }
139 if self.active.alternate_screen {
140 record_result(
141 &mut first_error,
142 self.ops.set_alternate_screen(false),
143 &mut self.active.alternate_screen,
144 );
145 }
146 if self.active.focus_events {
147 record_result(
148 &mut first_error,
149 self.ops.set_focus_events(false),
150 &mut self.active.focus_events,
151 );
152 }
153 if self.active.bracketed_paste {
154 record_result(
155 &mut first_error,
156 self.ops.set_bracketed_paste(false),
157 &mut self.active.bracketed_paste,
158 );
159 }
160 if self.active.raw {
161 record_result(
162 &mut first_error,
163 self.ops.set_raw_mode(false),
164 &mut self.active.raw,
165 );
166 }
167
168 first_error.map_or(Ok(()), Err)
169 }
170
171 pub fn with_restored<T>(&mut self, action: impl FnOnce() -> io::Result<T>) -> io::Result<T> {
174 self.restore()?;
175 let action_result = action();
176 let activate_result = self.activate();
177 match (action_result, activate_result) {
178 (Err(error), _) => Err(error),
179 (Ok(_), Err(error)) => Err(error),
180 (Ok(value), Ok(())) => Ok(value),
181 }
182 }
183
184 pub fn is_active(&self) -> bool {
186 self.active != ActiveModes::default()
187 }
188}
189
190impl<O: TerminalOps> Drop for TerminalGuard<O> {
191 fn drop(&mut self) {
192 let _ = self.restore();
193 }
194}
195
196fn record_result(first_error: &mut Option<io::Error>, result: io::Result<()>, active: &mut bool) {
197 match result {
198 Ok(()) => *active = false,
199 Err(error) => {
200 first_error.get_or_insert(error);
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use std::cell::RefCell;
210 use std::panic::AssertUnwindSafe;
211 use std::rc::Rc;
212
213 use super::*;
214
215 #[derive(Clone, Default)]
216 struct MockOps {
217 calls: Rc<RefCell<Vec<&'static str>>>,
218 fail_on: Rc<RefCell<Option<&'static str>>>,
219 }
220
221 impl MockOps {
222 fn call(&self, name: &'static str) -> io::Result<()> {
223 self.calls.borrow_mut().push(name);
224 if self.fail_on.borrow().as_ref() == Some(&name) {
225 Err(io::Error::other(format!("failed {name}")))
226 } else {
227 Ok(())
228 }
229 }
230 }
231
232 impl TerminalOps for MockOps {
233 fn set_raw_mode(&mut self, enabled: bool) -> io::Result<()> {
234 self.call(if enabled { "raw+" } else { "raw-" })
235 }
236
237 fn set_bracketed_paste(&mut self, enabled: bool) -> io::Result<()> {
238 self.call(if enabled { "paste+" } else { "paste-" })
239 }
240
241 fn set_focus_events(&mut self, enabled: bool) -> io::Result<()> {
242 self.call(if enabled { "focus+" } else { "focus-" })
243 }
244
245 fn set_alternate_screen(&mut self, enabled: bool) -> io::Result<()> {
246 self.call(if enabled { "screen+" } else { "screen-" })
247 }
248
249 fn set_cursor_visible(&mut self, visible: bool) -> io::Result<()> {
250 self.call(if visible { "cursor+" } else { "cursor-" })
251 }
252 }
253
254 const ENTER: &[&str] = &["raw+", "paste+", "focus+", "screen+", "cursor-"];
255 const RESTORE: &[&str] = &["cursor+", "screen-", "focus-", "paste-", "raw-"];
256
257 #[test]
258 fn normal_drop_restores_every_mode_in_reverse_order_once() {
259 let ops = MockOps::default();
260 let calls = Rc::clone(&ops.calls);
261 {
262 let guard = TerminalGuard::enter(ops).expect("enter");
263 assert!(guard.is_active());
264 }
265 assert_eq!(&*calls.borrow(), &[ENTER, RESTORE].concat());
266 }
267
268 #[test]
269 fn partial_setup_failure_rolls_back_the_successful_prefix() {
270 let ops = MockOps::default();
271 *ops.fail_on.borrow_mut() = Some("focus+");
272 let calls = Rc::clone(&ops.calls);
273 assert!(TerminalGuard::enter(ops).is_err());
274 assert_eq!(
275 &*calls.borrow(),
276 &["raw+", "paste+", "focus+", "paste-", "raw-"]
277 );
278 }
279
280 #[test]
281 fn panic_unwind_restores_every_mode() {
282 let ops = MockOps::default();
283 let calls = Rc::clone(&ops.calls);
284 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
285 let _guard = TerminalGuard::enter(ops).expect("enter");
286 panic!("test panic");
287 }));
288 assert!(result.is_err());
289 assert_eq!(&*calls.borrow(), &[ENTER, RESTORE].concat());
290 }
291
292 #[test]
293 fn external_action_restores_then_reacquires_modes() {
294 let ops = MockOps::default();
295 let calls = Rc::clone(&ops.calls);
296 let mut guard = TerminalGuard::enter(ops).expect("enter");
297 guard.with_restored(|| Ok(())).expect("resume");
298 drop(guard);
299 assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, ENTER, RESTORE].concat());
300 }
301
302 #[test]
303 fn external_action_error_still_reacquires_and_later_restores() {
304 let ops = MockOps::default();
305 let calls = Rc::clone(&ops.calls);
306 let mut guard = TerminalGuard::enter(ops).expect("enter");
307 let result = guard.with_restored(|| Err::<(), _>(io::Error::other("action failed")));
308 assert_eq!(
309 result.expect_err("action error").to_string(),
310 "action failed"
311 );
312 assert!(guard.is_active());
313 drop(guard);
314 assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, ENTER, RESTORE].concat());
315 }
316
317 #[test]
318 fn restore_attempts_all_modes_after_an_individual_failure() {
319 let ops = MockOps::default();
320 let calls = Rc::clone(&ops.calls);
321 let fail_on = Rc::clone(&ops.fail_on);
322 let mut guard = TerminalGuard::enter(ops).expect("enter");
323 *fail_on.borrow_mut() = Some("screen-");
324 assert_eq!(
325 guard.restore().expect_err("restore error").to_string(),
326 "failed screen-"
327 );
328 assert!(guard.is_active());
329 *fail_on.borrow_mut() = None;
330 drop(guard);
331 assert_eq!(&*calls.borrow(), &[ENTER, RESTORE, &["screen-"]].concat());
332 }
333}