blob: e363f14c11d14873316a41457099b0925122aa7b [file] [log] [blame]
//! is-terminal is a simple utility that answers one question:
//!
//! > Is this a terminal?
//!
//! A "terminal", also known as a "tty", is an I/O device which may be
//! interactive and may support color and other special features. This crate
//! doesn't provide any of those features; it just answers this one question.
//!
//! On Unix-family platforms, this is effectively the same as the [`isatty`]
//! function for testing whether a given stream is a terminal, though it
//! accepts high-level stream types instead of raw file descriptors.
//!
//! On Windows, it uses a variety of techniques to determine whether the
//! given stream is a terminal.
//!
//! # Example
//!
//! ```rust
//! use is_terminal::IsTerminal;
//!
//! if std::io::stdout().is_terminal() {
//! println!("stdout is a terminal")
//! }
//! ```
//!
//! [`isatty`]: https://man7.org/linux/man-pages/man3/isatty.3.html
#![cfg_attr(unix, no_std)]
#[cfg(not(target_os = "unknown"))]
use io_lifetimes::AsFilelike;
#[cfg(windows)]
use io_lifetimes::BorrowedHandle;
#[cfg(target_os = "hermit")]
use std::os::hermit::io::AsRawFd;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
#[cfg(windows)]
use windows_sys::Win32::Foundation::HANDLE;
/// Extension trait to check whether something is a terminal.
pub trait IsTerminal {
/// Returns true if this is a terminal.
///
/// # Example
///
/// ```
/// use is_terminal::IsTerminal;
///
/// if std::io::stdout().is_terminal() {
/// println!("stdout is a terminal")
/// }
/// ```
fn is_terminal(&self) -> bool;
}
/// Returns `true` if `this` is a terminal.
///
/// This is equivalent to calling `this.is_terminal()` and exists only as a
/// convenience to calling the trait method [`IsTerminal::is_terminal`]
/// without importing the trait.
///
/// # Example
///
/// ```
/// if is_terminal::is_terminal(&std::io::stdout()) {
/// println!("stdout is a terminal")
/// }
/// ```
pub fn is_terminal<T: IsTerminal>(this: &T) -> bool {
this.is_terminal()
}
#[cfg(not(target_os = "unknown"))]
impl<Stream: AsFilelike> IsTerminal for Stream {
#[inline]
fn is_terminal(&self) -> bool {
#[cfg(any(unix, target_os = "wasi"))]
{
rustix::termios::isatty(self)
}
#[cfg(target_os = "hermit")]
{
hermit_abi::isatty(self.as_filelike().as_raw_fd())
}
#[cfg(windows)]
{
handle_is_console(self.as_filelike())
}
}
}
// The Windows implementation here is copied from `handle_is_console` in
// std/src/sys/windows/io.rs in Rust at revision
// d7b0bcb20f2f7d5f3ea3489d56ece630147e98f5.
#[cfg(windows)]
fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
use windows_sys::Win32::System::Console::{
GetConsoleMode, GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
};
let handle = handle.as_raw_handle();
unsafe {
// A null handle means the process has no console.
if handle.is_null() {
return false;
}
let mut out = 0;
if GetConsoleMode(handle as HANDLE, &mut out) != 0 {
// False positives aren't possible. If we got a console then we definitely have a console.
return true;
}
// At this point, we *could* have a false negative. We can determine that this is a true
// negative if we can detect the presence of a console on any of the standard I/O streams. If
// another stream has a console, then we know we're in a Windows console and can therefore
// trust the negative.
for std_handle in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
let std_handle = GetStdHandle(std_handle);
if std_handle != 0
&& std_handle != handle as HANDLE
&& GetConsoleMode(std_handle, &mut out) != 0
{
return false;
}
}
// Otherwise, we fall back to an msys hack to see if we can detect the presence of a pty.
msys_tty_on(handle as HANDLE)
}
}
/// Returns true if there is an MSYS tty on the given handle.
///
/// This incoproates d7b0bcb20f2f7d5f3ea3489d56ece630147e98f5
#[cfg(windows)]
unsafe fn msys_tty_on(handle: HANDLE) -> bool {
use std::ffi::c_void;
use windows_sys::Win32::{
Foundation::MAX_PATH,
Storage::FileSystem::{
FileNameInfo, GetFileInformationByHandleEx, GetFileType, FILE_TYPE_PIPE,
},
};
// Early return if the handle is not a pipe.
if GetFileType(handle) != FILE_TYPE_PIPE {
return false;
}
/// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving
/// it a fixed length that we can stack allocate
#[repr(C)]
#[allow(non_snake_case)]
struct FILE_NAME_INFO {
FileNameLength: u32,
FileName: [u16; MAX_PATH as usize],
}
let mut name_info = FILE_NAME_INFO {
FileNameLength: 0,
FileName: [0; MAX_PATH as usize],
};
// Safety: buffer length is fixed.
let res = GetFileInformationByHandleEx(
handle,
FileNameInfo,
&mut name_info as *mut _ as *mut c_void,
std::mem::size_of::<FILE_NAME_INFO>() as u32,
);
if res == 0 {
return false;
}
// Use `get` because `FileNameLength` can be out of range.
let s = match name_info
.FileName
.get(..name_info.FileNameLength as usize / 2)
{
None => return false,
Some(s) => s,
};
let name = String::from_utf16_lossy(s);
// Get the file name only.
let name = name.rsplit('\\').next().unwrap_or(&name);
// This checks whether 'pty' exists in the file name, which indicates that
// a pseudo-terminal is attached. To mitigate against false positives
// (e.g., an actual file name that contains 'pty'), we also require that
// the file name begins with either the strings 'msys-' or 'cygwin-'.)
let is_msys = name.starts_with("msys-") || name.starts_with("cygwin-");
let is_pty = name.contains("-pty");
is_msys && is_pty
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::io::Stdin {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::io::Stdout {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::io::Stderr {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl<'a> IsTerminal for std::io::StdinLock<'a> {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl<'a> IsTerminal for std::io::StdoutLock<'a> {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl<'a> IsTerminal for std::io::StderrLock<'a> {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl<'a> IsTerminal for std::fs::File {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::process::ChildStdin {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::process::ChildStdout {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(target_os = "unknown")]
impl IsTerminal for std::process::ChildStderr {
#[inline]
fn is_terminal(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
#[cfg(not(target_os = "unknown"))]
use super::IsTerminal;
#[test]
#[cfg(windows)]
fn stdin() {
assert_eq!(
atty::is(atty::Stream::Stdin),
std::io::stdin().is_terminal()
)
}
#[test]
#[cfg(windows)]
fn stdout() {
assert_eq!(
atty::is(atty::Stream::Stdout),
std::io::stdout().is_terminal()
)
}
#[test]
#[cfg(windows)]
fn stderr() {
assert_eq!(
atty::is(atty::Stream::Stderr),
std::io::stderr().is_terminal()
)
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stdin() {
assert_eq!(
atty::is(atty::Stream::Stdin),
rustix::io::stdin().is_terminal()
)
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stdout() {
assert_eq!(
atty::is(atty::Stream::Stdout),
rustix::io::stdout().is_terminal()
)
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stderr() {
assert_eq!(
atty::is(atty::Stream::Stderr),
rustix::io::stderr().is_terminal()
)
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stdin_vs_libc() {
unsafe {
assert_eq!(
libc::isatty(libc::STDIN_FILENO) != 0,
rustix::io::stdin().is_terminal()
)
}
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stdout_vs_libc() {
unsafe {
assert_eq!(
libc::isatty(libc::STDOUT_FILENO) != 0,
rustix::io::stdout().is_terminal()
)
}
}
#[test]
#[cfg(any(unix, target_os = "wasi"))]
fn stderr_vs_libc() {
unsafe {
assert_eq!(
libc::isatty(libc::STDERR_FILENO) != 0,
rustix::io::stderr().is_terminal()
)
}
}
// Verify that the msys_tty_on function works with long path.
#[test]
#[cfg(windows)]
fn msys_tty_on_path_length() {
use std::{fs::File, os::windows::io::AsRawHandle};
use windows_sys::Win32::Foundation::MAX_PATH;
let dir = tempfile::tempdir().expect("Unable to create temporary directory");
let file_path = dir.path().join("ten_chars_".repeat(25));
// Ensure that the path is longer than MAX_PATH.
assert!(file_path.to_string_lossy().len() > MAX_PATH as usize);
let file = File::create(file_path).expect("Unable to create file");
assert!(!unsafe { crate::msys_tty_on(file.as_raw_handle() as isize) });
}
}