• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! atty is a simple utility that answers one question
2 //! > is this a tty?
3 //!
4 //! usage is just as simple
5 //!
6 //! ```
7 //! if atty::is(atty::Stream::Stdout) {
8 //!   println!("i'm a tty")
9 //! }
10 //! ```
11 //!
12 //! ```
13 //! if atty::isnt(atty::Stream::Stdout) {
14 //!   println!("i'm not a tty")
15 //! }
16 //! ```
17 
18 #![cfg_attr(unix, no_std)]
19 
20 #[cfg(unix)]
21 extern crate libc;
22 #[cfg(windows)]
23 extern crate winapi;
24 
25 #[cfg(windows)]
26 use winapi::shared::minwindef::DWORD;
27 #[cfg(windows)]
28 use winapi::shared::ntdef::WCHAR;
29 
30 /// possible stream sources
31 #[derive(Clone, Copy, Debug)]
32 pub enum Stream {
33     Stdout,
34     Stderr,
35     Stdin,
36 }
37 
38 /// returns true if this is a tty
39 #[cfg(all(unix, not(target_arch = "wasm32")))]
is(stream: Stream) -> bool40 pub fn is(stream: Stream) -> bool {
41     extern crate libc;
42 
43     let fd = match stream {
44         Stream::Stdout => libc::STDOUT_FILENO,
45         Stream::Stderr => libc::STDERR_FILENO,
46         Stream::Stdin => libc::STDIN_FILENO,
47     };
48     unsafe { libc::isatty(fd) != 0 }
49 }
50 
51 /// returns true if this is a tty
52 #[cfg(target_os = "hermit")]
is(stream: Stream) -> bool53 pub fn is(stream: Stream) -> bool {
54     extern crate hermit_abi;
55 
56     let fd = match stream {
57         Stream::Stdout => hermit_abi::STDOUT_FILENO,
58         Stream::Stderr => hermit_abi::STDERR_FILENO,
59         Stream::Stdin => hermit_abi::STDIN_FILENO,
60     };
61     hermit_abi::isatty(fd)
62 }
63 
64 /// returns true if this is a tty
65 #[cfg(windows)]
is(stream: Stream) -> bool66 pub fn is(stream: Stream) -> bool {
67     use winapi::um::winbase::{
68         STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT,
69         STD_OUTPUT_HANDLE as STD_OUTPUT,
70     };
71 
72     let (fd, others) = match stream {
73         Stream::Stdin => (STD_INPUT, [STD_ERROR, STD_OUTPUT]),
74         Stream::Stderr => (STD_ERROR, [STD_INPUT, STD_OUTPUT]),
75         Stream::Stdout => (STD_OUTPUT, [STD_INPUT, STD_ERROR]),
76     };
77     if unsafe { console_on_any(&[fd]) } {
78         // False positives aren't possible. If we got a console then
79         // we definitely have a tty on stdin.
80         return true;
81     }
82 
83     // At this point, we *could* have a false negative. We can determine that
84     // this is true negative if we can detect the presence of a console on
85     // any of the other streams. If another stream has a console, then we know
86     // we're in a Windows console and can therefore trust the negative.
87     if unsafe { console_on_any(&others) } {
88         return false;
89     }
90 
91     // Otherwise, we fall back to a very strange msys hack to see if we can
92     // sneakily detect the presence of a tty.
93     unsafe { msys_tty_on(fd) }
94 }
95 
96 /// returns true if this is _not_ a tty
isnt(stream: Stream) -> bool97 pub fn isnt(stream: Stream) -> bool {
98     !is(stream)
99 }
100 
101 /// Returns true if any of the given fds are on a console.
102 #[cfg(windows)]
console_on_any(fds: &[DWORD]) -> bool103 unsafe fn console_on_any(fds: &[DWORD]) -> bool {
104     use winapi::um::{consoleapi::GetConsoleMode, processenv::GetStdHandle};
105 
106     for &fd in fds {
107         let mut out = 0;
108         let handle = GetStdHandle(fd);
109         if GetConsoleMode(handle, &mut out) != 0 {
110             return true;
111         }
112     }
113     false
114 }
115 
116 /// Returns true if there is an MSYS tty on the given handle.
117 #[cfg(windows)]
msys_tty_on(fd: DWORD) -> bool118 unsafe fn msys_tty_on(fd: DWORD) -> bool {
119     use std::{mem, slice};
120 
121     use winapi::{
122         ctypes::c_void,
123         shared::minwindef::MAX_PATH,
124         um::{
125             fileapi::FILE_NAME_INFO, minwinbase::FileNameInfo, processenv::GetStdHandle,
126             winbase::GetFileInformationByHandleEx,
127         },
128     };
129 
130     let size = mem::size_of::<FILE_NAME_INFO>();
131     let mut name_info_bytes = vec![0u8; size + MAX_PATH * mem::size_of::<WCHAR>()];
132     let res = GetFileInformationByHandleEx(
133         GetStdHandle(fd),
134         FileNameInfo,
135         &mut *name_info_bytes as *mut _ as *mut c_void,
136         name_info_bytes.len() as u32,
137     );
138     if res == 0 {
139         return false;
140     }
141     let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO);
142     let s = slice::from_raw_parts(
143         name_info.FileName.as_ptr(),
144         name_info.FileNameLength as usize / 2,
145     );
146     let name = String::from_utf16_lossy(s);
147     // This checks whether 'pty' exists in the file name, which indicates that
148     // a pseudo-terminal is attached. To mitigate against false positives
149     // (e.g., an actual file name that contains 'pty'), we also require that
150     // either the strings 'msys-' or 'cygwin-' are in the file name as well.)
151     let is_msys = name.contains("msys-") || name.contains("cygwin-");
152     let is_pty = name.contains("-pty");
153     is_msys && is_pty
154 }
155 
156 /// returns true if this is a tty
157 #[cfg(target_arch = "wasm32")]
is(_stream: Stream) -> bool158 pub fn is(_stream: Stream) -> bool {
159     false
160 }
161 
162 #[cfg(test)]
163 mod tests {
164     use super::{is, Stream};
165 
166     #[test]
167     #[cfg(windows)]
is_err()168     fn is_err() {
169         // appveyor pipes its output
170         assert!(!is(Stream::Stderr))
171     }
172 
173     #[test]
174     #[cfg(windows)]
is_out()175     fn is_out() {
176         // appveyor pipes its output
177         assert!(!is(Stream::Stdout))
178     }
179 
180     #[test]
181     #[cfg(windows)]
is_in()182     fn is_in() {
183         assert!(is(Stream::Stdin))
184     }
185 
186     #[test]
187     #[cfg(unix)]
is_err()188     fn is_err() {
189         assert!(is(Stream::Stderr))
190     }
191 
192     #[test]
193     #[cfg(unix)]
is_out()194     fn is_out() {
195         assert!(is(Stream::Stdout))
196     }
197 
198     #[test]
199     #[cfg(target_os = "macos")]
is_in()200     fn is_in() {
201         // macos on travis seems to pipe its input
202         assert!(is(Stream::Stdin))
203     }
204 
205     #[test]
206     #[cfg(all(not(target_os = "macos"), unix))]
is_in()207     fn is_in() {
208         assert!(is(Stream::Stdin))
209     }
210 }
211