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 pub(crate) fn has_connected_stdio_stream(&self) -> bool {
129 self.same_as_stdin || self.same_as_stdout || self.same_as_stderr
130 }
131}
132
133impl Terminal {
134 fn from_stdio(file: TerminalFile) -> io::Result<Self> {
135 Ok(Terminal {
136 same_as_stdin: is_same_file(file.as_fd(), stdin().as_fd())?,
137 same_as_stdout: is_same_file(file.as_fd(), stdout().as_fd())?,
138 same_as_stderr: is_same_file(file.as_fd(), stderr().as_fd())?,
139 file,
140 })
141 }
142
143 fn from_controlling(file: TerminalFile) -> Self {
144 Terminal {
145 file,
146 same_as_stdin: false,
147 same_as_stdout: false,
148 same_as_stderr: false,
149 }
150 }
151}
152
153#[derive(Debug)]
154enum TerminalFile {
155 Owned(File),
156 Borrowed(ManuallyDrop<File>),
157}
158
159impl io::Write for Terminal {
160 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
161 self.file.write(buf)
162 }
163
164 fn flush(&mut self) -> io::Result<()> {
165 self.file.flush()
166 }
167}
168
169impl io::Read for Terminal {
170 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
171 self.file.read(buf)
172 }
173}
174
175impl Deref for TerminalFile {
176 type Target = File;
177
178 fn deref(&self) -> &Self::Target {
179 match self {
180 TerminalFile::Owned(f) => f,
181 TerminalFile::Borrowed(f) => f,
182 }
183 }
184}
185
186impl DerefMut for TerminalFile {
187 fn deref_mut(&mut self) -> &mut Self::Target {
188 match self {
189 TerminalFile::Owned(f) => f,
190 TerminalFile::Borrowed(f) => f,
191 }
192 }
193}
194
195impl AsFd for super::Terminal {
196 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
197 self.0.file.as_fd()
198 }
199}
200
201impl AsFd for super::TerminalLock<'_> {
202 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
203 self.inner.file.as_fd()
204 }
205}
206
207impl AsFd for super::RawModeGuard<'_> {
208 fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> {
209 self.0.inner.file.as_fd()
210 }
211}
212
213impl AsRawFd for super::Terminal {
214 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
215 self.0.file.as_raw_fd()
216 }
217}
218
219impl AsRawFd for super::TerminalLock<'_> {
220 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
221 self.inner.file.as_raw_fd()
222 }
223}
224
225impl AsRawFd for super::RawModeGuard<'_> {
226 fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
227 self.0.inner.file.as_raw_fd()
228 }
229}
230
231pub(crate) struct RawModeGuard<'a> {
232 inner: &'a mut Terminal,
233 old_termios: Option<termios>,
234}
235
236impl fmt::Debug for RawModeGuard<'_> {
237 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238 f.debug_struct("RawModeGuard")
239 .field("inner", &self.inner)
240 .finish_non_exhaustive()
241 }
242}
243
244impl io::Write for RawModeGuard<'_> {
245 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
246 self.inner.write(buf)
247 }
248
249 fn flush(&mut self) -> io::Result<()> {
250 self.inner.flush()
251 }
252}
253
254impl io::Read for RawModeGuard<'_> {
255 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
256 self.inner.read(buf)
257 }
258}
259
260impl Drop for RawModeGuard<'_> {
261 fn drop(&mut self) {
262 if let Some(old_termios) = self.old_termios {
263 _ = attr::set_terminal_attr(self.inner.file.as_fd(), &old_termios);
264 }
265 }
266}
267
268fn to_io_result(value: c_int) -> io::Result<c_int> {
269 if value == -1 {
270 Err(io::Error::last_os_error())
271 } else {
272 Ok(value)
273 }
274}
275
276#[cfg(not(target_os = "macos"))]
278fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
279 let mut buf = Vec::with_capacity(64);
280
281 loop {
282 let code = unsafe { libc::ttyname_r(fd.as_raw_fd(), buf.as_mut_ptr(), buf.capacity()) };
284 match code {
285 0 => return Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned()),
287 libc::ERANGE => buf.reserve(64),
288 code => return Err(io::Error::from_raw_os_error(code)),
289 }
290 }
291}
292
293#[cfg(target_os = "macos")]
295fn ttyname_r(fd: BorrowedFd) -> io::Result<CString> {
296 use libc::{F_GETPATH, PATH_MAX};
297
298 let buf: [i8; PATH_MAX as usize] = [0; PATH_MAX as usize];
300
301 unsafe {
302 match fcntl(fd.as_raw_fd(), F_GETPATH as c_int, &buf) {
303 0 => {
304 let res = CStr::from_ptr(buf.as_ptr()).to_owned();
305 Ok(res)
306 }
307 _ => Err(io::Error::last_os_error()),
308 }
309 }
310}