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