1
0
Fork 0

Adding upstream version 0.2.3.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-05 06:17:43 +01:00
parent c7e0ec57a4
commit 7f48381065
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
14 changed files with 647 additions and 0 deletions

191
src/lib.rs Normal file
View file

@ -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)
}
}

11
src/sys/mod.rs Normal file
View file

@ -0,0 +1,11 @@
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::*;

142
src/sys/unix.rs Normal file
View file

@ -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)
}
}

117
src/sys/windows.rs Normal file
View file

@ -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)
}
}