is_terminal/
lib.rs

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