From 198462d721f712e21ba805b248eaba4b485a247f Mon Sep 17 00:00:00 2001 From: Alexander Sergeev Date: Sun, 21 Jan 2024 23:39:53 +0300 Subject: [PATCH] Support of reading the `is_tty` status of `stdin` --- src/term.rs | 103 ++++++++++++++++++++++++++++++---------- src/unix_term.rs | 54 +++++++++++---------- src/windows_term/mod.rs | 74 +++++++++++------------------ 3 files changed, 134 insertions(+), 97 deletions(-) diff --git a/src/term.rs b/src/term.rs index 44e94055..786ab5ea 100644 --- a/src/term.rs +++ b/src/term.rs @@ -66,7 +66,13 @@ impl<'a> TermFeatures<'a> { /// Check if this is a real user attended terminal (`isatty`) #[inline] pub fn is_attended(&self) -> bool { - is_a_terminal(self.0) + self.0.tty.is_tty + } + + /// Check if input is a terminal + #[inline] + pub fn is_input_a_tty(&self) -> bool { + self.0.tty.input } /// Check if colors are supported by this terminal. @@ -82,16 +88,10 @@ impl<'a> TermFeatures<'a> { /// /// This is sometimes useful to disable features that are known to not /// work on msys terminals or require special handling. + #[cfg(windows)] #[inline] pub fn is_msys_tty(&self) -> bool { - #[cfg(windows)] - { - msys_tty_on(self.0) - } - #[cfg(not(windows))] - { - false - } + self.0.tty.msys } /// Check if this terminal wants emojis. @@ -121,6 +121,62 @@ impl<'a> TermFeatures<'a> { } } +pub(crate) enum Stream { + Stdin, + Stdout, + Stderr, +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct Tty { + /// Only considers output streams. Left for compatibility with `Term.is_tty` + pub(crate) is_tty: bool, + input: bool, + output: bool, + error: bool, + #[cfg(windows)] + pub(crate) msys: bool, +} + +impl From<&TermTarget> for Tty { + fn from(target: &TermTarget) -> Self { + let mut tty = Self { + is_tty: false, + input: is_a_tty(Stream::Stdin), + output: is_a_tty(Stream::Stdout), + error: is_a_tty(Stream::Stderr), + #[cfg(windows)] + msys: msys_tty_on(&target), + }; + tty.is_tty = match target { + TermTarget::Stdout => tty.output, + TermTarget::Stderr => tty.error, + #[cfg(unix)] + TermTarget::ReadWritePair(_) => false, + }; + + #[cfg(windows)] + { + if !tty.is_tty { + // At this point, we *could* have a false negative. We can determine that + // this is true negative if we can detect the presence of a console on + // any of the other streams. If another stream has a console, then we know + // we're in a Windows console and can therefore trust the negative. + tty.is_tty = if match target { + TermTarget::Stdout => tty.input || tty.error, + TermTarget::Stderr => tty.input || tty.output, + } { + false + } else { + tty.msys + } + } + } + + tty + } +} + /// Abstraction around a terminal. /// /// A terminal can be cloned. If a buffer is used it's shared across all @@ -128,21 +184,16 @@ impl<'a> TermFeatures<'a> { #[derive(Clone, Debug)] pub struct Term { inner: Arc, - pub(crate) is_msys_tty: bool, - pub(crate) is_tty: bool, + pub(crate) tty: Tty, } impl Term { fn with_inner(inner: TermInner) -> Term { - let mut term = Term { + let tty = Tty::from(&inner.target); + Term { inner: Arc::new(inner), - is_msys_tty: false, - is_tty: false, - }; - - term.is_msys_tty = term.features().is_msys_tty(); - term.is_tty = term.features().is_attended(); - term + tty, + } } /// Return a new unbuffered terminal. @@ -265,7 +316,7 @@ impl Term { /// or complete key chord is entered. If the terminal is not user attended /// the return value will be an error. pub fn read_char(&self) -> io::Result { - if !self.is_tty { + if !self.tty.is_tty { return Err(io::Error::new( io::ErrorKind::NotConnected, "Not a terminal", @@ -289,7 +340,7 @@ impl Term { /// This does not echo anything. If the terminal is not user attended /// the return value will always be the unknown key. pub fn read_key(&self) -> io::Result { - if !self.is_tty { + if !self.tty.is_tty { Ok(Key::Unknown) } else { read_single_key(false) @@ -297,7 +348,7 @@ impl Term { } pub fn read_key_raw(&self) -> io::Result { - if !self.is_tty { + if !self.tty.is_tty { Ok(Key::Unknown) } else { read_single_key(true) @@ -319,7 +370,7 @@ impl Term { /// This does not include the trailing newline. If the terminal is not /// user attended the return value will always be an empty string. pub fn read_line_initial_text(&self, initial: &str) -> io::Result { - if !self.is_tty { + if !self.tty.is_tty { return Ok("".into()); } *self.inner.prompt.write().unwrap() = initial.to_string(); @@ -369,7 +420,7 @@ impl Term { /// also switches the terminal into a different mode where not all /// characters might be accepted. pub fn read_secure_line(&self) -> io::Result { - if !self.is_tty { + if !self.tty.is_tty { return Ok("".into()); } match read_secure() { @@ -400,7 +451,7 @@ impl Term { /// Check if the terminal is indeed a terminal. #[inline] pub fn is_term(&self) -> bool { - self.is_tty + self.tty.is_tty } /// Check for common terminal features. @@ -509,7 +560,7 @@ impl Term { /// Set the terminal title. pub fn set_title(&self, title: T) { - if !self.is_tty { + if !self.tty.is_tty { return; } set_title(title); diff --git a/src/unix_term.rs b/src/unix_term.rs index 271709f2..b1702296 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -8,7 +8,7 @@ use std::os::unix::io::AsRawFd; use std::str; use crate::kb::Key; -use crate::term::Term; +use crate::term::{Stream, Term}; pub use crate::common_term::*; @@ -19,6 +19,16 @@ pub fn is_a_terminal(out: &Term) -> bool { unsafe { libc::isatty(out.as_raw_fd()) != 0 } } +#[inline] +pub fn is_a_tty(stream: Stream) -> bool { + let fd = match stream { + Stream::Stdin => libc::STDIN_FILENO, + Stream::Stdout => libc::STDOUT_FILENO, + Stream::Stderr => libc::STDERR_FILENO, + }; + unsafe { libc::isatty(fd) == 1 } +} + pub fn is_a_color_terminal(out: &Term) -> bool { if !is_a_terminal(out) { return false; @@ -65,19 +75,17 @@ pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { pub fn read_secure() -> io::Result { let f_tty; - let fd = unsafe { - if libc::isatty(libc::STDIN_FILENO) == 1 { - f_tty = None; - libc::STDIN_FILENO - } else { - let f = fs::OpenOptions::new() - .read(true) - .write(true) - .open("/dev/tty")?; - let fd = f.as_raw_fd(); - f_tty = Some(BufReader::new(f)); - fd - } + let fd = if is_a_tty(Stream::Stdin) { + f_tty = None; + libc::STDIN_FILENO + } else { + let f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + let fd = f.as_raw_fd(); + f_tty = Some(BufReader::new(f)); + fd }; let mut termios = mem::MaybeUninit::uninit(); @@ -296,16 +304,14 @@ fn read_single_key_impl(fd: i32) -> Result { pub fn read_single_key(ctrlc_key: bool) -> io::Result { let tty_f; - let fd = unsafe { - if libc::isatty(libc::STDIN_FILENO) == 1 { - libc::STDIN_FILENO - } else { - tty_f = fs::OpenOptions::new() - .read(true) - .write(true) - .open("/dev/tty")?; - tty_f.as_raw_fd() - } + let fd = if is_a_tty(Stream::Stdin) { + libc::STDIN_FILENO + } else { + tty_f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + tty_f.as_raw_fd() }; let mut termios = core::mem::MaybeUninit::uninit(); c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; diff --git a/src/windows_term/mod.rs b/src/windows_term/mod.rs index 173f3ef5..9b123b91 100644 --- a/src/windows_term/mod.rs +++ b/src/windows_term/mod.rs @@ -19,13 +19,13 @@ use windows_sys::Win32::System::Console::{ GetConsoleScreenBufferInfo, GetNumberOfConsoleInputEvents, GetStdHandle, ReadConsoleInputW, SetConsoleCursorInfo, SetConsoleCursorPosition, SetConsoleMode, SetConsoleTitleW, CONSOLE_CURSOR_INFO, CONSOLE_SCREEN_BUFFER_INFO, COORD, INPUT_RECORD, KEY_EVENT, - KEY_EVENT_RECORD, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, + KEY_EVENT_RECORD, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, }; use windows_sys::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY; use crate::common_term; use crate::kb::Key; -use crate::term::{Term, TermTarget}; +use crate::term::{Stream, Term, TermTarget}; #[cfg(feature = "windows-console-colors")] mod colors; @@ -41,34 +41,22 @@ pub fn as_handle(term: &Term) -> HANDLE { term.as_raw_handle() as HANDLE } -pub fn is_a_terminal(out: &Term) -> bool { - let (fd, others) = match out.target() { - TermTarget::Stdout => (STD_OUTPUT_HANDLE, [STD_INPUT_HANDLE, STD_ERROR_HANDLE]), - TermTarget::Stderr => (STD_ERROR_HANDLE, [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE]), +pub fn is_a_tty(stream: Stream) -> bool { + let fd = match stream { + Stream::Stdin => STD_INPUT_HANDLE, + Stream::Stdout => STD_OUTPUT_HANDLE, + Stream::Stderr => STD_ERROR_HANDLE, }; - if unsafe { console_on_any(&[fd]) } { - // False positives aren't possible. If we got a console then - // we definitely have a tty on stdin. - return true; - } - - // At this point, we *could* have a false negative. We can determine that - // this is true negative if we can detect the presence of a console on - // any of the other streams. If another stream has a console, then we know - // we're in a Windows console and can therefore trust the negative. - if unsafe { console_on_any(&others) } { - return false; - } - - msys_tty_on(out) + let mut out = 0; + unsafe { GetConsoleMode(GetStdHandle(fd), &mut out) != 0 } } pub fn is_a_color_terminal(out: &Term) -> bool { - if !is_a_terminal(out) { + if !out.is_term() { return false; } - if msys_tty_on(out) { + if out.tty.msys { return match env::var("TERM") { Ok(term) => term != "dumb", Err(_) => true, @@ -95,17 +83,6 @@ fn enable_ansi_on(out: &Term) -> bool { } } -unsafe fn console_on_any(fds: &[STD_HANDLE]) -> bool { - for &fd in fds { - let mut out = 0; - let handle = GetStdHandle(fd); - if GetConsoleMode(handle, &mut out) != 0 { - return true; - } - } - false -} - pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { use windows_sys::Win32::System::Console::SMALL_RECT; @@ -141,7 +118,7 @@ pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { } pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::move_cursor_to(out, x, y); } if let Some((hand, _)) = get_console_screen_buffer_info(as_handle(out)) { @@ -159,7 +136,7 @@ pub fn move_cursor_to(out: &Term, x: usize, y: usize) -> io::Result<()> { } pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::move_cursor_up(out, n); } @@ -170,7 +147,7 @@ pub fn move_cursor_up(out: &Term, n: usize) -> io::Result<()> { } pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::move_cursor_down(out, n); } @@ -181,7 +158,7 @@ pub fn move_cursor_down(out: &Term, n: usize) -> io::Result<()> { } pub fn move_cursor_left(out: &Term, n: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::move_cursor_left(out, n); } @@ -196,7 +173,7 @@ pub fn move_cursor_left(out: &Term, n: usize) -> io::Result<()> { } pub fn move_cursor_right(out: &Term, n: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::move_cursor_right(out, n); } @@ -211,7 +188,7 @@ pub fn move_cursor_right(out: &Term, n: usize) -> io::Result<()> { } pub fn clear_line(out: &Term) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::clear_line(out); } if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { @@ -231,7 +208,7 @@ pub fn clear_line(out: &Term) -> io::Result<()> { } pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::clear_chars(out, n); } if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { @@ -251,7 +228,7 @@ pub fn clear_chars(out: &Term, n: usize) -> io::Result<()> { } pub fn clear_screen(out: &Term) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::clear_screen(out); } if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { @@ -268,7 +245,7 @@ pub fn clear_screen(out: &Term) -> io::Result<()> { } pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::clear_to_end_of_screen(out); } if let Some((hand, csbi)) = get_console_screen_buffer_info(as_handle(out)) { @@ -289,7 +266,7 @@ pub fn clear_to_end_of_screen(out: &Term) -> io::Result<()> { } pub fn show_cursor(out: &Term) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::show_cursor(out); } if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) { @@ -302,7 +279,7 @@ pub fn show_cursor(out: &Term) -> io::Result<()> { } pub fn hide_cursor(out: &Term) -> io::Result<()> { - if out.is_msys_tty { + if out.tty.msys { return common_term::hide_cursor(out); } if let Some((hand, mut cci)) = get_console_cursor_info(as_handle(out)) { @@ -509,9 +486,12 @@ pub fn wants_emoji() -> bool { } /// Returns true if there is an MSYS tty on the given handle. -pub fn msys_tty_on(term: &Term) -> bool { - let handle = term.as_raw_handle(); +pub fn msys_tty_on(target: &TermTarget) -> bool { unsafe { + let handle = match target { + TermTarget::Stdout => GetStdHandle(STD_OUTPUT_HANDLE), + TermTarget::Stderr => GetStdHandle(STD_ERROR_HANDLE), + }; // Check whether the Windows 10 native pty is enabled { let mut out = MaybeUninit::uninit();