From f3a5686a153202c30b11b9778b0d5cfae9607318 Mon Sep 17 00:00:00 2001 From: Jun Wu Date: Tue, 23 Feb 2021 22:31:15 -0800 Subject: [PATCH] io: add IsTty API Summary: Add a new API to test if a stream is a tty. This is needed to replace the Python `fin`, `fout` etc. to Rust objects, because the Python land requires the `istty` API. It is also useful for properly implement color detection in the pure Rust land. Reviewed By: sfilipco Differential Revision: D26612480 fbshipit-source-id: 5cf79447b1d74e0031a954788db342afd48dc288 --- eden/scm/lib/cpython-ext/Cargo.toml | 1 + eden/scm/lib/cpython-ext/src/io.rs | 12 ++++ eden/scm/lib/io/Cargo.toml | 1 + eden/scm/lib/io/src/impls.rs | 99 +++++++++++++++++++++++++++++ eden/scm/lib/io/src/lib.rs | 28 ++++++-- 5 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 eden/scm/lib/io/src/impls.rs diff --git a/eden/scm/lib/cpython-ext/Cargo.toml b/eden/scm/lib/cpython-ext/Cargo.toml index 6787bdf102..d919dae5f6 100644 --- a/eden/scm/lib/cpython-ext/Cargo.toml +++ b/eden/scm/lib/cpython-ext/Cargo.toml @@ -12,6 +12,7 @@ python3 = ["python3-sys", "cpython/python3-sys"] anyhow = "1.0.20" cpython = { version = "0.5", default-features = false } encoding = { path = "../encoding" } +io = { path = "../io" } lazy_static = "1" libc = "0.2" parking_lot = "0.9" diff --git a/eden/scm/lib/cpython-ext/src/io.rs b/eden/scm/lib/cpython-ext/src/io.rs index 0678572cfb..87562fa21f 100644 --- a/eden/scm/lib/cpython-ext/src/io.rs +++ b/eden/scm/lib/cpython-ext/src/io.rs @@ -61,6 +61,18 @@ impl io::Write for WrappedIO { } } +impl ::io::IsTty for WrappedIO { + fn is_tty(&self) -> bool { + (|| -> PyResult { + let gil = Python::acquire_gil(); + let py = gil.python(); + let result = self.0.call_method(py, "isatty", NoArgs, None)?; + result.extract(py) + })() + .unwrap_or(false) + } +} + /// Convert a Python `IOError` to Rust `io::Error`. fn convert_ioerr(mut pyerr: PyErr) -> io::Error { let gil = Python::acquire_gil(); diff --git a/eden/scm/lib/io/Cargo.toml b/eden/scm/lib/io/Cargo.toml index 4cab440a82..1e352e0ec9 100644 --- a/eden/scm/lib/io/Cargo.toml +++ b/eden/scm/lib/io/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Facebook Source Control Team "] edition = "2018" [dependencies] +atty = "0.2" configparser = { path = "../configparser" } once_cell = "1" parking_lot = "0.10" diff --git a/eden/scm/lib/io/src/impls.rs b/eden/scm/lib/io/src/impls.rs new file mode 100644 index 0000000000..0b5f18fe81 --- /dev/null +++ b/eden/scm/lib/io/src/impls.rs @@ -0,0 +1,99 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2. + */ + +use crate::IsTty; +use std::io::Cursor; +use std::sync::Weak; + +impl IsTty for std::io::Stdin { + fn is_tty(&self) -> bool { + atty::is(atty::Stream::Stdin) + } +} + +impl IsTty for std::io::Stdout { + fn is_tty(&self) -> bool { + atty::is(atty::Stream::Stdout) + } +} + +impl IsTty for std::io::Stderr { + fn is_tty(&self) -> bool { + atty::is(atty::Stream::Stderr) + } +} + +impl IsTty for Vec { + fn is_tty(&self) -> bool { + false + } +} + +impl<'a> IsTty for &'a [u8] { + fn is_tty(&self) -> bool { + false + } +} + +impl IsTty for Cursor { + fn is_tty(&self) -> bool { + false + } +} + +impl IsTty for crate::IOOutput { + fn is_tty(&self) -> bool { + let inner = match Weak::upgrade(&self.0) { + Some(inner) => inner, + None => return false, + }; + let inner = inner.lock(); + inner.output.is_tty() + } +} + +impl IsTty for crate::IOError { + fn is_tty(&self) -> bool { + let inner = match Weak::upgrade(&self.0) { + Some(inner) => inner, + None => return false, + }; + let inner = inner.lock(); + if let Some(error) = inner.error.as_ref() { + error.is_tty() + } else { + false + } + } +} + +pub(crate) struct PipeWriterWithTty { + inner: pipe::PipeWriter, + is_tty: bool, +} + +impl std::io::Write for PipeWriterWithTty { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.inner.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +impl IsTty for PipeWriterWithTty { + fn is_tty(&self) -> bool { + self.is_tty + } +} + +impl PipeWriterWithTty { + pub fn new(inner: pipe::PipeWriter, is_tty: bool) -> Self { + Self { inner, is_tty } + } +} diff --git a/eden/scm/lib/io/src/lib.rs b/eden/scm/lib/io/src/lib.rs index 68ac517f32..8952506e3c 100644 --- a/eden/scm/lib/io/src/lib.rs +++ b/eden/scm/lib/io/src/lib.rs @@ -56,7 +56,11 @@ struct Inner { /// to obtain the "main" `IO`. static MAIN_IO_REF: Lazy>>>> = Lazy::new(Default::default); -pub trait Read: io::Read + Any + Send + Sync { +pub trait IsTty { + fn is_tty(&self) -> bool; +} + +pub trait Read: io::Read + IsTty + Any + Send + Sync { fn as_any(&self) -> &dyn Any; fn type_name(&self) -> &'static str { @@ -64,7 +68,7 @@ pub trait Read: io::Read + Any + Send + Sync { } } -pub trait Write: io::Write + Any + Send + Sync { +pub trait Write: io::Write + IsTty + Any + Send + Sync { fn as_any(&self) -> &dyn Any; fn type_name(&self) -> &'static str { @@ -72,18 +76,20 @@ pub trait Write: io::Write + Any + Send + Sync { } } -impl Read for T { +impl Read for T { fn as_any(&self) -> &dyn Any { self } } -impl Write for T { +impl Write for T { fn as_any(&self) -> &dyn Any { self } } +mod impls; + // Write to error. impl io::Write for IOError { fn write(&mut self, buf: &[u8]) -> io::Result { @@ -336,10 +342,18 @@ impl IO { let (err_read, err_write) = pipe(); let (prg_read, prg_write) = pipe(); + use impls::PipeWriterWithTty; + let out_is_tty = inner.output.is_tty(); + let err_is_tty = inner + .error + .as_ref() + .map(|e| e.is_tty()) + .unwrap_or_else(|| out_is_tty); + inner.flush()?; - inner.output = Box::new(out_write); - inner.error = Some(Box::new(err_write)); - inner.progress = Some(Box::new(prg_write)); + inner.output = Box::new(PipeWriterWithTty::new(out_write, out_is_tty)); + inner.error = Some(Box::new(PipeWriterWithTty::new(err_write, err_is_tty))); + inner.progress = Some(Box::new(PipeWriterWithTty::new(prg_write, false))); inner.pager_handle = Some(spawn(|| { pager