xref: /third_party/rust/crates/is-terminal/src/lib.rs (revision d0b88b7e)
1//! is-terminal is a simple utility that answers one question:
2//!
3//! > Is this a terminal?
4//!
5//! A "terminal", also known as a "tty", is an I/O device which may be
6//! interactive and may support color and other special features. This crate
7//! doesn't provide any of those features; it just answers this one question.
8//!
9//! On Unix-family platforms, this is effectively the same as the [`isatty`]
10//! function for testing whether a given stream is a terminal, though it
11//! accepts high-level stream types instead of raw file descriptors.
12//!
13//! On Windows, it uses a variety of techniques to determine whether the
14//! given stream is a terminal.
15//!
16//! # Example
17//!
18//! ```rust
19//! use is_terminal::IsTerminal;
20//!
21//! if std::io::stdout().is_terminal() {
22//!     println!("stdout is a terminal")
23//! }
24//! ```
25//!
26//! [`isatty`]: https://man7.org/linux/man-pages/man3/isatty.3.html
27
28#![cfg_attr(unix, no_std)]
29
30#[cfg(not(target_os = "unknown"))]
31use io_lifetimes::AsFilelike;
32#[cfg(windows)]
33use io_lifetimes::BorrowedHandle;
34#[cfg(windows)]
35use windows_sys::Win32::Foundation::HANDLE;
36#[cfg(windows)]
37use windows_sys::Win32::System::Console::STD_HANDLE;
38
39pub trait IsTerminal {
40    /// Returns true if this is a terminal.
41    ///
42    /// # Example
43    ///
44    /// ```
45    /// use is_terminal::IsTerminal;
46    ///
47    /// if std::io::stdout().is_terminal() {
48    ///     println!("stdout is a terminal")
49    /// }
50    /// ```
51    fn is_terminal(&self) -> bool;
52}
53
54#[cfg(not(target_os = "unknown"))]
55impl<Stream: AsFilelike> IsTerminal for Stream {
56    #[inline]
57    fn is_terminal(&self) -> bool {
58        #[cfg(any(unix, target_os = "wasi"))]
59        {
60            rustix::termios::isatty(self)
61        }
62
63        #[cfg(target_os = "hermit")]
64        {
65            hermit_abi::isatty(self.as_filelike().as_fd())
66        }
67
68        #[cfg(windows)]
69        {
70            _is_terminal(self.as_filelike())
71        }
72    }
73}
74
75// The Windows implementation here is copied from atty, with #51 and #54
76// applied. The only significant modification is to take a `BorrowedHandle`
77// argument instead of using a `Stream` enum.
78
79#[cfg(windows)]
80fn _is_terminal(stream: BorrowedHandle<'_>) -> bool {
81    use std::os::windows::io::AsRawHandle;
82    use windows_sys::Win32::System::Console::GetStdHandle;
83    use windows_sys::Win32::System::Console::{
84        STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT,
85        STD_OUTPUT_HANDLE as STD_OUTPUT,
86    };
87
88    let (fd, others) = unsafe {
89        if stream.as_raw_handle() == GetStdHandle(STD_INPUT) as _ {
90            (STD_INPUT, [STD_ERROR, STD_OUTPUT])
91        } else if stream.as_raw_handle() == GetStdHandle(STD_OUTPUT) as _ {
92            (STD_OUTPUT, [STD_INPUT, STD_ERROR])
93        } else if stream.as_raw_handle() == GetStdHandle(STD_ERROR) as _ {
94            (STD_ERROR, [STD_INPUT, STD_OUTPUT])
95        } else {
96            return false;
97        }
98    };
99    if unsafe { console_on_any(&[fd]) } {
100        // False positives aren't possible. If we got a console then
101        // we definitely have a tty on stdin.
102        return true;
103    }
104
105    // At this point, we *could* have a false negative. We can determine that
106    // this is true negative if we can detect the presence of a console on
107    // any of the other streams. If another stream has a console, then we know
108    // we're in a Windows console and can therefore trust the negative.
109    if unsafe { console_on_any(&others) } {
110        return false;
111    }
112
113    // Otherwise, we fall back to a very strange msys hack to see if we can
114    // sneakily detect the presence of a tty.
115    // Safety: function has no invariants. an invalid handle id will cause
116    // GetFileInformationByHandleEx to return an error.
117    let handle = unsafe { GetStdHandle(fd) };
118    unsafe { msys_tty_on(handle) }
119}
120
121/// Returns true if any of the given fds are on a console.
122#[cfg(windows)]
123unsafe fn console_on_any(fds: &[STD_HANDLE]) -> bool {
124    use windows_sys::Win32::System::Console::{GetConsoleMode, GetStdHandle};
125
126    for &fd in fds {
127        let mut out = 0;
128        let handle = GetStdHandle(fd);
129        if GetConsoleMode(handle, &mut out) != 0 {
130            return true;
131        }
132    }
133    false
134}
135
136/// Returns true if there is an MSYS tty on the given handle.
137#[cfg(windows)]
138unsafe fn msys_tty_on(handle: HANDLE) -> bool {
139    use std::ffi::c_void;
140    use windows_sys::Win32::{
141        Foundation::MAX_PATH,
142        Storage::FileSystem::{FileNameInfo, GetFileInformationByHandleEx},
143    };
144
145    /// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving
146    /// it a fixed length that we can stack allocate
147    #[repr(C)]
148    #[allow(non_snake_case)]
149    struct FILE_NAME_INFO {
150        FileNameLength: u32,
151        FileName: [u16; MAX_PATH as usize],
152    }
153    let mut name_info = FILE_NAME_INFO {
154        FileNameLength: 0,
155        FileName: [0; MAX_PATH as usize],
156    };
157    // Safety: buffer length is fixed.
158    let res = GetFileInformationByHandleEx(
159        handle,
160        FileNameInfo,
161        &mut name_info as *mut _ as *mut c_void,
162        std::mem::size_of::<FILE_NAME_INFO>() as u32,
163    );
164    if res == 0 {
165        return false;
166    }
167
168    let s = &name_info.FileName[..name_info.FileNameLength as usize / 2];
169    let name = String::from_utf16_lossy(s);
170    // This checks whether 'pty' exists in the file name, which indicates that
171    // a pseudo-terminal is attached. To mitigate against false positives
172    // (e.g., an actual file name that contains 'pty'), we also require that
173    // either the strings 'msys-' or 'cygwin-' are in the file name as well.)
174    let is_msys = name.contains("msys-") || name.contains("cygwin-");
175    let is_pty = name.contains("-pty");
176    is_msys && is_pty
177}
178
179#[cfg(target_os = "unknown")]
180impl IsTerminal for std::io::Stdin {
181    #[inline]
182    fn is_terminal(&self) -> bool {
183        false
184    }
185}
186
187#[cfg(target_os = "unknown")]
188impl IsTerminal for std::io::Stdout {
189    #[inline]
190    fn is_terminal(&self) -> bool {
191        false
192    }
193}
194
195#[cfg(target_os = "unknown")]
196impl IsTerminal for std::io::Stderr {
197    #[inline]
198    fn is_terminal(&self) -> bool {
199        false
200    }
201}
202
203#[cfg(target_os = "unknown")]
204impl<'a> IsTerminal for std::io::StdinLock<'a> {
205    #[inline]
206    fn is_terminal(&self) -> bool {
207        false
208    }
209}
210
211#[cfg(target_os = "unknown")]
212impl<'a> IsTerminal for std::io::StdoutLock<'a> {
213    #[inline]
214    fn is_terminal(&self) -> bool {
215        false
216    }
217}
218
219#[cfg(target_os = "unknown")]
220impl<'a> IsTerminal for std::io::StderrLock<'a> {
221    #[inline]
222    fn is_terminal(&self) -> bool {
223        false
224    }
225}
226
227#[cfg(target_os = "unknown")]
228impl<'a> IsTerminal for std::fs::File {
229    #[inline]
230    fn is_terminal(&self) -> bool {
231        false
232    }
233}
234
235#[cfg(target_os = "unknown")]
236impl IsTerminal for std::process::ChildStdin {
237    #[inline]
238    fn is_terminal(&self) -> bool {
239        false
240    }
241}
242
243#[cfg(target_os = "unknown")]
244impl IsTerminal for std::process::ChildStdout {
245    #[inline]
246    fn is_terminal(&self) -> bool {
247        false
248    }
249}
250
251#[cfg(target_os = "unknown")]
252impl IsTerminal for std::process::ChildStderr {
253    #[inline]
254    fn is_terminal(&self) -> bool {
255        false
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    #[cfg(not(target_os = "unknown"))]
262    use super::IsTerminal;
263
264    #[test]
265    #[cfg(windows)]
266    fn stdin() {
267        assert_eq!(
268            atty::is(atty::Stream::Stdin),
269            std::io::stdin().is_terminal()
270        )
271    }
272
273    #[test]
274    #[cfg(windows)]
275    fn stdout() {
276        assert_eq!(
277            atty::is(atty::Stream::Stdout),
278            std::io::stdout().is_terminal()
279        )
280    }
281
282    #[test]
283    #[cfg(windows)]
284    fn stderr() {
285        assert_eq!(
286            atty::is(atty::Stream::Stderr),
287            std::io::stderr().is_terminal()
288        )
289    }
290
291    #[test]
292    #[cfg(any(unix, target_os = "wasi"))]
293    fn stdin() {
294        unsafe {
295            assert_eq!(
296                atty::is(atty::Stream::Stdin),
297                rustix::io::stdin().is_terminal()
298            )
299        }
300    }
301
302    #[test]
303    #[cfg(any(unix, target_os = "wasi"))]
304    fn stdout() {
305        unsafe {
306            assert_eq!(
307                atty::is(atty::Stream::Stdout),
308                rustix::io::stdout().is_terminal()
309            )
310        }
311    }
312
313    #[test]
314    #[cfg(any(unix, target_os = "wasi"))]
315    fn stderr() {
316        unsafe {
317            assert_eq!(
318                atty::is(atty::Stream::Stderr),
319                rustix::io::stderr().is_terminal()
320            )
321        }
322    }
323
324    #[test]
325    #[cfg(any(unix, target_os = "wasi"))]
326    fn stdin_vs_libc() {
327        unsafe {
328            assert_eq!(
329                libc::isatty(libc::STDIN_FILENO) != 0,
330                rustix::io::stdin().is_terminal()
331            )
332        }
333    }
334
335    #[test]
336    #[cfg(any(unix, target_os = "wasi"))]
337    fn stdout_vs_libc() {
338        unsafe {
339            assert_eq!(
340                libc::isatty(libc::STDOUT_FILENO) != 0,
341                rustix::io::stdout().is_terminal()
342            )
343        }
344    }
345
346    #[test]
347    #[cfg(any(unix, target_os = "wasi"))]
348    fn stderr_vs_libc() {
349        unsafe {
350            assert_eq!(
351                libc::isatty(libc::STDERR_FILENO) != 0,
352                rustix::io::stderr().is_terminal()
353            )
354        }
355    }
356
357    // Verify that the msys_tty_on function works with long path.
358    #[test]
359    #[cfg(windows)]
360    fn msys_tty_on_path_length() {
361        use std::{fs::File, os::windows::io::AsRawHandle};
362        use windows_sys::Win32::Foundation::MAX_PATH;
363
364        let dir = tempfile::tempdir().expect("Unable to create temporary directory");
365        let file_path = dir.path().join("ten_chars_".repeat(25));
366        // Ensure that the path is longer than MAX_PATH.
367        assert!(file_path.to_string_lossy().len() > MAX_PATH as usize);
368        let file = File::create(file_path).expect("Unable to create file");
369
370        assert!(!unsafe { crate::msys_tty_on(file.as_raw_handle() as isize) });
371    }
372}
373