rtoolbox/
atty.rs

1// Copyright (c) 2015-2019 Doug Tangren
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13
14#[cfg(windows)]
15use winapi::shared::minwindef::DWORD;
16#[cfg(windows)]
17use winapi::shared::ntdef::WCHAR;
18
19/// possible stream sources
20#[derive(Clone, Copy, Debug)]
21pub enum Stream {
22    Stdout,
23    Stderr,
24    Stdin,
25}
26
27/// returns true if this is a tty
28#[cfg(target_family = "unix")]
29pub fn is(stream: Stream) -> bool {
30    let fd = match stream {
31        Stream::Stdout => libc::STDOUT_FILENO,
32        Stream::Stderr => libc::STDERR_FILENO,
33        Stream::Stdin => libc::STDIN_FILENO,
34    };
35    unsafe { libc::isatty(fd) != 0 }
36}
37
38/// returns true if this is a tty
39#[cfg(target_os = "hermit")]
40pub fn is(stream: Stream) -> bool {
41    let fd = match stream {
42        Stream::Stdout => hermit_abi::STDOUT_FILENO,
43        Stream::Stderr => hermit_abi::STDERR_FILENO,
44        Stream::Stdin => hermit_abi::STDIN_FILENO,
45    };
46    hermit_abi::isatty(fd)
47}
48
49/// returns true if this is a tty
50#[cfg(windows)]
51pub fn is(stream: Stream) -> bool {
52    use winapi::um::winbase::{
53        STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT,
54        STD_OUTPUT_HANDLE as STD_OUTPUT,
55    };
56
57    let (fd, others) = match stream {
58        Stream::Stdin => (STD_INPUT, [STD_ERROR, STD_OUTPUT]),
59        Stream::Stderr => (STD_ERROR, [STD_INPUT, STD_OUTPUT]),
60        Stream::Stdout => (STD_OUTPUT, [STD_INPUT, STD_ERROR]),
61    };
62    if unsafe { console_on_any(&[fd]) } {
63        // False positives aren't possible. If we got a console then
64        // we definitely have a tty on stdin.
65        return true;
66    }
67
68    // At this point, we *could* have a false negative. We can determine that
69    // this is true negative if we can detect the presence of a console on
70    // any of the other streams. If another stream has a console, then we know
71    // we're in a Windows console and can therefore trust the negative.
72    if unsafe { console_on_any(&others) } {
73        return false;
74    }
75
76    // Otherwise, we fall back to a very strange msys hack to see if we can
77    // sneakily detect the presence of a tty.
78    unsafe { msys_tty_on(fd) }
79}
80
81/// returns true if this is _not_ a tty
82pub fn isnt(stream: Stream) -> bool {
83    !is(stream)
84}
85
86/// Returns true if any of the given fds are on a console.
87#[cfg(windows)]
88unsafe fn console_on_any(fds: &[DWORD]) -> bool {
89    use winapi::um::{consoleapi::GetConsoleMode, processenv::GetStdHandle};
90
91    for &fd in fds {
92        let mut out = 0;
93        let handle = GetStdHandle(fd);
94        if GetConsoleMode(handle, &mut out) != 0 {
95            return true;
96        }
97    }
98    false
99}
100
101/// Returns true if there is an MSYS tty on the given handle.
102#[cfg(windows)]
103unsafe fn msys_tty_on(fd: DWORD) -> bool {
104    use std::{mem, slice};
105
106    use winapi::{
107        ctypes::c_void,
108        shared::minwindef::MAX_PATH,
109        um::{
110            fileapi::FILE_NAME_INFO, minwinbase::FileNameInfo, processenv::GetStdHandle,
111            winbase::GetFileInformationByHandleEx,
112        },
113    };
114
115    let size = mem::size_of::<FILE_NAME_INFO>();
116    let mut name_info_bytes = vec![0u8; size + MAX_PATH * mem::size_of::<WCHAR>()];
117    let res = GetFileInformationByHandleEx(
118        GetStdHandle(fd),
119        FileNameInfo,
120        &mut *name_info_bytes as *mut _ as *mut c_void,
121        name_info_bytes.len() as u32,
122    );
123    if res == 0 {
124        return false;
125    }
126    let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO);
127    let s = slice::from_raw_parts(
128        name_info.FileName.as_ptr(),
129        name_info.FileNameLength as usize / 2,
130    );
131    let name = String::from_utf16_lossy(s);
132    // This checks whether 'pty' exists in the file name, which indicates that
133    // a pseudo-terminal is attached. To mitigate against false positives
134    // (e.g., an actual file name that contains 'pty'), we also require that
135    // either the strings 'msys-' or 'cygwin-' are in the file name as well.)
136    let is_msys = name.contains("msys-") || name.contains("cygwin-");
137    let is_pty = name.contains("-pty");
138    is_msys && is_pty
139}
140
141/// returns true if this is a tty
142#[cfg(target_family = "wasm")]
143pub fn is(_stream: Stream) -> bool {
144    false
145}