1use crate::StdioLocks;
2use libc::{c_int, fcntl, termios, F_GETFL, O_RDWR};
3use std::ffi::{CStr, CString, OsStr};
4use std::fmt;
5use std::fs::{File, OpenOptions};
6use std::io::{self, stderr, stdin, stdout, IsTerminal};
7use std::mem::{self, ManuallyDrop};
8use std::ops::{Deref, DerefMut};
9use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd as _};
10use std::os::unix::ffi::OsStrExt;
11
12mod attr;
13#[cfg(test)]
14mod pty_utils;
15#[cfg(test)]
16mod tests;
17
18pub(crate) fn terminal() -> io::Result<Terminal> {
19 None.or_else(|| reuse_tty_from_stdio(stderr).transpose())
20 .or_else(|| reuse_tty_from_stdio(stdout).transpose())
21 .or_else(|| reuse_tty_from_stdio(stdin).transpose())
22 .map(|r| r.and_then(Terminal::from_stdio))
23 .unwrap_or_else(|| Ok(Terminal::from_controlling(open_controlling_tty()?)))
24}
25
26fn reuse_tty_from_stdio<S: IsTerminal + AsFd>(
27 stream: impl FnOnce() -> S,
28) -> io::Result<Option<TerminalFile>> {
29 let stream = stream();
30
31 if stream.is_terminal() {
32 if is_read_write(stream.as_fd())? {
37 let file = unsafe { File::from_raw_fd(stream.as_fd().as_raw_fd()) };
42 Ok(Some(TerminalFile::Borrowed(ManuallyDrop::new(file))))
43 } else {
44 reopen_tty(stream.as_fd())
45 .map(TerminalFile::Owned)
46 .map(Some)
47 }
48 } else {
49 Ok(None)
50 }
51}
52
53fn open_controlling_tty() -> io::Result<TerminalFile> {
54 OpenOptions::new()
55 .read(true)
56 .write(true)
57 .open("/dev/tty")
58 .map(TerminalFile::Owned)
59}
60
61fn is_read_write(fd: BorrowedFd) -> io::Result<bool> {
62 let mode = to_io_result(unsafe { fcntl(fd.as_raw_fd(), F_GETFL) })?;
64 Ok(mode & O_RDWR == O_RDWR)
65}
66
67fn reopen_tty(fd: BorrowedFd) -> io::Result<File> {
68 let name = ttyname_r(fd)?;
69 OpenOptions::new()
70 .read(true)
71 .write(true)
72 .open(OsStr::from_bytes(name.as_bytes()))
73}
74
75fn is_same_file(a: BorrowedFd, b: BorrowedFd) -> io::Result<bool> {
76 Ok(a.as_raw_fd() == b.as_raw_fd() || {
77 let stat_a = fstat(a)?;
78 let stat_b = fstat(b)?;
79 stat_a.st_dev == stat_b.st_dev && stat_a.st_ino == stat_b.st_ino
80 })
81}
82
83fn fstat(fd: BorrowedFd) -> io::Result<libc::stat> {
84 let mut stat = unsafe { mem::zeroed() };
86 to_io_result(unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) })?;
88 Ok(stat)
89}
90
91#[derive(Debug)]
92pub(crate) struct Terminal {
93 file: TerminalFile,
94 same_as_stdin: bool,
95 same_as_stdout: bool,
96 same_as_stderr: bool,
97}
98
99impl Terminal {
100 pub(crate) fn lock_stdio(&self) -> StdioLocks {
101 StdioLocks {
102 stdin_lock: self.same_as_stdin.then(|| stdin().lock()),
103 stdout_lock: self.same_as_stdout.then(|| stdout().lock()),
104 stderr_lock: self.same_as_stderr.then(|| stderr().lock()),
105 }
106 }
107
108 pub(crate) fn enable_raw_mode(&mut self) -> io::Result<RawModeGuard<'_>> {
109 let fd = self.file.as_fd();
110 let old_termios = attr::get_terminal_attr(fd)?;
111
112 if !attr::is_raw_mode_enabled(&old_termios) {
113 let mut termios = old_termios;
114 attr::enable_raw_mode(&mut termios);
115 attr::set_terminal_attr(fd, &termios)?;
116 Ok(RawModeGuard {
117 inner: self,
118 old_termios: Some(old_termios),
119 })
120 } else {
121 Ok(RawModeGuard {
122 inner: self,
123 old_termios: None,
124 })
125 }
126 }
127}
128
129impl Terminal {
130 fn from_stdio(file: TerminalFile) -> io::Result<Self> {
131 Ok(Terminal {
132 same_as_stdin: is_same_file(file.as_fd(), stdin().as_fd())?,
133 same_as_stdout: is_same_file(file.as_fd(), stdout().as_fd())?,
134 same_as_stderr: is_same_file(file.as_fd(), stderr().as_fd())?,
135 file,
136 })
137 }
138
139 fn from_controlling(file: TerminalFile) -> Self {
140 Terminal {
141 file,
142 same_as_stdin: false,
143 same_as_stdout: false,
144 same_as_stderr: false,
145 }
146 }
147}
148
149#[derive(Debug)]
150enum TerminalFile {
151 Owned(File),
152 Borrowed(ManuallyDrop<File>),
153}
154
155impl io::Write for Terminal {
156 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
157 self.file.write(buf)
158 }
159
160 fn flush(&mut self) -> io::Result<()> {
161 self.file.flush()
162 }
163}
164
165impl io::Read for Terminal {
166 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
167 self.file.read(buf)
168 }
169}
170
171impl Deref for TerminalFile {
172 type Target = File;
173
174 fn deref(&self) -> &Self::Target {
175 match self {
176 TerminalFile::Owned(f) => f,
177 TerminalFile::Borrowed(f) => f,
178 }
179 }
180}
181
182impl DerefMut for TerminalFile {
183 fn deref_mut(&mut self) -> &mut Self::Target {
184 match self {
185 TerminalFile::Owned(f) => f,
186 TerminalFile::Borrowed(f) => f,
187 }
188 }
189}
190
191impl AsFd for super::Terminal {
192 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
193 self.0.file.as_fd()
194 }
195}
196
197impl AsFd for super::TerminalLock<'_> {
198 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
199 self.inner.file.as_fd()
200 }
201}
202
203impl AsFd for super::RawModeGuard<'_> {
204 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
205 self.0.inner.file.as_fd()
206 }
207}
208
209impl AsRawFd for super::Terminal {
210 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
211 self.0.file.as_raw_fd()
212 }
213}
214
215impl AsRawFd for super::TerminalLock<'_> {
216 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
217 self.inner.file.as_raw_fd()
218 }
219}
220
221impl AsRawFd for super::RawModeGuard<'_> {
222 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
223 self.0.inner.file.as_raw_fd()
224 }
225}
226
227pub(crate) struct RawModeGuard<'a> {
228 inner: &'a mut Terminal,
229 old_termios: Option<termios>,
230}
231
232impl fmt::Debug for RawModeGuard<'_> {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 f.debug_struct("RawModeGuard")
235 .field("inner", &self.inner)
236 .finish_non_exhaustive()
237 }
238}
239
240impl io::Write for RawModeGuard<'_> {
241 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
242 self.inner.write(buf)
243 }
244
245 fn flush(&mut self) -> io::Result<()> {
246 self.inner.flush()
247 }
248}
249
250impl io::Read for RawModeGuard<'_> {
251 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
252 self.inner.read(buf)
253 }
254}
255
256impl Drop for RawModeGuard<'_> {
257 fn drop(&mut self) {
258 if let Some(old_termios) = self.old_termios {
259 _ = attr::set_terminal_attr(self.inner.file.as_fd(), &old_termios);
260 }
261 }
262}
263
264fn to_io_result(value: c_int) -> io::Result<c_int> {
265 if value == -1 {
266 Err(io::Error::last_os_error())
267 } else {
268 Ok(value)
269 }
270}
271
272#[cfg(not(target_os = "macos"))]
274fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
275 let mut buf = Vec::with_capacity(64);
276
277 loop {
278 let code = unsafe { libc::ttyname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.capacity()) };
280 match code {
281 0 => return Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned()),
283 libc::ERANGE => buf.reserve(64),
284 code => return Err(io::Error::from_raw_os_error(code)),
285 }
286 }
287}
288
289#[cfg(target_os = "macos")]
291fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
292 use libc::{F_GETPATH, PATH_MAX};
293
294 let buf: [i8; PATH_MAX as usize] = [0; PATH_MAX as usize];
296
297 unsafe {
298 match fcntl(fd.as_raw_fd(), F_GETPATH as c_int, &buf) {
299 0 => {
300 let res = CStr::from_ptr(buf.as_ptr()).to_owned();
301 Ok(res)
302 }
303 _ => Err(io::Error::last_os_error()),
304 }
305 }
306}