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 windows_sys::Win32::System::Console::STD_HANDLE;
16
17#[cfg(windows)]
18type WCHAR = u16;
19
20/// possible stream sources
21#[derive(Clone, Copy, Debug)]
22pub enum Stream {
23    Stdout,
24    Stderr,
25    Stdin,
26}
27
28/// returns true if this is a tty
29#[cfg(target_family = "unix")]
30pub fn is(stream: Stream) -> bool {
31    let fd = match stream {
32        Stream::Stdout => libc::STDOUT_FILENO,
33        Stream::Stderr => libc::STDERR_FILENO,
34        Stream::Stdin => libc::STDIN_FILENO,
35    };
36    unsafe { libc::isatty(fd) != 0 }
37}
38
39/// returns true if this is a tty
40#[cfg(target_os = "hermit")]
41pub fn is(stream: Stream) -> bool {
42    let fd = match stream {
43        Stream::Stdout => hermit_abi::STDOUT_FILENO,
44        Stream::Stderr => hermit_abi::STDERR_FILENO,
45        Stream::Stdin => hermit_abi::STDIN_FILENO,
46    };
47    hermit_abi::isatty(fd)
48}
49
50/// returns true if this is a tty
51#[cfg(windows)]
52pub fn is(stream: Stream) -> bool {
53    use windows_sys::Win32::System::Console::{
54        STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT,
55        STD_OUTPUT_HANDLE as STD_OUTPUT,
56    };
57
58    let (fd, others) = match stream {
59        Stream::Stdin => (STD_INPUT, [STD_ERROR, STD_OUTPUT]),
60        Stream::Stderr => (STD_ERROR, [STD_INPUT, STD_OUTPUT]),
61        Stream::Stdout => (STD_OUTPUT, [STD_INPUT, STD_ERROR]),
62    };
63    if unsafe { console_on_any(&[fd]) } {
64        // False positives aren't possible. If we got a console then
65        // we definitely have a tty on stdin.
66        return true;
67    }
68
69    // At this point, we *could* have a false negative. We can determine that
70    // this is true negative if we can detect the presence of a console on
71    // any of the other streams. If another stream has a console, then we know
72    // we're in a Windows console and can therefore trust the negative.
73    if unsafe { console_on_any(&others) } {
74        return false;
75    }
76
77    // Otherwise, we fall back to a very strange msys hack to see if we can
78    // sneakily detect the presence of a tty.
79    unsafe { msys_tty_on(fd) }
80}
81
82/// returns true if this is _not_ a tty
83pub fn isnt(stream: Stream) -> bool {
84    !is(stream)
85}
86
87/// Returns true if any of the given fds are on a console.
88#[cfg(windows)]
89unsafe fn console_on_any(fds: &[STD_HANDLE]) -> bool {
90    use windows_sys::Win32::System::Console::{GetConsoleMode, GetStdHandle};
91
92    for &fd in fds {
93        let mut out = 0;
94        let handle = GetStdHandle(fd);
95        if GetConsoleMode(handle, &mut out) != 0 {
96            return true;
97        }
98    }
99    false
100}
101
102/// Returns true if there is an MSYS tty on the given handle.
103#[cfg(windows)]
104unsafe fn msys_tty_on(fd: STD_HANDLE) -> bool {
105    use std::os::raw::c_void;
106    use std::{mem, slice};
107
108    use windows_sys::Win32::Foundation::MAX_PATH;
109    use windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandleEx;
110    use windows_sys::Win32::Storage::FileSystem::{FileNameInfo, FILE_NAME_INFO};
111    use windows_sys::Win32::System::Console::GetStdHandle;
112
113    let size = mem::size_of::<FILE_NAME_INFO>();
114    let mut name_info_bytes = vec![0u8; size + MAX_PATH as usize * mem::size_of::<WCHAR>()];
115    let res = GetFileInformationByHandleEx(
116        GetStdHandle(fd),
117        FileNameInfo,
118        &mut *name_info_bytes as *mut _ as *mut c_void,
119        name_info_bytes.len() as u32,
120    );
121    if res == 0 {
122        return false;
123    }
124    let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO);
125    let s = slice::from_raw_parts(
126        name_info.FileName.as_ptr(),
127        name_info.FileNameLength as usize / 2,
128    );
129    let name = String::from_utf16_lossy(s);
130    // This checks whether 'pty' exists in the file name, which indicates that
131    // a pseudo-terminal is attached. To mitigate against false positives
132    // (e.g., an actual file name that contains 'pty'), we also require that
133    // either the strings 'msys-' or 'cygwin-' are in the file name as well.)
134    let is_msys = name.contains("msys-") || name.contains("cygwin-");
135    let is_pty = name.contains("-pty");
136    is_msys && is_pty
137}
138
139/// returns true if this is a tty
140#[cfg(target_family = "wasm")]
141pub fn is(_stream: Stream) -> bool {
142    false
143}