Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of reading the is_tty status of stdin #200

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
103 changes: 77 additions & 26 deletions src/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -121,28 +121,79 @@ 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
/// clones which means it largely acts as a handle.
#[derive(Clone, Debug)]
pub struct Term {
inner: Arc<TermInner>,
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.
Expand Down Expand Up @@ -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<char> {
if !self.is_tty {
if !self.tty.is_tty {
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"Not a terminal",
Expand All @@ -289,15 +340,15 @@ 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<Key> {
if !self.is_tty {
if !self.tty.is_tty {
Ok(Key::Unknown)
} else {
read_single_key(false)
}
}

pub fn read_key_raw(&self) -> io::Result<Key> {
if !self.is_tty {
if !self.tty.is_tty {
Ok(Key::Unknown)
} else {
read_single_key(true)
Expand All @@ -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<String> {
if !self.is_tty {
if !self.tty.is_tty {
return Ok("".into());
}
*self.inner.prompt.write().unwrap() = initial.to_string();
Expand Down Expand Up @@ -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<String> {
if !self.is_tty {
if !self.tty.is_tty {
return Ok("".into());
}
match read_secure() {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -509,7 +560,7 @@ impl Term {

/// Set the terminal title.
pub fn set_title<T: Display>(&self, title: T) {
if !self.is_tty {
if !self.tty.is_tty {
return;
}
set_title(title);
Expand Down
54 changes: 30 additions & 24 deletions src/unix_term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand All @@ -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;
Expand Down Expand Up @@ -65,19 +75,17 @@ pub fn terminal_size(out: &Term) -> Option<(u16, u16)> {

pub fn read_secure() -> io::Result<String> {
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();
Expand Down Expand Up @@ -296,16 +304,14 @@ fn read_single_key_impl(fd: i32) -> Result<Key, io::Error> {

pub fn read_single_key(ctrlc_key: bool) -> io::Result<Key> {
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()) })?;
Expand Down