Skip to main content

indigo_term/
tty.rs

1#![expect(unsafe_code)]
2
3use std::{
4    fs::{self, File},
5    io::{self, BufWriter, Read, Write},
6    mem::{self, MaybeUninit},
7    os::fd::{AsRawFd as _, RawFd},
8};
9
10pub struct Tty {
11    reader: File,
12    writer: BufWriter<File>,
13    original_termios: Option<libc::termios>,
14}
15
16impl Tty {
17    pub fn init() -> io::Result<Self> {
18        let file = fs::OpenOptions::new()
19            .read(true)
20            .write(true)
21            .open("/dev/tty")?;
22
23        let writer = BufWriter::new(file.try_clone()?);
24
25        Ok(Self {
26            reader: file,
27            writer,
28            original_termios: None,
29        })
30    }
31
32    pub fn enable_raw_mode(&mut self) -> io::Result<()> {
33        if self.original_termios.is_some() {
34            return Ok(());
35        }
36
37        self.original_termios = Some(enable_raw_mode(self.reader.as_raw_fd())?);
38
39        Ok(())
40    }
41
42    pub fn disable_raw_mode(&mut self) -> io::Result<()> {
43        if let Some(original_termios) = mem::take(&mut self.original_termios) {
44            disable_raw_mode(self.reader.as_raw_fd(), &original_termios)?;
45        }
46
47        Ok(())
48    }
49
50    pub fn size(&self) -> io::Result<(u16, u16)> {
51        let mut winsize = MaybeUninit::<libc::winsize>::uninit();
52        // SAFETY: `winsize` points to valid, writable memory of the right size.
53        let ret = unsafe {
54            libc::ioctl(
55                self.reader.as_raw_fd(),
56                libc::TIOCGWINSZ,
57                winsize.as_mut_ptr(),
58            )
59        };
60        if ret != 0 {
61            return Err(io::Error::last_os_error());
62        }
63        // SAFETY: `ioctl` succeeded, so `winsize` is initialized.
64        let winsize = unsafe { winsize.assume_init() };
65        Ok((winsize.ws_row, winsize.ws_col))
66    }
67}
68
69impl Drop for Tty {
70    fn drop(&mut self) {
71        let _ = self.disable_raw_mode();
72    }
73}
74
75impl Read for Tty {
76    #[inline]
77    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
78        self.reader.read(buf)
79    }
80}
81
82impl Write for Tty {
83    #[inline]
84    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
85        self.writer.write(buf)
86    }
87
88    #[inline]
89    fn flush(&mut self) -> io::Result<()> {
90        self.writer.flush()
91    }
92}
93
94fn tcgetattr(fd: RawFd) -> io::Result<libc::termios> {
95    let mut termios = MaybeUninit::<libc::termios>::uninit();
96    // SAFETY: `termios` points to valid, writable memory of the right size.
97    let ret = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) };
98    if ret != 0 {
99        return Err(io::Error::last_os_error());
100    }
101    // SAFETY: `tcgetattr` succeeded, so `termios` is initialized.
102    Ok(unsafe { termios.assume_init() })
103}
104
105fn tcsetattr(fd: RawFd, termios: &libc::termios) -> io::Result<()> {
106    // SAFETY: `termios` is a valid, initialized `termios` value.
107    let ret = unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, termios) };
108    if ret != 0 {
109        return Err(io::Error::last_os_error());
110    }
111    Ok(())
112}
113
114fn enable_raw_mode(fd: RawFd) -> io::Result<libc::termios> {
115    let cooked_termios = tcgetattr(fd)?;
116    let mut raw_termios = cooked_termios;
117    // SAFETY: `raw_termios` is a valid, initialized `termios` value.
118    unsafe { libc::cfmakeraw(&raw mut raw_termios) };
119    tcsetattr(fd, &raw_termios)?;
120    Ok(cooked_termios)
121}
122
123fn disable_raw_mode(fd: RawFd, termios: &libc::termios) -> io::Result<()> {
124    tcsetattr(fd, termios)?;
125    Ok(())
126}