summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-11-20 08:00:44 +0100
committerDaniel Baumann <daniel@debian.org>2024-11-20 08:00:44 +0100
commit826d6de99c06770adba223a4f6f61d7fb7acd1c8 (patch)
tree76ed8100340c674cde0a65f7ec1b4d656be095f1
parentInitial commit. (diff)
downloadrust-terminal-prompt-826d6de99c06770adba223a4f6f61d7fb7acd1c8.tar.xz
rust-terminal-prompt-826d6de99c06770adba223a4f6f61d7fb7acd1c8.zip
Adding upstream version 0.2.3.upstream/0.2.3upstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
-rw-r--r--.cargo_vcs_info.json6
-rw-r--r--.gitignore1
-rw-r--r--CHANGELOG20
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml41
-rw-r--r--Cargo.toml.orig22
-rw-r--r--LICENSE.md24
-rw-r--r--README.md18
-rw-r--r--README.tpl5
-rw-r--r--examples/username-password.rs10
-rw-r--r--src/lib.rs191
-rw-r--r--src/sys/mod.rs11
-rw-r--r--src/sys/unix.rs142
-rw-r--r--src/sys/windows.rs117
14 files changed, 647 insertions, 0 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..81fad5f
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+ "git": {
+ "sha1": "0fbe095a9b44a8da5b42c972b54af660a340acda"
+ },
+ "path_in_vcs": ""
+} \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..992ae6e
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,20 @@
+# Version 0.2.3 - 2023-08-09
+- [change][patch] Remove use of `IsTerminal` to support rust version 1.66 and up.
+- [change][patch] Declare minimum rust version in `Cargo.toml`.
+
+# Version 0.2.2 - 2023-08-08
+- [fix][minor] Fix printing of newline in `Terminal::read_input_line()`.
+
+# Version 0.2.1 - 2023-08-04
+- [change][patch] Fix category slug in `Cargo.toml`.
+
+# Version 0.2.0 - 2023-08-04
+- [rename][major] Rename `TerminalPrompter` to `Terminal`.
+- [rename][major] Rename `read_line()` to `read_input_line()`.
+- [add][minor] Implement `BufRead` for `Terminal`.
+- [change][minor] Try `sterr`, `stdin`, `stdout` and `/dev/tty` in that order on Unix.
+- [change][minor] Do not cache the terminal mode, retrieve it every time when needed.
+- [add][minor] Add documentation.
+
+# Version 0.1.0 - 2023-08-03
+- [add][minor] Initial release.
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..44de46e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,39 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "terminal-prompt"
+version = "0.2.3"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..752f5f4
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,41 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.66"
+name = "terminal-prompt"
+version = "0.2.3"
+authors = ["Maarten de Vries <maarten@de-vri.es>"]
+publish = ["crates-io"]
+description = "Tiny library for prompting sensitive or non-sensitive data on the terminal"
+documentation = "https://docs.rs/terminal-prompt"
+readme = "README.md"
+keywords = [
+ "terminal",
+ "console",
+ "prompt",
+ "input",
+ "tty",
+]
+categories = [
+ "command-line-interface",
+ "os",
+]
+license = "BSD-2-Clause"
+repository = "https://github.com/de-vri-es/terminal-prompt-rs"
+
+[target."cfg(unix)".dependencies.libc]
+version = "0.2.147"
+
+[target."cfg(windows)".dependencies.winapi]
+version = "0.3.9"
+features = ["consoleapi"]
diff --git a/Cargo.toml.orig b/Cargo.toml.orig
new file mode 100644
index 0000000..360a8fc
--- /dev/null
+++ b/Cargo.toml.orig
@@ -0,0 +1,22 @@
+[package]
+name = "terminal-prompt"
+version = "0.2.3"
+description = "Tiny library for prompting sensitive or non-sensitive data on the terminal"
+license = "BSD-2-Clause"
+
+authors = ["Maarten de Vries <maarten@de-vri.es>"]
+repository = "https://github.com/de-vri-es/terminal-prompt-rs"
+documentation = "https://docs.rs/terminal-prompt"
+
+keywords = ["terminal", "console", "prompt", "input", "tty"]
+categories = ["command-line-interface", "os"]
+
+publish = ["crates-io"]
+edition = "2021"
+ rust-version = "1.66"
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2.147"
+
+[target.'cfg(windows)'.dependencies]
+winapi = { version = "0.3.9", features = ["consoleapi"] }
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..df9bcf5
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,24 @@
+BSD 2-Clause License
+
+Copyright (c) 2023, Maarten de Vries <maarten@de-vri.es>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8f4bb46
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# terminal-prompt
+
+Tiny library for prompting sensitive or non-sensitive data on the terminal.
+
+The only dependency is `libc` on Unix and `winapi` on Windows.
+
+See [`Terminal`] for the API documentation.
+
+## Example
+Read a username and password from the terminal:
+```rust
+use terminal_prompt::Terminal;
+let mut terminal = Terminal::open()?;
+let username = terminal.prompt("Username: ")?;
+let password = terminal.prompt_sensitive("Password: ")?;
+```
+
+[`Terminal`]: https://docs.rs/terminal-prompt/latest/terminal_prompt/struct.Terminal.html
diff --git a/README.tpl b/README.tpl
new file mode 100644
index 0000000..28afaae
--- /dev/null
+++ b/README.tpl
@@ -0,0 +1,5 @@
+# {{crate}}
+
+{{readme}}
+
+[`Terminal`]: https://docs.rs/terminal-prompt/latest/terminal_prompt/struct.Terminal.html
diff --git a/examples/username-password.rs b/examples/username-password.rs
new file mode 100644
index 0000000..93071c9
--- /dev/null
+++ b/examples/username-password.rs
@@ -0,0 +1,10 @@
+use terminal_prompt::Terminal;
+
+fn main() -> std::io::Result<()> {
+ let mut terminal = Terminal::open()?;
+ let username = terminal.prompt("Username: ")?;
+ let password = terminal.prompt_sensitive("Password: ")?;
+ println!("Username: {username}");
+ println!("Password: {password}");
+ Ok(())
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..b8d42ee
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,191 @@
+//! Tiny library for prompting sensitive or non-sensitive data on the terminal.
+//!
+//! The only dependency is `libc` on Unix and `winapi` on Windows.
+//!
+//! See [`Terminal`] for the API documentation.
+//!
+//! # Example
+//! Read a username and password from the terminal:
+//! ```no_run
+//! # fn main() -> std::io::Result<()> {
+//! use terminal_prompt::Terminal;
+//! let mut terminal = Terminal::open()?;
+//! let username = terminal.prompt("Username: ")?;
+//! let password = terminal.prompt_sensitive("Password: ")?;
+//! # Ok(())
+//! # }
+//! ```
+
+#![warn(missing_docs)]
+
+use std::io::{BufReader, BufRead, Read, Write};
+
+mod sys;
+
+/// A handle to the terminal associated with the current process.
+///
+/// Once opened, you can use [`Self::prompt()`] to read non-sensitive data from the terminal,
+/// and [`Self::prompt_sensitive()`] to read sensitive data like passwords.
+///
+/// Alternatively, you can manually call [`Self::enable_echo()`] and [`Self::disable_echo()`], and read/write from the terminal directly.
+/// The terminal handle implements the standard [`Read`], [`Write`] and [`BufRead`] traits,
+/// and it has a [`Self::read_line()`] convenience function that returns a new string.
+///
+/// # Terminal modes
+/// When opened, the terminal will be put in line editing mode.
+/// When dropped, the original mode of the terminal will be restored.
+/// Note that the terminal is inherently a global resource,
+/// so creating multiple terminal objects and dropping them in a different order can cause the terminal to be left in a different mode.
+pub struct Terminal {
+ /// The underlying terminal.
+ terminal: BufReader<sys::Terminal>,
+
+ /// The mode of the terminal when we opened it.
+ initial_mode: sys::TerminalMode,
+}
+
+impl Terminal {
+ /// Open the terminal associated with the current process.
+ ///
+ /// The exact behavior is platform dependent.
+ ///
+ /// On Unix platforms, if one of standard I/O streams is a terminal, that terminal is used.
+ /// First standard error is tried, then standard input and finally standard output.
+ /// If none of those work, the function tries to open `/dev/tty`.
+ /// This means that on Unix platforms, the terminal prompt can still work, even when both standard input and standard output are connected to pipes instead of the terminal.
+ ///
+ /// On Windows, if both standard input and standard error are connected to a terminal, those streams are used.
+ ///
+ /// In all cases, if the function fails to find a terminal for the process, an error is returned.
+ pub fn open() -> std::io::Result<Self> {
+ // Open the terminal and retrieve the initial mode.
+ let terminal = sys::Terminal::open()?;
+ let initial_mode = terminal.get_terminal_mode()?;
+
+ // Enable line editing mode.
+ let mut mode = initial_mode;
+ mode.enable_line_editing();
+ terminal.set_terminal_mode(&mode)?;
+
+ Ok(Self {
+ terminal: BufReader::new(terminal),
+ initial_mode,
+ })
+ }
+
+ /// Check if the terminal is echoing input.
+ ///
+ /// If enabled, any text typed on the terminal will be visible.
+ pub fn is_echo_enabled(&self) -> std::io::Result<bool> {
+ let mode = self.terminal.get_ref().get_terminal_mode()?;
+ Ok(mode.is_echo_enabled())
+ }
+
+ /// Disable echoing of terminal input.
+ ///
+ /// This will prevent text typed on the terminal from being visible.
+ /// This can be used to hide passwords while they are being typed.
+ pub fn disable_echo(&self) -> std::io::Result<()> {
+ let mut mode = self.terminal.get_ref().get_terminal_mode()?;
+ mode.disable_echo();
+ self.terminal.get_ref().set_terminal_mode(&mode)?;
+ Ok(())
+ }
+
+ /// Enable echoing of terminal input.
+ ///
+ /// This will cause any text typed on the terminal to be visible.
+ pub fn enable_echo(&mut self) -> std::io::Result<()> {
+ let mut mode = self.terminal.get_ref().get_terminal_mode()?;
+ mode.enable_echo();
+ self.terminal.get_ref().set_terminal_mode(&mode)?;
+ Ok(())
+ }
+
+ /// Read a line of input from the terminal.
+ ///
+ /// If echoing is disabled, this will also print a newline character to visually indicate to the user.
+ /// If this is not desired, use the [`BufRead::read_line()`] function instead.
+ pub fn read_input_line(&mut self) -> std::io::Result<String> {
+ let mut buffer = String::new();
+ self.terminal.read_line(&mut buffer)?;
+
+ if self.is_echo_enabled().ok() == Some(false) {
+ writeln!(self).ok();
+ }
+ if buffer.ends_with('\n') {
+ buffer.pop();
+ }
+ Ok(buffer)
+ }
+
+ /// Prompt the user on the terminal.
+ ///
+ /// This function does not enable or disable echoing and should not normally be used for reading sensitive data like passwords.
+ /// Consider [`Self::prompt_sensitive()`] instead.
+ pub fn prompt(&mut self, prompt: impl std::fmt::Display) -> std::io::Result<String> {
+ write!(self, "{prompt}")?;
+ self.read_input_line()
+ }
+
+ /// Prompt the user for sensitive data (like passwords) on the terminal.
+ ///
+ /// This function makes sure that echoing is disabled before the prompt is shown.
+ /// If echoing was enabled, it is re-enabled after the response is read.
+ ///
+ /// Use [`Self::prompt()`] to read non-sensitive data.
+ pub fn prompt_sensitive(&mut self, prompt: impl std::fmt::Display) -> std::io::Result<String> {
+ let old_mode = self.terminal.get_ref().get_terminal_mode()?;
+ if old_mode.is_echo_enabled() {
+ let mut new_mode = old_mode;
+ new_mode.disable_echo();
+ self.terminal.get_ref().set_terminal_mode(&new_mode)?;
+ }
+ write!(self, "{prompt}")?;
+ let line = self.read_input_line();
+ if old_mode.is_echo_enabled() {
+ self.terminal.get_ref().set_terminal_mode(&old_mode).ok();
+ }
+ line
+ }
+}
+
+impl Drop for Terminal {
+ fn drop(&mut self) {
+ self.terminal.get_ref().set_terminal_mode(&self.initial_mode).ok();
+ }
+}
+
+impl Read for Terminal {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.terminal.read(buf)
+ }
+
+ fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
+ self.terminal.read_vectored(bufs)
+ }
+}
+
+impl BufRead for Terminal {
+ fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
+ self.terminal.fill_buf()
+ }
+
+ fn consume(&mut self, amt: usize) {
+ self.terminal.consume(amt)
+ }
+}
+
+impl Write for Terminal {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ self.terminal.get_mut().write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.terminal.get_mut().flush()
+ }
+
+ fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
+ self.terminal.get_mut().write_vectored(bufs)
+ }
+}
diff --git a/src/sys/mod.rs b/src/sys/mod.rs
new file mode 100644
index 0000000..3525e01
--- /dev/null
+++ b/src/sys/mod.rs
@@ -0,0 +1,11 @@
+#[cfg(unix)]
+mod unix;
+
+#[cfg(unix)]
+pub use unix::*;
+
+#[cfg(windows)]
+mod windows;
+
+#[cfg(windows)]
+pub use windows::*;
diff --git a/src/sys/unix.rs b/src/sys/unix.rs
new file mode 100644
index 0000000..67bc1da
--- /dev/null
+++ b/src/sys/unix.rs
@@ -0,0 +1,142 @@
+use std::fs::File;
+use std::io::{Read, Write};
+use std::mem::ManuallyDrop;
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, RawFd};
+
+/// Unix handle to an open terminal.
+pub enum Terminal {
+ /// Non-owning file for one of the standard I/O streams.
+ Stdio(ManuallyDrop<File>),
+
+ /// Owned file for `/dev/tty`.
+ File(File),
+}
+
+#[derive(Copy, Clone)]
+pub struct TerminalMode {
+ termios: libc::termios,
+}
+
+impl Terminal {
+ pub fn open() -> std::io::Result<Self> {
+ if let Some(terminal) = open_fd_terminal(2) {
+ Ok(terminal)
+ } else if let Some(terminal) = open_fd_terminal(0) {
+ Ok(terminal)
+ } else if let Some(terminal) = open_fd_terminal(1) {
+ Ok(terminal)
+ } else {
+ let file = std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open("/dev/tty")?;
+ if is_terminal(file.as_fd()) {
+ Ok(Self::File(file))
+ } else {
+ Err(std::io::Error::from_raw_os_error(libc::ENOTTY))
+ }
+ }
+ }
+
+ pub fn get_terminal_mode(&self) -> std::io::Result<TerminalMode> {
+ unsafe {
+ let mut termios = std::mem::zeroed();
+ check_ret(libc::tcgetattr(self.as_fd().as_raw_fd(), &mut termios))?;
+ Ok(TerminalMode { termios })
+ }
+ }
+
+ pub fn set_terminal_mode(&self, mode: &TerminalMode) -> std::io::Result<()> {
+ unsafe {
+ check_ret(libc::tcsetattr(
+ self.as_fd().as_raw_fd(),
+ libc::TCSANOW,
+ &mode.termios,
+ ))?;
+ Ok(())
+ }
+ }
+
+ fn as_file(&self) -> &File {
+ match self {
+ Self::Stdio(io) => io,
+ Self::File(io) => io,
+ }
+ }
+}
+
+fn open_fd_terminal(fd: RawFd) -> Option<Terminal> {
+ let file = unsafe { ManuallyDrop::new(File::from_raw_fd(fd)) };
+ if is_terminal(file.as_fd()) {
+ Some(Terminal::Stdio(file))
+ } else {
+ None
+ }
+}
+
+impl TerminalMode {
+ pub fn enable_line_editing(&mut self) {
+ self.termios.c_lflag |= libc::ICANON;
+ }
+
+ pub fn disable_echo(&mut self) {
+ self.termios.c_lflag &= !libc::ECHO;
+ self.termios.c_lflag &= !libc::ICANON;
+ }
+
+ pub fn enable_echo(&mut self) {
+ self.termios.c_lflag |= libc::ECHO;
+ self.termios.c_lflag |= !libc::ICANON;
+ }
+
+ pub fn is_echo_enabled(&self) -> bool {
+ self.termios.c_lflag & libc::ECHO != 0
+ }
+}
+
+fn is_terminal(fd: BorrowedFd) -> bool {
+ unsafe {
+ libc::isatty(fd.as_raw_fd()) == 1
+ }
+}
+
+fn check_ret(input: i32) -> std::io::Result<()> {
+ if input == 0 {
+ Ok(())
+ } else {
+ Err(std::io::Error::last_os_error())
+ }
+}
+
+impl AsFd for Terminal {
+ fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
+ match self {
+ Self::Stdio(stdin) => stdin.as_fd(),
+ Self::File(file) => file.as_fd(),
+ }
+ }
+}
+
+impl Read for Terminal {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.as_file().read(buf)
+ }
+
+ fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
+ self.as_file().read_vectored(bufs)
+ }
+}
+
+impl Write for Terminal {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ self.as_file().write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.as_file().flush()
+ }
+
+ fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
+ self.as_file().write_vectored(bufs)
+ }
+}
diff --git a/src/sys/windows.rs b/src/sys/windows.rs
new file mode 100644
index 0000000..958f993
--- /dev/null
+++ b/src/sys/windows.rs
@@ -0,0 +1,117 @@
+use std::io::{Read, Write};
+use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
+
+use winapi::um::consoleapi::{
+ GetConsoleMode,
+ SetConsoleMode,
+};
+use winapi::um::wincon::{
+ ENABLE_LINE_INPUT,
+ ENABLE_ECHO_INPUT,
+};
+
+use winapi::shared::minwindef::{BOOL, DWORD};
+
+pub struct Terminal {
+ input: std::io::Stdin,
+ output: std::io::Stderr,
+}
+
+#[derive(Copy, Clone)]
+pub struct TerminalMode {
+ input_mode: DWORD,
+}
+
+impl Terminal {
+ pub fn open() -> std::io::Result<Self> {
+ let input = std::io::stdin();
+ let output = std::io::stderr();
+ if !is_terminal(input.as_handle()) {
+ return Err(std::io::Error::new(std::io::ErrorKind::Other, "stdin is not a terminal"));
+ }
+ if !is_terminal(output.as_handle()) {
+ return Err(std::io::Error::new(std::io::ErrorKind::Other, "stderr is not a terminal"));
+ }
+ Ok(Self {
+ input,
+ output,
+ })
+ }
+
+ pub fn get_terminal_mode(&self) -> std::io::Result<TerminalMode> {
+ unsafe {
+ let mut input_mode = 0;
+ check_ret(GetConsoleMode(self.input.as_raw_handle().cast(), &mut input_mode))?;
+ Ok(TerminalMode {
+ input_mode,
+ })
+ }
+ }
+
+ pub fn set_terminal_mode(&self, mode: &TerminalMode) -> std::io::Result<()> {
+ unsafe {
+ check_ret(SetConsoleMode(
+ self.input.as_raw_handle().cast(),
+ mode.input_mode,
+ ))?;
+ Ok(())
+ }
+ }
+}
+
+impl TerminalMode {
+ pub fn enable_line_editing(&mut self) {
+ self.input_mode |= ENABLE_LINE_INPUT;
+ }
+
+ pub fn disable_echo(&mut self) {
+ self.input_mode &= !ENABLE_ECHO_INPUT;
+ }
+
+ pub fn enable_echo(&mut self) {
+ self.input_mode |= ENABLE_ECHO_INPUT;
+ }
+
+ pub fn is_echo_enabled(&self) -> bool {
+ self.input_mode & ENABLE_ECHO_INPUT != 0
+ }
+}
+
+fn is_terminal(handle: BorrowedHandle) -> bool {
+ unsafe {
+ let mut mode = 0;
+ GetConsoleMode(handle.as_raw_handle().cast(), &mut mode) != 0
+ }
+}
+
+fn check_ret(input: BOOL) -> std::io::Result<()> {
+ if input != 0 {
+ Ok(())
+ } else {
+ Err(std::io::Error::last_os_error())
+ }
+}
+
+impl Read for Terminal {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.input.read(buf)
+ }
+
+ fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
+ self.input.read_vectored(bufs)
+ }
+}
+
+impl Write for Terminal {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+ self.output.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.output.flush()
+ }
+
+ fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
+ self.output.write_vectored(bufs)
+ }
+}