Skip to main content

git_worktree_manager/tui/
raw_mode.rs

1//! RAII guard for Unix terminal raw-mode + cursor state.
2//!
3//! Callers that manipulate termios directly risk leaving the terminal in raw
4//! mode with the cursor hidden if they panic mid-render. `RawModeGuard` owns
5//! both pieces of state so `Drop` restores them on every exit path, including
6//! panic-unwind.
7
8#![cfg(unix)]
9
10use std::io::Write;
11
12pub(crate) struct RawModeGuard {
13    fd: i32,
14    original_termios: libc::termios,
15    cursor_hidden: bool,
16}
17
18impl RawModeGuard {
19    /// Enter raw mode on `fd`. If `hide_cursor` is true, also emits the
20    /// hide-cursor escape sequence to stderr. Returns `None` if `tcgetattr`
21    /// or `tcsetattr` fails — the caller falls back to cooked-mode I/O.
22    pub(crate) fn enter(fd: i32, hide_cursor: bool) -> Option<Self> {
23        let mut original_termios: libc::termios = unsafe { std::mem::zeroed() };
24        if unsafe { libc::tcgetattr(fd, &mut original_termios) } != 0 {
25            return None;
26        }
27
28        let mut raw = original_termios;
29        unsafe { libc::cfmakeraw(&mut raw) };
30        if unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &raw) } != 0 {
31            return None;
32        }
33
34        // Hide cursor only after termios is in place, so a failed tcsetattr
35        // never leaves a hidden cursor with no guard to restore it.
36        if hide_cursor {
37            let stderr = std::io::stderr();
38            let mut handle = stderr.lock();
39            let _ = handle.write_all(b"\x1b[?25l");
40            let _ = handle.flush();
41        }
42
43        Some(Self {
44            fd,
45            original_termios,
46            cursor_hidden: hide_cursor,
47        })
48    }
49}
50
51impl Drop for RawModeGuard {
52    fn drop(&mut self) {
53        if self.cursor_hidden {
54            let stderr = std::io::stderr();
55            let mut handle = stderr.lock();
56            let _ = handle.write_all(b"\x1b[?25h");
57            let _ = handle.flush();
58        }
59        // Errors are ignored: the process is already in trouble (likely mid-panic).
60        unsafe { libc::tcsetattr(self.fd, libc::TCSADRAIN, &self.original_termios) };
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn enter_returns_none_on_bad_fd() {
70        // fd -1 is never a valid tty; tcgetattr must fail, enter must yield None.
71        assert!(RawModeGuard::enter(-1, false).is_none());
72        assert!(RawModeGuard::enter(-1, true).is_none());
73    }
74}