• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"))]
31 use io_lifetimes::AsFilelike;
32 #[cfg(windows)]
33 use io_lifetimes::BorrowedHandle;
34 #[cfg(windows)]
35 use windows_sys::Win32::Foundation::HANDLE;
36 #[cfg(windows)]
37 use windows_sys::Win32::System::Console::STD_HANDLE;
38 
39 pub 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     /// ```
is_terminal(&self) -> bool51     fn is_terminal(&self) -> bool;
52 }
53 
54 #[cfg(not(target_os = "unknown"))]
55 impl<Stream: AsFilelike> IsTerminal for Stream {
56     #[inline]
is_terminal(&self) -> bool57     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)]
_is_terminal(stream: BorrowedHandle<'_>) -> bool80 fn _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)]
console_on_any(fds: &[STD_HANDLE]) -> bool123 unsafe 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)]
msys_tty_on(handle: HANDLE) -> bool138 unsafe 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")]
180 impl IsTerminal for std::io::Stdin {
181     #[inline]
is_terminal(&self) -> bool182     fn is_terminal(&self) -> bool {
183         false
184     }
185 }
186 
187 #[cfg(target_os = "unknown")]
188 impl IsTerminal for std::io::Stdout {
189     #[inline]
is_terminal(&self) -> bool190     fn is_terminal(&self) -> bool {
191         false
192     }
193 }
194 
195 #[cfg(target_os = "unknown")]
196 impl IsTerminal for std::io::Stderr {
197     #[inline]
is_terminal(&self) -> bool198     fn is_terminal(&self) -> bool {
199         false
200     }
201 }
202 
203 #[cfg(target_os = "unknown")]
204 impl<'a> IsTerminal for std::io::StdinLock<'a> {
205     #[inline]
is_terminal(&self) -> bool206     fn is_terminal(&self) -> bool {
207         false
208     }
209 }
210 
211 #[cfg(target_os = "unknown")]
212 impl<'a> IsTerminal for std::io::StdoutLock<'a> {
213     #[inline]
is_terminal(&self) -> bool214     fn is_terminal(&self) -> bool {
215         false
216     }
217 }
218 
219 #[cfg(target_os = "unknown")]
220 impl<'a> IsTerminal for std::io::StderrLock<'a> {
221     #[inline]
is_terminal(&self) -> bool222     fn is_terminal(&self) -> bool {
223         false
224     }
225 }
226 
227 #[cfg(target_os = "unknown")]
228 impl<'a> IsTerminal for std::fs::File {
229     #[inline]
is_terminal(&self) -> bool230     fn is_terminal(&self) -> bool {
231         false
232     }
233 }
234 
235 #[cfg(target_os = "unknown")]
236 impl IsTerminal for std::process::ChildStdin {
237     #[inline]
is_terminal(&self) -> bool238     fn is_terminal(&self) -> bool {
239         false
240     }
241 }
242 
243 #[cfg(target_os = "unknown")]
244 impl IsTerminal for std::process::ChildStdout {
245     #[inline]
is_terminal(&self) -> bool246     fn is_terminal(&self) -> bool {
247         false
248     }
249 }
250 
251 #[cfg(target_os = "unknown")]
252 impl IsTerminal for std::process::ChildStderr {
253     #[inline]
is_terminal(&self) -> bool254     fn is_terminal(&self) -> bool {
255         false
256     }
257 }
258 
259 #[cfg(test)]
260 mod tests {
261     #[cfg(not(target_os = "unknown"))]
262     use super::IsTerminal;
263 
264     #[test]
265     #[cfg(windows)]
stdin()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)]
stdout()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)]
stderr()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"))]
stdin()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"))]
stdout()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"))]
stderr()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"))]
stdin_vs_libc()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"))]
stdout_vs_libc()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"))]
stderr_vs_libc()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)]
msys_tty_on_path_length()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