rustix_is_terminal/
lib.rs

1//! rustix-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 rustix_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(any(windows, target_os = "hermit", target_os = "unknown")))]
31use rustix::fd::AsFd;
32#[cfg(target_os = "hermit")]
33use std::os::hermit::io::AsFd;
34#[cfg(windows)]
35use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
36#[cfg(windows)]
37use windows_sys::Win32::Foundation::HANDLE;
38
39/// Extension trait to check whether something is a terminal.
40pub trait IsTerminal {
41    /// Returns true if this is a terminal.
42    ///
43    /// # Example
44    ///
45    /// ```
46    /// use rustix_is_terminal::IsTerminal;
47    ///
48    /// if std::io::stdout().is_terminal() {
49    ///     println!("stdout is a terminal")
50    /// }
51    /// ```
52    fn is_terminal(&self) -> bool;
53}
54
55/// Returns `true` if `this` is a terminal.
56///
57/// This is equivalent to calling `this.is_terminal()` and exists only as a
58/// convenience to calling the trait method [`IsTerminal::is_terminal`]
59/// without importing the trait.
60///
61/// # Example
62///
63/// ```
64/// if rustix_is_terminal::is_terminal(&std::io::stdout()) {
65///     println!("stdout is a terminal")
66/// }
67/// ```
68pub fn is_terminal<T: IsTerminal>(this: T) -> bool {
69    this.is_terminal()
70}
71
72#[cfg(not(any(windows, target_os = "unknown")))]
73impl<Stream: AsFd> IsTerminal for Stream {
74    #[inline]
75    fn is_terminal(&self) -> bool {
76        #[cfg(any(unix, target_os = "wasi"))]
77        {
78            rustix::termios::isatty(self)
79        }
80
81        #[cfg(target_os = "hermit")]
82        {
83            use std::os::hermit::io::AsRawFd;
84            hermit_abi::isatty(self.as_fd().as_fd().as_raw_fd())
85        }
86    }
87}
88
89#[cfg(windows)]
90impl<Stream: AsHandle> IsTerminal for Stream {
91    #[inline]
92    fn is_terminal(&self) -> bool {
93        handle_is_console(self.as_handle())
94    }
95}
96
97// The Windows implementation here is copied from `handle_is_console` in
98// library/std/src/sys/pal/windows/io.rs in Rust at revision
99// e74c667a53c6368579867a74494e6fb7a7f17d13.
100
101#[cfg(windows)]
102fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
103    use windows_sys::Win32::System::Console::GetConsoleMode;
104
105    let handle = handle.as_raw_handle();
106
107    // A null handle means the process has no console.
108    if handle.is_null() {
109        return false;
110    }
111
112    unsafe {
113        let mut out = 0;
114        if GetConsoleMode(handle, &mut out) != 0 {
115            // False positives aren't possible. If we got a console then we definitely have a console.
116            return true;
117        }
118
119        // Otherwise, we fall back to an msys hack to see if we can detect the presence of a pty.
120        msys_tty_on(handle)
121    }
122}
123
124/// Returns true if there is an MSYS tty on the given handle.
125#[cfg(windows)]
126unsafe fn msys_tty_on(handle: HANDLE) -> bool {
127    use std::ffi::c_void;
128    use windows_sys::Win32::{
129        Foundation::MAX_PATH,
130        Storage::FileSystem::{
131            FileNameInfo, GetFileInformationByHandleEx, GetFileType, FILE_TYPE_PIPE,
132        },
133    };
134
135    // Early return if the handle is not a pipe.
136    if GetFileType(handle) != FILE_TYPE_PIPE {
137        return false;
138    }
139
140    /// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving
141    /// it a fixed length that we can stack allocate
142    #[repr(C)]
143    #[allow(non_snake_case)]
144    struct FILE_NAME_INFO {
145        FileNameLength: u32,
146        FileName: [u16; MAX_PATH as usize],
147    }
148    let mut name_info = FILE_NAME_INFO {
149        FileNameLength: 0,
150        FileName: [0; MAX_PATH as usize],
151    };
152    // Safety: buffer length is fixed.
153    let res = GetFileInformationByHandleEx(
154        handle,
155        FileNameInfo,
156        &mut name_info as *mut _ as *mut c_void,
157        std::mem::size_of::<FILE_NAME_INFO>() as u32,
158    );
159    if res == 0 {
160        return false;
161    }
162
163    // Use `get` because `FileNameLength` can be out of range.
164    let s = match name_info
165        .FileName
166        .get(..name_info.FileNameLength as usize / 2)
167    {
168        None => return false,
169        Some(s) => s,
170    };
171    let name = String::from_utf16_lossy(s);
172    // Get the file name only.
173    let name = name.rsplit('\\').next().unwrap_or(&name);
174    // This checks whether 'pty' exists in the file name, which indicates that
175    // a pseudo-terminal is attached. To mitigate against false positives
176    // (e.g., an actual file name that contains 'pty'), we also require that
177    // the file name begins with either the strings 'msys-' or 'cygwin-'.)
178    let is_msys = name.starts_with("msys-") || name.starts_with("cygwin-");
179    let is_pty = name.contains("-pty");
180    is_msys && is_pty
181}
182
183#[cfg(target_os = "unknown")]
184impl IsTerminal for std::io::Stdin {
185    #[inline]
186    fn is_terminal(&self) -> bool {
187        false
188    }
189}
190
191#[cfg(target_os = "unknown")]
192impl IsTerminal for std::io::Stdout {
193    #[inline]
194    fn is_terminal(&self) -> bool {
195        false
196    }
197}
198
199#[cfg(target_os = "unknown")]
200impl IsTerminal for std::io::Stderr {
201    #[inline]
202    fn is_terminal(&self) -> bool {
203        false
204    }
205}
206
207#[cfg(target_os = "unknown")]
208impl<'a> IsTerminal for std::io::StdinLock<'a> {
209    #[inline]
210    fn is_terminal(&self) -> bool {
211        false
212    }
213}
214
215#[cfg(target_os = "unknown")]
216impl<'a> IsTerminal for std::io::StdoutLock<'a> {
217    #[inline]
218    fn is_terminal(&self) -> bool {
219        false
220    }
221}
222
223#[cfg(target_os = "unknown")]
224impl<'a> IsTerminal for std::io::StderrLock<'a> {
225    #[inline]
226    fn is_terminal(&self) -> bool {
227        false
228    }
229}
230
231#[cfg(target_os = "unknown")]
232impl<'a> IsTerminal for std::fs::File {
233    #[inline]
234    fn is_terminal(&self) -> bool {
235        false
236    }
237}
238
239#[cfg(target_os = "unknown")]
240impl IsTerminal for std::process::ChildStdin {
241    #[inline]
242    fn is_terminal(&self) -> bool {
243        false
244    }
245}
246
247#[cfg(target_os = "unknown")]
248impl IsTerminal for std::process::ChildStdout {
249    #[inline]
250    fn is_terminal(&self) -> bool {
251        false
252    }
253}
254
255#[cfg(target_os = "unknown")]
256impl IsTerminal for std::process::ChildStderr {
257    #[inline]
258    fn is_terminal(&self) -> bool {
259        false
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    #[cfg(not(target_os = "unknown"))]
266    use super::IsTerminal;
267
268    #[test]
269    #[cfg(windows)]
270    fn stdin() {
271        assert_eq!(
272            atty::is(atty::Stream::Stdin),
273            std::io::stdin().is_terminal()
274        )
275    }
276
277    #[test]
278    #[cfg(windows)]
279    fn stdout() {
280        assert_eq!(
281            atty::is(atty::Stream::Stdout),
282            std::io::stdout().is_terminal()
283        )
284    }
285
286    #[test]
287    #[cfg(windows)]
288    fn stderr() {
289        assert_eq!(
290            atty::is(atty::Stream::Stderr),
291            std::io::stderr().is_terminal()
292        )
293    }
294
295    #[test]
296    #[cfg(any(unix, target_os = "wasi"))]
297    fn stdin() {
298        assert_eq!(
299            atty::is(atty::Stream::Stdin),
300            rustix::stdio::stdin().is_terminal()
301        )
302    }
303
304    #[test]
305    #[cfg(any(unix, target_os = "wasi"))]
306    fn stdout() {
307        assert_eq!(
308            atty::is(atty::Stream::Stdout),
309            rustix::stdio::stdout().is_terminal()
310        )
311    }
312
313    #[test]
314    #[cfg(any(unix, target_os = "wasi"))]
315    fn stderr() {
316        assert_eq!(
317            atty::is(atty::Stream::Stderr),
318            rustix::stdio::stderr().is_terminal()
319        )
320    }
321
322    #[test]
323    #[cfg(any(unix, target_os = "wasi"))]
324    fn stdin_vs_libc() {
325        unsafe {
326            assert_eq!(
327                libc::isatty(libc::STDIN_FILENO) != 0,
328                rustix::stdio::stdin().is_terminal()
329            )
330        }
331    }
332
333    #[test]
334    #[cfg(any(unix, target_os = "wasi"))]
335    fn stdout_vs_libc() {
336        unsafe {
337            assert_eq!(
338                libc::isatty(libc::STDOUT_FILENO) != 0,
339                rustix::stdio::stdout().is_terminal()
340            )
341        }
342    }
343
344    #[test]
345    #[cfg(any(unix, target_os = "wasi"))]
346    fn stderr_vs_libc() {
347        unsafe {
348            assert_eq!(
349                libc::isatty(libc::STDERR_FILENO) != 0,
350                rustix::stdio::stderr().is_terminal()
351            )
352        }
353    }
354
355    // Verify that the msys_tty_on function works with long path.
356    #[test]
357    #[cfg(windows)]
358    fn msys_tty_on_path_length() {
359        use std::{fs::File, os::windows::io::AsRawHandle};
360        use windows_sys::Win32::Foundation::MAX_PATH;
361
362        let dir = tempfile::tempdir().expect("Unable to create temporary directory");
363        let file_path = dir.path().join("ten_chars_".repeat(25));
364        // Ensure that the path is longer than MAX_PATH.
365        assert!(file_path.to_string_lossy().len() > MAX_PATH as usize);
366        let file = File::create(file_path).expect("Unable to create file");
367
368        assert!(!unsafe { crate::msys_tty_on(file.as_raw_handle()) });
369    }
370}