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 = "hermit")]
40use std::os::hermit::io::AsFd;
41#[cfg(unix)]
42use std::os::unix::io::{AsFd, AsRawFd};
43#[cfg(target_os = "wasi")]
44use std::os::wasi::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: BorrowedFd<'_> = 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 unsafe {
120 // A null handle means the process has no console.
121 if handle.is_null() {
122 return false;
123 }
124
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() as isize) });
381 }
382}
383