Skip to main content

supercode_frontend_tui/terminal/
lifecycle.rs

1// Derived from OpenAI Codex: codex-rs/tui/src/tui.rs
2// Pinned source: 8604689ec5e3437eb79802d8d72249b7722fbf5b
3// Copyright 2025 OpenAI
4// Licensed under the Apache License, Version 2.0.
5// Modified by the Supercode contributors; see docs/legal/codex-frontend-extraction.toml.
6
7//! Panic-safe, idempotent terminal mode ownership.
8
9use 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
24/// Backend seam used to prove exact mode setup and restoration without a TTY.
25pub 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/// Real Crossterm terminal operations.
34#[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
88/// Owns every terminal mode enabled by the frontend and restores them on drop.
89pub struct TerminalGuard<O: TerminalOps> {
90    ops: O,
91    active: ActiveModes,
92}
93
94impl<O: TerminalOps> TerminalGuard<O> {
95    /// Enable all terminal modes, rolling back any successfully enabled prefix
96    /// if a later operation fails.
97    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    /// Restore every active mode in reverse order. Restoration is best-effort:
128    /// all operations are attempted and the first error is returned.
129    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    /// Temporarily restore the terminal around an external interactive action,
172    /// then reacquire all modes even when that action returns an error.
173    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    /// True when this guard currently owns at least one terminal mode.
185    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            // Keep ownership recorded so `Drop` can retry a transient
201            // restoration failure instead of silently abandoning the mode.
202            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}