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 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 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 let ret = unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) };
98 if ret != 0 {
99 return Err(io::Error::last_os_error());
100 }
101 Ok(unsafe { termios.assume_init() })
103}
104
105fn tcsetattr(fd: RawFd, termios: &libc::termios) -> io::Result<()> {
106 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 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}