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(target_os = "unknown"))] 31use io_lifetimes::AsFilelike; 32#[cfg(windows)] 33use io_lifetimes::BorrowedHandle; 34#[cfg(windows)] 35use windows_sys::Win32::Foundation::HANDLE; 36#[cfg(windows)] 37use windows_sys::Win32::System::Console::STD_HANDLE; 38 39pub trait IsTerminal { 40 /// Returns true if this is a terminal. 41 /// 42 /// # Example 43 /// 44 /// ``` 45 /// use is_terminal::IsTerminal; 46 /// 47 /// if std::io::stdout().is_terminal() { 48 /// println!("stdout is a terminal") 49 /// } 50 /// ``` 51 fn is_terminal(&self) -> bool; 52} 53 54#[cfg(not(target_os = "unknown"))] 55impl<Stream: AsFilelike> IsTerminal for Stream { 56 #[inline] 57 fn is_terminal(&self) -> bool { 58 #[cfg(any(unix, target_os = "wasi"))] 59 { 60 rustix::termios::isatty(self) 61 } 62 63 #[cfg(target_os = "hermit")] 64 { 65 hermit_abi::isatty(self.as_filelike().as_fd()) 66 } 67 68 #[cfg(windows)] 69 { 70 _is_terminal(self.as_filelike()) 71 } 72 } 73} 74 75// The Windows implementation here is copied from atty, with #51 and #54 76// applied. The only significant modification is to take a `BorrowedHandle` 77// argument instead of using a `Stream` enum. 78 79#[cfg(windows)] 80fn _is_terminal(stream: BorrowedHandle<'_>) -> bool { 81 use std::os::windows::io::AsRawHandle; 82 use windows_sys::Win32::System::Console::GetStdHandle; 83 use windows_sys::Win32::System::Console::{ 84 STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT, 85 STD_OUTPUT_HANDLE as STD_OUTPUT, 86 }; 87 88 let (fd, others) = unsafe { 89 if stream.as_raw_handle() == GetStdHandle(STD_INPUT) as _ { 90 (STD_INPUT, [STD_ERROR, STD_OUTPUT]) 91 } else if stream.as_raw_handle() == GetStdHandle(STD_OUTPUT) as _ { 92 (STD_OUTPUT, [STD_INPUT, STD_ERROR]) 93 } else if stream.as_raw_handle() == GetStdHandle(STD_ERROR) as _ { 94 (STD_ERROR, [STD_INPUT, STD_OUTPUT]) 95 } else { 96 return false; 97 } 98 }; 99 if unsafe { console_on_any(&[fd]) } { 100 // False positives aren't possible. If we got a console then 101 // we definitely have a tty on stdin. 102 return true; 103 } 104 105 // At this point, we *could* have a false negative. We can determine that 106 // this is true negative if we can detect the presence of a console on 107 // any of the other streams. If another stream has a console, then we know 108 // we're in a Windows console and can therefore trust the negative. 109 if unsafe { console_on_any(&others) } { 110 return false; 111 } 112 113 // Otherwise, we fall back to a very strange msys hack to see if we can 114 // sneakily detect the presence of a tty. 115 // Safety: function has no invariants. an invalid handle id will cause 116 // GetFileInformationByHandleEx to return an error. 117 let handle = unsafe { GetStdHandle(fd) }; 118 unsafe { msys_tty_on(handle) } 119} 120 121/// Returns true if any of the given fds are on a console. 122#[cfg(windows)] 123unsafe fn console_on_any(fds: &[STD_HANDLE]) -> bool { 124 use windows_sys::Win32::System::Console::{GetConsoleMode, GetStdHandle}; 125 126 for &fd in fds { 127 let mut out = 0; 128 let handle = GetStdHandle(fd); 129 if GetConsoleMode(handle, &mut out) != 0 { 130 return true; 131 } 132 } 133 false 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::{FileNameInfo, GetFileInformationByHandleEx}, 143 }; 144 145 /// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving 146 /// it a fixed length that we can stack allocate 147 #[repr(C)] 148 #[allow(non_snake_case)] 149 struct FILE_NAME_INFO { 150 FileNameLength: u32, 151 FileName: [u16; MAX_PATH as usize], 152 } 153 let mut name_info = FILE_NAME_INFO { 154 FileNameLength: 0, 155 FileName: [0; MAX_PATH as usize], 156 }; 157 // Safety: buffer length is fixed. 158 let res = GetFileInformationByHandleEx( 159 handle, 160 FileNameInfo, 161 &mut name_info as *mut _ as *mut c_void, 162 std::mem::size_of::<FILE_NAME_INFO>() as u32, 163 ); 164 if res == 0 { 165 return false; 166 } 167 168 let s = &name_info.FileName[..name_info.FileNameLength as usize / 2]; 169 let name = String::from_utf16_lossy(s); 170 // This checks whether 'pty' exists in the file name, which indicates that 171 // a pseudo-terminal is attached. To mitigate against false positives 172 // (e.g., an actual file name that contains 'pty'), we also require that 173 // either the strings 'msys-' or 'cygwin-' are in the file name as well.) 174 let is_msys = name.contains("msys-") || name.contains("cygwin-"); 175 let is_pty = name.contains("-pty"); 176 is_msys && is_pty 177} 178 179#[cfg(target_os = "unknown")] 180impl IsTerminal for std::io::Stdin { 181 #[inline] 182 fn is_terminal(&self) -> bool { 183 false 184 } 185} 186 187#[cfg(target_os = "unknown")] 188impl IsTerminal for std::io::Stdout { 189 #[inline] 190 fn is_terminal(&self) -> bool { 191 false 192 } 193} 194 195#[cfg(target_os = "unknown")] 196impl IsTerminal for std::io::Stderr { 197 #[inline] 198 fn is_terminal(&self) -> bool { 199 false 200 } 201} 202 203#[cfg(target_os = "unknown")] 204impl<'a> IsTerminal for std::io::StdinLock<'a> { 205 #[inline] 206 fn is_terminal(&self) -> bool { 207 false 208 } 209} 210 211#[cfg(target_os = "unknown")] 212impl<'a> IsTerminal for std::io::StdoutLock<'a> { 213 #[inline] 214 fn is_terminal(&self) -> bool { 215 false 216 } 217} 218 219#[cfg(target_os = "unknown")] 220impl<'a> IsTerminal for std::io::StderrLock<'a> { 221 #[inline] 222 fn is_terminal(&self) -> bool { 223 false 224 } 225} 226 227#[cfg(target_os = "unknown")] 228impl<'a> IsTerminal for std::fs::File { 229 #[inline] 230 fn is_terminal(&self) -> bool { 231 false 232 } 233} 234 235#[cfg(target_os = "unknown")] 236impl IsTerminal for std::process::ChildStdin { 237 #[inline] 238 fn is_terminal(&self) -> bool { 239 false 240 } 241} 242 243#[cfg(target_os = "unknown")] 244impl IsTerminal for std::process::ChildStdout { 245 #[inline] 246 fn is_terminal(&self) -> bool { 247 false 248 } 249} 250 251#[cfg(target_os = "unknown")] 252impl IsTerminal for std::process::ChildStderr { 253 #[inline] 254 fn is_terminal(&self) -> bool { 255 false 256 } 257} 258 259#[cfg(test)] 260mod tests { 261 #[cfg(not(target_os = "unknown"))] 262 use super::IsTerminal; 263 264 #[test] 265 #[cfg(windows)] 266 fn stdin() { 267 assert_eq!( 268 atty::is(atty::Stream::Stdin), 269 std::io::stdin().is_terminal() 270 ) 271 } 272 273 #[test] 274 #[cfg(windows)] 275 fn stdout() { 276 assert_eq!( 277 atty::is(atty::Stream::Stdout), 278 std::io::stdout().is_terminal() 279 ) 280 } 281 282 #[test] 283 #[cfg(windows)] 284 fn stderr() { 285 assert_eq!( 286 atty::is(atty::Stream::Stderr), 287 std::io::stderr().is_terminal() 288 ) 289 } 290 291 #[test] 292 #[cfg(any(unix, target_os = "wasi"))] 293 fn stdin() { 294 unsafe { 295 assert_eq!( 296 atty::is(atty::Stream::Stdin), 297 rustix::io::stdin().is_terminal() 298 ) 299 } 300 } 301 302 #[test] 303 #[cfg(any(unix, target_os = "wasi"))] 304 fn stdout() { 305 unsafe { 306 assert_eq!( 307 atty::is(atty::Stream::Stdout), 308 rustix::io::stdout().is_terminal() 309 ) 310 } 311 } 312 313 #[test] 314 #[cfg(any(unix, target_os = "wasi"))] 315 fn stderr() { 316 unsafe { 317 assert_eq!( 318 atty::is(atty::Stream::Stderr), 319 rustix::io::stderr().is_terminal() 320 ) 321 } 322 } 323 324 #[test] 325 #[cfg(any(unix, target_os = "wasi"))] 326 fn stdin_vs_libc() { 327 unsafe { 328 assert_eq!( 329 libc::isatty(libc::STDIN_FILENO) != 0, 330 rustix::io::stdin().is_terminal() 331 ) 332 } 333 } 334 335 #[test] 336 #[cfg(any(unix, target_os = "wasi"))] 337 fn stdout_vs_libc() { 338 unsafe { 339 assert_eq!( 340 libc::isatty(libc::STDOUT_FILENO) != 0, 341 rustix::io::stdout().is_terminal() 342 ) 343 } 344 } 345 346 #[test] 347 #[cfg(any(unix, target_os = "wasi"))] 348 fn stderr_vs_libc() { 349 unsafe { 350 assert_eq!( 351 libc::isatty(libc::STDERR_FILENO) != 0, 352 rustix::io::stderr().is_terminal() 353 ) 354 } 355 } 356 357 // Verify that the msys_tty_on function works with long path. 358 #[test] 359 #[cfg(windows)] 360 fn msys_tty_on_path_length() { 361 use std::{fs::File, os::windows::io::AsRawHandle}; 362 use windows_sys::Win32::Foundation::MAX_PATH; 363 364 let dir = tempfile::tempdir().expect("Unable to create temporary directory"); 365 let file_path = dir.path().join("ten_chars_".repeat(25)); 366 // Ensure that the path is longer than MAX_PATH. 367 assert!(file_path.to_string_lossy().len() > MAX_PATH as usize); 368 let file = File::create(file_path).expect("Unable to create file"); 369 370 assert!(!unsafe { crate::msys_tty_on(file.as_raw_handle() as isize) }); 371 } 372} 373