rustix_is_terminal/
lib.rs1#![cfg_attr(unix, no_std)]
29
30#[cfg(not(any(windows, target_os = "hermit", target_os = "unknown")))]
31use rustix::fd::AsFd;
32#[cfg(target_os = "hermit")]
33use std::os::hermit::io::AsFd;
34#[cfg(windows)]
35use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
36#[cfg(windows)]
37use windows_sys::Win32::Foundation::HANDLE;
38
39pub trait IsTerminal {
41 fn is_terminal(&self) -> bool;
53}
54
55pub fn is_terminal<T: IsTerminal>(this: T) -> bool {
69 this.is_terminal()
70}
71
72#[cfg(not(any(windows, target_os = "unknown")))]
73impl<Stream: AsFd> IsTerminal for Stream {
74 #[inline]
75 fn is_terminal(&self) -> bool {
76 #[cfg(any(unix, target_os = "wasi"))]
77 {
78 rustix::termios::isatty(self)
79 }
80
81 #[cfg(target_os = "hermit")]
82 {
83 use std::os::hermit::io::AsRawFd;
84 hermit_abi::isatty(self.as_fd().as_fd().as_raw_fd())
85 }
86 }
87}
88
89#[cfg(windows)]
90impl<Stream: AsHandle> IsTerminal for Stream {
91 #[inline]
92 fn is_terminal(&self) -> bool {
93 handle_is_console(self.as_handle())
94 }
95}
96
97#[cfg(windows)]
102fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
103 use windows_sys::Win32::System::Console::GetConsoleMode;
104
105 let handle = handle.as_raw_handle();
106
107 if handle.is_null() {
109 return false;
110 }
111
112 unsafe {
113 let mut out = 0;
114 if GetConsoleMode(handle, &mut out) != 0 {
115 return true;
117 }
118
119 msys_tty_on(handle)
121 }
122}
123
124#[cfg(windows)]
126unsafe fn msys_tty_on(handle: HANDLE) -> bool {
127 use std::ffi::c_void;
128 use windows_sys::Win32::{
129 Foundation::MAX_PATH,
130 Storage::FileSystem::{
131 FileNameInfo, GetFileInformationByHandleEx, GetFileType, FILE_TYPE_PIPE,
132 },
133 };
134
135 if GetFileType(handle) != FILE_TYPE_PIPE {
137 return false;
138 }
139
140 #[repr(C)]
143 #[allow(non_snake_case)]
144 struct FILE_NAME_INFO {
145 FileNameLength: u32,
146 FileName: [u16; MAX_PATH as usize],
147 }
148 let mut name_info = FILE_NAME_INFO {
149 FileNameLength: 0,
150 FileName: [0; MAX_PATH as usize],
151 };
152 let res = GetFileInformationByHandleEx(
154 handle,
155 FileNameInfo,
156 &mut name_info as *mut _ as *mut c_void,
157 std::mem::size_of::<FILE_NAME_INFO>() as u32,
158 );
159 if res == 0 {
160 return false;
161 }
162
163 let s = match name_info
165 .FileName
166 .get(..name_info.FileNameLength as usize / 2)
167 {
168 None => return false,
169 Some(s) => s,
170 };
171 let name = String::from_utf16_lossy(s);
172 let name = name.rsplit('\\').next().unwrap_or(&name);
174 let is_msys = name.starts_with("msys-") || name.starts_with("cygwin-");
179 let is_pty = name.contains("-pty");
180 is_msys && is_pty
181}
182
183#[cfg(target_os = "unknown")]
184impl IsTerminal for std::io::Stdin {
185 #[inline]
186 fn is_terminal(&self) -> bool {
187 false
188 }
189}
190
191#[cfg(target_os = "unknown")]
192impl IsTerminal for std::io::Stdout {
193 #[inline]
194 fn is_terminal(&self) -> bool {
195 false
196 }
197}
198
199#[cfg(target_os = "unknown")]
200impl IsTerminal for std::io::Stderr {
201 #[inline]
202 fn is_terminal(&self) -> bool {
203 false
204 }
205}
206
207#[cfg(target_os = "unknown")]
208impl<'a> IsTerminal for std::io::StdinLock<'a> {
209 #[inline]
210 fn is_terminal(&self) -> bool {
211 false
212 }
213}
214
215#[cfg(target_os = "unknown")]
216impl<'a> IsTerminal for std::io::StdoutLock<'a> {
217 #[inline]
218 fn is_terminal(&self) -> bool {
219 false
220 }
221}
222
223#[cfg(target_os = "unknown")]
224impl<'a> IsTerminal for std::io::StderrLock<'a> {
225 #[inline]
226 fn is_terminal(&self) -> bool {
227 false
228 }
229}
230
231#[cfg(target_os = "unknown")]
232impl<'a> IsTerminal for std::fs::File {
233 #[inline]
234 fn is_terminal(&self) -> bool {
235 false
236 }
237}
238
239#[cfg(target_os = "unknown")]
240impl IsTerminal for std::process::ChildStdin {
241 #[inline]
242 fn is_terminal(&self) -> bool {
243 false
244 }
245}
246
247#[cfg(target_os = "unknown")]
248impl IsTerminal for std::process::ChildStdout {
249 #[inline]
250 fn is_terminal(&self) -> bool {
251 false
252 }
253}
254
255#[cfg(target_os = "unknown")]
256impl IsTerminal for std::process::ChildStderr {
257 #[inline]
258 fn is_terminal(&self) -> bool {
259 false
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 #[cfg(not(target_os = "unknown"))]
266 use super::IsTerminal;
267
268 #[test]
269 #[cfg(windows)]
270 fn stdin() {
271 assert_eq!(
272 atty::is(atty::Stream::Stdin),
273 std::io::stdin().is_terminal()
274 )
275 }
276
277 #[test]
278 #[cfg(windows)]
279 fn stdout() {
280 assert_eq!(
281 atty::is(atty::Stream::Stdout),
282 std::io::stdout().is_terminal()
283 )
284 }
285
286 #[test]
287 #[cfg(windows)]
288 fn stderr() {
289 assert_eq!(
290 atty::is(atty::Stream::Stderr),
291 std::io::stderr().is_terminal()
292 )
293 }
294
295 #[test]
296 #[cfg(any(unix, target_os = "wasi"))]
297 fn stdin() {
298 assert_eq!(
299 atty::is(atty::Stream::Stdin),
300 rustix::stdio::stdin().is_terminal()
301 )
302 }
303
304 #[test]
305 #[cfg(any(unix, target_os = "wasi"))]
306 fn stdout() {
307 assert_eq!(
308 atty::is(atty::Stream::Stdout),
309 rustix::stdio::stdout().is_terminal()
310 )
311 }
312
313 #[test]
314 #[cfg(any(unix, target_os = "wasi"))]
315 fn stderr() {
316 assert_eq!(
317 atty::is(atty::Stream::Stderr),
318 rustix::stdio::stderr().is_terminal()
319 )
320 }
321
322 #[test]
323 #[cfg(any(unix, target_os = "wasi"))]
324 fn stdin_vs_libc() {
325 unsafe {
326 assert_eq!(
327 libc::isatty(libc::STDIN_FILENO) != 0,
328 rustix::stdio::stdin().is_terminal()
329 )
330 }
331 }
332
333 #[test]
334 #[cfg(any(unix, target_os = "wasi"))]
335 fn stdout_vs_libc() {
336 unsafe {
337 assert_eq!(
338 libc::isatty(libc::STDOUT_FILENO) != 0,
339 rustix::stdio::stdout().is_terminal()
340 )
341 }
342 }
343
344 #[test]
345 #[cfg(any(unix, target_os = "wasi"))]
346 fn stderr_vs_libc() {
347 unsafe {
348 assert_eq!(
349 libc::isatty(libc::STDERR_FILENO) != 0,
350 rustix::stdio::stderr().is_terminal()
351 )
352 }
353 }
354
355 #[test]
357 #[cfg(windows)]
358 fn msys_tty_on_path_length() {
359 use std::{fs::File, os::windows::io::AsRawHandle};
360 use windows_sys::Win32::Foundation::MAX_PATH;
361
362 let dir = tempfile::tempdir().expect("Unable to create temporary directory");
363 let file_path = dir.path().join("ten_chars_".repeat(25));
364 assert!(file_path.to_string_lossy().len() > MAX_PATH as usize);
366 let file = File::create(file_path).expect("Unable to create file");
367
368 assert!(!unsafe { crate::msys_tty_on(file.as_raw_handle()) });
369 }
370}