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