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