Skip to main content

rusty_bubbletea/
tty.rs

1//! Cleanroom Rust port of upstream Go source file: `tty.go`
2//! Upstream Target Tag / Version: `v2.0.8`
3//!
4//! <public-docs>
5//! # TTY Terminal Management
6//!
7//! TTY initialization, input stream setup, raw mode toggles, and window dimension queries for Bubble Tea v2.0.8.
8//! </public-docs>
9
10use crossterm::terminal::size as term_size;
11use std::sync::{Mutex, OnceLock};
12
13/// Saved terminal state for the raw mode toggle, mirroring the upstream
14/// `p.previousTtyInputState` (x/term `MakeRaw`/`Restore`).
15static SAVED_TERMIOS: OnceLock<Mutex<Option<libc::termios>>> = OnceLock::new();
16
17/// Initializes terminal raw mode, mirroring the upstream `initInput` ->
18/// `term.MakeRaw` path (`tty_unix.go`). Unlike a fully-zeroed `cfmakeraw`,
19/// only `OPOST` is cleared from the output flags, so `TABDLY` (and thus the
20/// hard-tab cursor optimization) behaves exactly as it does upstream.
21pub fn enable_raw_mode() -> std::io::Result<()> {
22    use std::os::fd::AsRawFd;
23    let fd = std::io::stdin().as_raw_fd();
24    let mut t: libc::termios = unsafe { std::mem::zeroed() };
25    if unsafe { libc::tcgetattr(fd, &mut t) } != 0 {
26        return Err(std::io::Error::last_os_error());
27    }
28    *SAVED_TERMIOS
29        .get_or_init(|| Mutex::new(None))
30        .lock()
31        .unwrap() = Some(t);
32
33    // This attempts to replicate the behaviour documented for cfmakeraw in
34    // the termios(3) manpage, as x/term's `makeRaw` does.
35    t.c_iflag &= !(libc::IGNBRK
36        | libc::BRKINT
37        | libc::PARMRK
38        | libc::ISTRIP
39        | libc::INLCR
40        | libc::IGNCR
41        | libc::ICRNL
42        | libc::IXON);
43    t.c_oflag &= !libc::OPOST;
44    t.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON | libc::ISIG | libc::IEXTEN);
45    t.c_cflag &= !(libc::CSIZE | libc::PARENB);
46    t.c_cflag |= libc::CS8;
47    t.c_cc[libc::VMIN] = 1;
48    t.c_cc[libc::VTIME] = 0;
49    if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &t) } != 0 {
50        return Err(std::io::Error::last_os_error());
51    }
52    Ok(())
53}
54
55/// Restores the terminal state saved by [`enable_raw_mode`], mirroring the
56/// upstream `term.Restore` path.
57pub fn disable_raw_mode() -> std::io::Result<()> {
58    use std::os::fd::AsRawFd;
59    let saved = SAVED_TERMIOS
60        .get_or_init(|| Mutex::new(None))
61        .lock()
62        .unwrap()
63        .take();
64    if let Some(t) = saved {
65        let fd = std::io::stdin().as_raw_fd();
66        if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &t) } != 0 {
67            return Err(std::io::Error::last_os_error());
68        }
69    }
70    Ok(())
71}
72
73/// Initializes terminal raw mode.
74pub fn init_terminal() -> Result<(), Box<dyn std::error::Error>> {
75    enable_raw_mode()?;
76    Ok(())
77}
78
79/// Restores terminal raw mode.
80pub fn restore_terminal() -> Result<(), Box<dyn std::error::Error>> {
81    disable_raw_mode()?;
82    Ok(())
83}
84
85/// Queries current window size columns and rows.
86pub fn get_window_size() -> Result<(u16, u16), Box<dyn std::error::Error>> {
87    let (w, h) = term_size()?;
88    Ok((w, h))
89}