diff --git a/Cargo.toml b/Cargo.toml index e1cbd03..1612bf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,9 +25,12 @@ version = "0.59.0" optional = true features = [ "Win32_Foundation", + "Win32_Globalization", "Win32_System_Console", + "Win32_System_Threading", ] + [features] default = ["f64", "term"] f64 = [] diff --git a/src/lib.rs b/src/lib.rs index ae1ad14..1c4feca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,13 +68,12 @@ feature disabled, [on Docs.rs](https://docs.rs/prettypretty/latest/prettypretty/ //! for doing so. //! * The optional [`term`] module supports just enough **terminal I/O** to //! query a terminal for the current color theme, which is required for -//! instantiating a `Translator`. Notably, the Unix-only +//! instantiating a `Translator`. Notably, //! [`Terminal`](crate::term::Terminal) type configures the terminal to use //! raw mode and to time out reads. Meanwhile, //! [`VtScanner`](crate::term::VtScanner) implements the complete state -//! machine for **parsing ANSI escape sequences**. Applications that require -//! support for Windows or async I/O should bring their own, more -//! fully-featured terminal crate. +//! machine for **parsing ANSI escape sequences**. Both Unix and Windows are +//! supported but Windows support is largely untested. #![cfg_attr( feature = "gamut", doc = " * The optional [`gamut`] and [`spectrum`] submodules enable the traversal @@ -144,8 +143,8 @@ feature disabled, [on Docs.rs](https://docs.rs/prettypretty/latest/prettypretty/ //! ### 2. Adjust Your Styles //! //! Second, determine the terminal's current color theme with -//! [`Theme::query_terminal`](trans::Theme::query_terminal) (sorry, but -//! Unix-only for now) and `stdout`'s color support with +//! [`Theme::query_terminal`](trans::Theme::query_terminal) +//! and `stdout`'s color support with //! [`Fidelity::from_environment`](style::Fidelity::from_environment). //! //! ``` diff --git a/src/term/mod.rs b/src/term/mod.rs index 4a5b159..34f8d94 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -13,18 +13,18 @@ //! //! When combined with [`Theme`](crate::trans::Theme) and //! [`ThemeEntry`](crate::trans::ThemeEntry), querying the terminal for its -//! color theme looks straightforward. Let's walk through one possible solution. +//! color theme becomes fairly straightforward. Let's walk through one possible +//! solution. //! //! # Examples //! -//! ## 1. Taking Care of Not-Unix +//! ## 1. Scaffolding //! -//! First, since terminal support is limited to Unix only, lets set up two -//! versions of the same function, one for other platforms and one for Unix. -//! Since we are assembling a color theme by interacting with the terminal, -//! `std::io::Result` seems like a good result type, with -//! [`Theme`](crate::trans::Theme) collecting the two default and 16 ANSI colors -//! belonging to the, ahem, theme: +//! Windows support is largely untested. So for now, we run the example code on +//! Unix only. Prettypretty has a dedicated type for a color +//! [`Theme`](crate::trans::Theme), which collects the two default and 16 ANSI +//! colors. Since we are interacting with the terminal to generate an instance, +//! `std::io::Result` seems like a good result type for our function. //! //! ``` //! # use std::io::{ErrorKind, Result}; @@ -254,11 +254,11 @@ //! //! # Background //! -//! Integrating terminal I/O is trivial, as long as an application does not need -//! to read terminal input: The application simply writes text and ANSI escape -//! sequences to style the text to standard output or error. For just that -//! reason, the display of [`Style`](crate::style::Style) is the ANSI escape -//! sequence that changes the terminal to use that style. +//! Integrating terminal I/O is trivial on Unix, as long as an application does +//! not need to read terminal input: The application simply writes text and ANSI +//! escape sequences to style the text to standard output or error. For just +//! that reason, the display of [`Style`](crate::style::Style) is the ANSI +//! escape sequence that changes the terminal to use that style. //! //! //! ## 1. Gnarly Input @@ -291,10 +291,9 @@ //! a lean but functional terminal integration layer. //! //! However, they won't meet all application needs. Notably, if your application -//! requires Windows support or async I/O, please consider using a more -//! fully-featured terminal crate such as -//! [Crossterm](https://github.com/crossterm-rs/crossterm). For the same reason, -//! this module is option and requires the `term` feature. +//! requires async I/O, please consider using a more fully-featured terminal +//! crate such as [Crossterm](https://github.com/crossterm-rs/crossterm). For +//! the same reason, this module is optional and requires the `term` feature. //! //! //! ## 2. Ways to Time Out Reads @@ -336,6 +335,37 @@ //! A third pitfall is that Rust turns read operations that return zero bytes //! into end-of-file errors. This module helps to mitigate those errors, but an //! application may need to detect them as well. +//! +//! +//! ## 3. Windows +//! +//! Starting with Windows 10 TH2 (v1511), Windows also supports ANSI escape +//! sequences. While applications currently need to explicitly enable virtual +//! terminal processing, they also are the preferred means for interacting with +//! the console host moving forward, and several console functions that provide +//! equivalent functionality have been deprecated. +//! +//! The Windows console host provides much of the same functionality as Unix +//! pseudo-terminals with a completely different API. Two differences stick out: +//! +//! 1. To interact with the terminal independent of redirected streams, a Unix +//! application connects to one endpoint by opening `/dev/tty`. Windows has +//! independent abstractions for input and output, with the equivalent code +//! on Windows opening `CONIN$` and `CONOUT$`. As a direct consequence, +//! configuring the console on Windows also requires reading and writing two +//! modes. +//! 2. The Windows API duplicates a large number of functions, with the name +//! ending either in `A` or `W`, e.g., `ReadConsoleA` vs `ReadConsoleW`. The +//! former represent strings with 8-bit characters, whose encoding is +//! determined by the current "code page". The latter represent strings as +//! UTF-16. Thankfully, there is a code page for UTF-8, but that does +//! require reading and writing the code page for input and output during +//! the initial configuration. +//! +//! Meanwhile, timing out reads is easy. `WaitForSingleObject` takes only two +//! arguments, the handle for console input and the timeout, and gets the job +//! done. Potentially adding a future waker is easy as well: Just switch to +//! `WaitForMultipleObjects`. mod escape; mod render; diff --git a/src/term/sys/unix.rs b/src/term/sys/unix.rs index 40ec97c..8bd8f1c 100644 --- a/src/term/sys/unix.rs +++ b/src/term/sys/unix.rs @@ -6,6 +6,7 @@ //! same reason, [`TerminalConfig`], [`TerminalReader`], and [`TerminalWriter`] //! must not be directly exposed to application code. +use std::ffi::c_void; use std::fs::OpenOptions; use std::io::{Read, Result, Write}; use std::os::fd::{AsRawFd, OwnedFd}; @@ -76,11 +77,13 @@ impl Device { } /// Get process group ID. + #[inline] pub fn pid(&self) -> Result { unsafe { libc::tcgetsid(self.fd.as_raw_fd()) }.into_result() } /// Get a handle for the device. + #[inline] pub fn handle(&self) -> DeviceHandle { DeviceHandle(self.fd.as_raw_fd()) } @@ -144,12 +147,14 @@ impl Termios { } impl AsRef for Termios { + #[inline] fn as_ref(&self) -> &libc::termios { &self.inner } } impl AsMut for Termios { + #[inline] fn as_mut(&mut self) -> &mut libc::termios { &mut self.inner } @@ -158,12 +163,9 @@ impl AsMut for Termios { impl std::fmt::Debug for Termios { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Determine enabled flags - let mut flags = String::new(); + let mut flags = Vec::new(); let mut append = |s| { - if !flags.is_empty() { - flags.push_str(", "); - } - flags.push_str(s); + flags.push(s); }; for (name, value) in [ @@ -207,8 +209,16 @@ impl std::fmt::Debug for Termios { } } + struct Flags(Vec<&'static str>); + + impl std::fmt::Debug for Flags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } + } + f.debug_struct("Termios") - .field("flags", &flags) + .field("flags", &Flags(flags)) .field("vmin", &self.inner.c_cc[libc::VMIN]) .field("vtime", &self.inner.c_cc[libc::VTIME]) .finish() @@ -282,7 +292,7 @@ impl Config { let mut wrapper = attributes.clone(); let inner = wrapper.as_mut(); - match options.mode { + match options.mode() { Mode::Rare => { inner.c_lflag &= !(libc::ECHO | libc::ICANON); } @@ -292,7 +302,7 @@ impl Config { } inner.c_cc[libc::VMIN] = 0; - inner.c_cc[libc::VTIME] = options.timeout.get(); + inner.c_cc[libc::VTIME] = options.timeout().get(); wrapper } @@ -319,7 +329,10 @@ pub(crate) struct Reader { impl Reader { /// Create a new reader with a raw file descriptor. - pub fn new(handle: RawHandle) -> Self { + /// + /// The second parameter is only used on Windows. Adding it for Unix too + /// simplifies the code instantiating the reader. + pub fn new(handle: RawHandle, _: u32) -> Self { Self { handle } } } @@ -329,7 +342,7 @@ impl Read for Reader { unsafe { libc::read( self.handle, - buf.as_mut_ptr() as *mut libc::c_void, + buf.as_mut_ptr() as *mut c_void, buf.len() as libc::size_t, ) } @@ -362,7 +375,7 @@ impl Write for Writer { unsafe { libc::write( self.handle, - buf.as_ptr() as *mut libc::c_void, + buf.as_ptr() as *const c_void, buf.len() as libc::size_t, ) } diff --git a/src/term/sys/windows.rs b/src/term/sys/windows.rs index 706ef73..a353f8c 100644 --- a/src/term/sys/windows.rs +++ b/src/term/sys/windows.rs @@ -6,12 +6,16 @@ //! For that same reason, [`Config`], [`Reader`], and [`Writer`] must not be //! directly exposed to application code. +use std::ffi::c_void; use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write}; use std::os::windows::io::{AsRawHandle, OwnedHandle}; use std::ptr::from_mut; -use windows_sys::Win32::System::Console; +use windows_sys::Win32::Foundation; +use windows_sys::Win32::Globalization; +use windows_sys::Win32::System::Console::{self, CONSOLE_MODE as ConsoleMode}; +use windows_sys::Win32::System::Threading; use super::RawHandle; use crate::term::{Mode, Options}; @@ -22,11 +26,11 @@ trait IntoResult { fn into_result(self) -> Result<()>; } -impl IntoResult for i32 { +impl IntoResult for u32 { #[inline] - fn into_result(self) -> Result<()> { + fn into_result(self) -> Result { if self != 0 { - Ok(()) + Ok(self) } else { Err(Error::last_os_error()) } @@ -66,6 +70,7 @@ impl Device { } /// Get the process group ID. + #[inline] pub fn pid(&self) -> Result { Err(ErrorKind::Unsupported.into()) } @@ -76,6 +81,7 @@ impl Device { /// /// The caller must ensure that the returned device handle does not outlive /// this device. + #[inline] pub fn handle(&self) -> DeviceHandle { DeviceHandle { input: self.input.as_raw_handle(), @@ -111,17 +117,30 @@ impl DeviceHandle { #[derive(Debug)] pub(crate) struct Config { handle: DeviceHandle, - input_mode: u32, - output_mode: u32, + input_mode: ConsoleMode, + input_encoding: u32, + output_mode: ConsoleMode, + output_encoding: u32, } impl Config { /// Create a new terminal configuration. pub fn new(handle: DeviceHandle, options: &Options) -> Result { - // It's safe to exit early because for now we are just reading modes. + // Early exit is safe because we are only reading. let input_mode = Self::read(handle.input())?; + let input_encoding = unsafe { Console::GetConsoleCP() }.into_result()?; let output_mode = Self::read(handle.output())?; + let output_encoding = unsafe { Console::GetConsoleOutputCP() }.into_result()?; + + let this = Self { + handle, + input_mode, + input_encoding, + output_mode, + output_encoding, + }; + // Determine new input and output modes. let mut new_input_mode = input_mode & !Console::ENABLE_ECHO_INPUT & !Console::ENABLE_LINE_INPUT @@ -135,37 +154,43 @@ impl Config { & Console::ENABLE_PROCESSED_OUTPUT & Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; - // If first update fails, nothing was changed. If second update fails, - // we probably should reset first update. - Self::write(handle.input(), new_input_mode)?; - Self::write(handle.output(), new_output_mode)?; + // If the update fails, try to restore old configuration. + this.update(new_input_mode, new_output_mode) + .or_else(|e| this.restore())?; - Ok(Self { - handle, - input_mode, - output_mode, - }) + Ok(this) + } + + fn update(&self, input_mode: ConsoleMode, output_mode: ConsoleMode) -> Result<()> { + // Fail early to limit damage. + Self::write(self.handle.input(), input_mode)?; + unsafe { Console::SetConsoleCP(Globalization::CP_UTF8) }.into_result()?; + Self::write(self.handle.output(), output_mode)?; + unsafe { Console::SetConsoleOutputCP(Globalization::CP_UTF8) }.into_result()?; + Ok(()) } /// Restore the original terminal configuration. pub fn restore(&self) -> Result<()> { // Since we are trying to restore the original terminal modes, we should - // always try to apply both updates, even if one of them fails. + // always try to apply all four updates, even if one of them fails. let result1 = Self::write(self.handle.input(), self.input_mode); - let result2 = Self::write(self.handle.output(), self.output_mode); + let result2 = unsafe { Console::SetConsoleCP(self.input_encoding) }.into_result(); + let result3 = Self::write(self.handle.output(), self.output_mode); + let result4 = unsafe { Console::SetConsoleOutputCP(self.output_encoding) }.into_result(); - result1.and(result2) + result1.and(result2).and(result3).and(result4); } // ------------------------------------------------------------------------------------------------------ - fn read(handle: RawHandle) -> Result { + fn read(handle: RawHandle) -> Result { let mut mode = 0; unsafe { Console::GetConsoleMode(handle, from_mut(&mut mode)) }.into_result()?; Ok(mode) } - fn write(handle: RawHandle, mode: u32) -> Result<()> { + fn write_mode(handle: RawHandle, mode: ConsoleMode) -> Result<()> { unsafe { Console::SetConsoleMode(handle, mode) }.into_result() } } @@ -182,24 +207,40 @@ unsafe impl Send for Config {} /// outlive its handle. #[derive(Debug)] pub(crate) struct Reader { - #[allow(dead_code)] handle: RawHandle, + timeout: u32, } impl Reader { /// Create a new reader with a raw handle. - pub fn new(handle: RawHandle) -> Self { - Self { handle } + pub fn new(handle: RawHandle, timeout: u32) -> Self { + Self { handle, timeout } } } -// WaitForSingleObject -// WaitForMultipleObjects -// ReadConsoleInput - impl Read for Reader { - fn read(&mut self, _: &mut [u8]) -> Result { - Err(ErrorKind::Unsupported.into()) // FIXME! + fn read(&mut self, buf: &mut [u8]) -> Result { + let status = unsafe { Threading::WaitForSingleObject(self.handle, self.timeout) }; + if status == Foundation::WAIT_OBJECT_0 { + let mut did_read: u32 = 0; + unsafe { + Console::ReadConsoleA( + self.handle, + buf.as_mut_ptr() as *mut c_void, + buf.len() as u32, + from_mut(&mut did_read), + null(), + ) + } + .into_result()?; + Ok(did_read as usize) + } else if status == Foundation::WAIT_TIMEOUT { + Ok(0) + } else if status == Foundation::WAIT_FAILED { + Err(Error::last_os_error()) + } else { + Err(ErrorKind::Other.into()) + } } } @@ -225,13 +266,24 @@ impl Writer { } impl Write for Writer { - fn write(&mut self, _: &[u8]) -> Result { - // WriteConsole - - Err(ErrorKind::Unsupported.into()) // FIXME! + fn write(&mut self, buf: &[u8]) -> Result { + let mut did_write: u32 = 0; + unsafe { + Console::WriteConsoleA( + self.handle, + buf.as_ptr() as *const c_void, + buf.len() as u32, + from_mut(&mut did_write), + null(), + ) + } + .into_result()?; + Ok(did_write as usize) } fn flush(&mut self) -> Result<()> { - Err(ErrorKind::Unsupported.into()) // FIXME! + Ok(()) } } + +// ------------------------------------------------------------------------------------------------ diff --git a/src/term/terminal.rs b/src/term/terminal.rs index ed41306..f7db2b6 100644 --- a/src/term/terminal.rs +++ b/src/term/terminal.rs @@ -17,56 +17,148 @@ pub enum Mode { Raw, } -/// Options for configuring the terminal. -#[derive(Clone, Copy, Debug)] -pub struct Options { - /// The terminal mode. - pub mode: Mode, - - /// The read timeout in 0.1s increments. - pub timeout: NonZeroU8, - - /// The size of the read buffer. - /// - /// Parsing ANSI escape sequences requires a lookahead of one byte. Hence, - /// this option must be positive. - pub read_buffer: NonZeroUsize, - - /// The size of the write buffer. - /// - /// If this size is zero, writing to the terminal is effectively unbuffered. - pub write_buffer: usize, +#[derive(Clone, Debug)] +struct OptionData { + logging: bool, + mode: Mode, + timeout: NonZeroU8, + read_buffer_size: NonZeroUsize, + write_buffer_size: usize, } -impl Options { - /// Determine the default timeout. - /// - /// This method determines the default timeout. If this process seems to be - /// running under SSH, this method increases the local default threefold. +impl OptionData { fn default_timeout() -> NonZeroU8 { use std::env::var_os; - let mut timeout = 1; + let mut timeout = 10; if var_os("SSH_CLIENT").is_some() || var_os("SSH_CONNECTION").is_some() { timeout *= 3; } NonZeroU8::new(timeout).unwrap() } - /// Create a new terminal options object with the default values. - pub fn new() -> Self { + #[inline] + fn default_read_buffer_size() -> NonZeroUsize { + NonZeroUsize::new(1_024).unwrap() + } + + #[inline] + fn default_write_buffer_size() -> usize { + 1_024 + } + + fn new() -> Self { Self { + logging: false, mode: Mode::Rare, - timeout: Options::default_timeout(), - read_buffer: NonZeroUsize::new(1_024).unwrap(), - write_buffer: 1_024, + timeout: OptionData::default_timeout(), + read_buffer_size: OptionData::default_read_buffer_size(), + write_buffer_size: OptionData::default_write_buffer_size(), + } + } +} + +pub struct OptionBuilder { + inner: OptionData, +} + +impl OptionBuilder { + /// Enable/disable debug logging. + #[inline] + pub fn logging(&mut self, logging: bool) -> &mut Self { + self.inner.logging = logging; + self + } + + /// Set the mode. + #[inline] + pub fn mode(&mut self, mode: Mode) -> &mut Self { + self.inner.mode = mode; + self + } + + /// Set the timeout. + #[inline] + pub fn timeout(&mut self, timeout: u8) -> &mut Self { + self.inner.timeout = NonZeroU8::new(timeout).expect("timeout is positive"); + self + } + + /// Set the read buffer size. + #[inline] + pub fn read_buffer_size(&mut self, size: usize) -> &mut Self { + self.inner.read_buffer_size = + NonZeroUsize::new(size).expect("read buffer size is positive"); + self + } + + /// Set the write buffer size. + #[inline] + pub fn write_buffer_size(&mut self, size: usize) -> &mut Self { + self.inner.write_buffer_size = size; + self + } + + /// Build the options. + #[inline] + pub fn build(&self) -> Options { + Options { + inner: self.inner.clone(), } } } +/// Options for configuring the terminal. +#[derive(Clone, Debug)] +pub struct Options { + inner: OptionData, +} + +impl Options { + /// Get a new option builder with the defaults. + #[inline] + pub fn builder() -> OptionBuilder { + OptionBuilder { + inner: OptionData::new(), + } + } + + /// Determine whether tracing is enabled. + #[inline] + pub fn logging(&self) -> bool { + self.inner.logging + } + + /// Get the mode. + #[inline] + pub fn mode(&self) -> Mode { + self.inner.mode + } + + /// Get the timeout. + #[inline] + pub fn timeout(&self) -> NonZeroU8 { + self.inner.timeout + } + + /// Get the read buffer size. + #[inline] + pub fn read_buffer_size(&self) -> NonZeroUsize { + self.inner.read_buffer_size + } + + /// Get the write buffer size. + #[inline] + pub fn write_buffer_size(&self) -> usize { + self.inner.write_buffer_size + } +} + impl Default for Options { fn default() -> Self { - Self::new() + Options { + inner: OptionData::new(), + } } } @@ -79,13 +171,14 @@ impl Default for Options { /// configuration and closes the connection. #[derive(Debug)] struct State { + // Drop the device last because otherwise a raw handle may outlive the + // owning handle. Drop the writer second last so that we can still use it. options: Options, - device: Device, + stamp: u64, config: Config, reader: BufReader, writer: BufWriter, - #[cfg(test)] - stamp: std::time::SystemTime, + device: Device, } impl State { @@ -95,7 +188,7 @@ impl State { /// configures the terminal with the default options for mode and read /// timeout. pub fn new() -> Result { - State::with_options(Options::new()) + State::with_options(Options::default()) } /// Access the controlling terminal with the given attributes. @@ -106,64 +199,57 @@ impl State { let device = Device::new()?; let handle = device.handle(); let config = Config::new(handle, &options)?; - let reader = - BufReader::with_capacity(options.read_buffer.get(), Reader::new(handle.input())); - #[allow(unused_mut)] - let mut writer = - BufWriter::with_capacity(options.write_buffer, Writer::new(handle.output())); - - #[cfg(test)] - { - let stamp = std::time::SystemTime::now(); - let _ = write!( - writer, - " \r\n", - handle, stamp - ); - - Ok(Self { - options, - device, - config, - reader, - writer, - stamp, - }) - } + let reader = BufReader::with_capacity( + options.read_buffer_size().get(), + Reader::new(handle.input(), 100 * (options.timeout().get() as u32)), + ); + let writer = + BufWriter::with_capacity(options.write_buffer_size(), Writer::new(handle.output())); + let stamp = if options.logging() { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis() as u64 + } else { + 0 + }; - #[cfg(not(test))] - Ok(Self { + let mut this = Self { options, device, config, reader, writer, - }) + stamp, + }; + + if this.options.logging() { + write!( + this.writer, + // The extra space aligns the close tag + "tty::connect pid={} tid={} in={:?} out={:?} stamp={}\r\n", + std::process::id(), + this.device.pid().unwrap_or(0), + handle.input(), + handle.output(), + stamp + )?; + this.writer.flush()?; + } + + Ok(this) } /// Get the process group ID. + #[inline] pub fn pid(&self) -> Result { self.device.pid() } - /// Get the current terminal mode. - pub fn mode(&self) -> Mode { - self.options.mode - } - - /// Get the current timeout. - pub fn timeout(&self) -> NonZeroU8 { - self.options.timeout - } - - /// Get the size of the read buffer. - pub fn read_buffer(&self) -> NonZeroUsize { - self.options.read_buffer - } - - /// Get the size of the write buffer. - pub fn write_buffer(&self) -> usize { - self.options.write_buffer + /// Get the options. + #[inline] + pub fn options(&self) -> &Options { + &self.options } } @@ -174,13 +260,18 @@ unsafe impl Send for State {} impl Drop for State { fn drop(&mut self) { - #[cfg(test)] - let _ = write!( - self.writer, - " \r\n", - self.device.handle(), - self.stamp - ); + // No need to flush, see below. + if 0 < self.stamp { + let _ = write!( + self.writer, + "tty:disconnect pid={} tid={} in={:?} out={:?} stamp={}\r\n", + std::process::id(), + self.device.pid().unwrap_or(0), + self.device.handle().input(), + self.device.handle().output(), + self.stamp + ); + } // Make sure all output has been written. let _ = self.writer.flush(); @@ -269,7 +360,9 @@ impl Terminal { pub unsafe fn connect_with(mut self, options: Options) -> Result { let mut inner = self.lock(); if let Some(state) = &*inner { - if state.mode() == options.mode && state.timeout() == options.timeout { + if state.options().mode() == options.mode() + && state.options().timeout() == options.timeout() + { Ok(self) } else { Err(ErrorKind::InvalidInput.into()) @@ -333,7 +426,9 @@ impl Terminal { pub fn access_with(mut self, options: Options) -> Result> { let mut inner = self.lock(); if let Some(state) = &*inner { - if state.mode() == options.mode && state.timeout() == options.timeout { + if state.options().mode() == options.mode() + && state.options().timeout() == options.timeout() + { Ok(TerminalAccess::new(inner, OnDrop::DoNothing)) } else { Err(ErrorKind::InvalidInput.into()) @@ -402,29 +497,17 @@ impl TerminalAccess<'_> { } /// Get the terminal's process group ID. + #[inline] pub fn pid(&self) -> Result { self.inner.as_ref().unwrap().pid() } - /// Get the terminal's current mode. - pub fn mode(&self) -> Mode { - self.inner.as_ref().unwrap().mode() - } - - /// Get the terminal's current read timeout. - pub fn timeout(&self) -> u8 { - self.inner.as_ref().unwrap().timeout().get() - } - - /// Get the size of the read buffer. - pub fn read_buffer(&self) -> usize { - self.inner.as_ref().unwrap().read_buffer().get() + /// Get the terminal's current options. + #[inline] + pub fn options(&self) -> &Options { + self.inner.as_ref().unwrap().options() } - /// Get the size of the write buffer. - pub fn write_buffer(&self) -> usize { - self.inner.as_ref().unwrap().write_buffer() - } /// Write the entire buffer and flush thereafter. pub fn print_bytes(&mut self, buf: &[u8]) -> Result<()> { self.write_all(buf)?; @@ -439,62 +522,76 @@ impl TerminalAccess<'_> { } impl Read for TerminalAccess<'_> { + #[inline] fn read(&mut self, buf: &mut [u8]) -> Result { self.get_mut().reader.read(buf) } + #[inline] fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { self.get_mut().reader.read_vectored(bufs) } + #[inline] fn read_to_end(&mut self, buf: &mut Vec) -> Result { self.get_mut().reader.read_to_end(buf) } + #[inline] fn read_to_string(&mut self, buf: &mut String) -> Result { self.get_mut().reader.read_to_string(buf) } + #[inline] fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { self.get_mut().reader.read_exact(buf) } } impl BufRead for TerminalAccess<'_> { + #[inline] fn fill_buf(&mut self) -> Result<&[u8]> { self.get_mut().reader.fill_buf() } + #[inline] fn consume(&mut self, n: usize) { self.get_mut().reader.consume(n) } + #[inline] fn read_until(&mut self, byte: u8, buf: &mut Vec) -> Result { self.get_mut().reader.read_until(byte, buf) } + #[inline] fn read_line(&mut self, buf: &mut String) -> Result { self.get_mut().reader.read_line(buf) } } impl Write for TerminalAccess<'_> { + #[inline] fn write(&mut self, buf: &[u8]) -> Result { self.get_mut().writer.write(buf) } + #[inline] fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result { self.get_mut().writer.write_vectored(bufs) } + #[inline] fn flush(&mut self) -> Result<()> { self.get_mut().writer.flush() } + #[inline] fn write_all(&mut self, buf: &[u8]) -> Result<()> { self.get_mut().writer.write_all(buf) } + #[inline] fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> Result<()> { self.get_mut().writer.write_fmt(args) }