atty/
lib.rs

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