Coverage Report

Created: 2025-11-11 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/is-terminal-0.4.17/src/lib.rs
Line
Count
Source
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 = "wasi")]
40
use std::os::fd::{AsFd, AsRawFd};
41
#[cfg(target_os = "hermit")]
42
use std::os::hermit::io::AsFd;
43
#[cfg(unix)]
44
use std::os::unix::io::{AsFd, AsRawFd};
45
#[cfg(windows)]
46
use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
47
#[cfg(windows)]
48
use windows_sys::Win32::Foundation::HANDLE;
49
50
/// Extension trait to check whether something is a terminal.
51
pub 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
/// ```
79
pub fn is_terminal<T: IsTerminal>(this: T) -> bool {
80
    this.is_terminal()
81
}
82
83
#[cfg(not(any(windows, target_os = "unknown")))]
84
impl<Stream: AsFd> IsTerminal for Stream {
85
    #[inline]
86
0
    fn is_terminal(&self) -> bool {
87
        #[cfg(any(unix, target_os = "wasi"))]
88
        {
89
0
            let fd = self.as_fd();
90
0
            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
0
    }
Unexecuted instantiation: <std::io::stdio::Stderr as is_terminal::IsTerminal>::is_terminal
Unexecuted instantiation: <std::io::stdio::Stdout as is_terminal::IsTerminal>::is_terminal
99
}
100
101
#[cfg(windows)]
102
impl<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)]
114
fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
115
    use windows_sys::Win32::System::Console::GetConsoleMode;
116
117
    let handle = handle.as_raw_handle();
118
119
    // A null handle means the process has no console.
120
    if handle.is_null() {
121
        return false;
122
    }
123
124
    unsafe {
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)]
138
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::{
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")]
196
impl IsTerminal for std::io::Stdin {
197
    #[inline]
198
    fn is_terminal(&self) -> bool {
199
        false
200
    }
201
}
202
203
#[cfg(target_os = "unknown")]
204
impl IsTerminal for std::io::Stdout {
205
    #[inline]
206
    fn is_terminal(&self) -> bool {
207
        false
208
    }
209
}
210
211
#[cfg(target_os = "unknown")]
212
impl IsTerminal for std::io::Stderr {
213
    #[inline]
214
    fn is_terminal(&self) -> bool {
215
        false
216
    }
217
}
218
219
#[cfg(target_os = "unknown")]
220
impl<'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")]
228
impl<'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")]
236
impl<'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")]
244
impl<'a> IsTerminal for std::fs::File {
245
    #[inline]
246
    fn is_terminal(&self) -> bool {
247
        false
248
    }
249
}
250
251
#[cfg(target_os = "unknown")]
252
impl IsTerminal for std::process::ChildStdin {
253
    #[inline]
254
    fn is_terminal(&self) -> bool {
255
        false
256
    }
257
}
258
259
#[cfg(target_os = "unknown")]
260
impl IsTerminal for std::process::ChildStdout {
261
    #[inline]
262
    fn is_terminal(&self) -> bool {
263
        false
264
    }
265
}
266
267
#[cfg(target_os = "unknown")]
268
impl IsTerminal for std::process::ChildStderr {
269
    #[inline]
270
    fn is_terminal(&self) -> bool {
271
        false
272
    }
273
}
274
275
#[cfg(test)]
276
mod 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()) });
381
    }
382
}