/// The path to the default TTY on linux pub const TTY_PATH: &str = "/dev/tty"; #[cfg(unix)] pub(crate) mod imp { use std::{ fs::File, io, io::{BufRead, Read, Write}, }; use parking_lot::{const_mutex, lock_api::MutexGuard, Mutex, RawMutex}; use rustix::termios::{self, Termios}; use crate::{unix::TTY_PATH, Error, Mode, Options}; static TERM_STATE: Mutex> = const_mutex(None); /// Ask the user given a `prompt`, returning the result. pub(crate) fn ask(prompt: &str, Options { mode, .. }: &Options<'_>) -> Result { match mode { Mode::Disable => Err(Error::Disabled), Mode::Hidden => { let state = TERM_STATE.lock(); let mut in_out = save_term_state_and_disable_echo( state, std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?, )?; in_out.write_all(prompt.as_bytes())?; let mut buf_read = std::io::BufReader::with_capacity(64, in_out); let mut out = String::with_capacity(64); buf_read.read_line(&mut out)?; out.pop(); if out.ends_with('\r') { out.pop(); } buf_read.into_inner().restore_term_state()?; Ok(out) } Mode::Visible => { let mut in_out = std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?; in_out.write_all(prompt.as_bytes())?; let mut buf_read = std::io::BufReader::with_capacity(64, in_out); let mut out = String::with_capacity(64); buf_read.read_line(&mut out)?; Ok(out.trim_end().to_owned()) } } } type TermiosGuard<'a> = MutexGuard<'a, RawMutex, Option>; struct RestoreTerminalStateOnDrop<'a> { state: TermiosGuard<'a>, fd: File, } impl<'a> Read for RestoreTerminalStateOnDrop<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.fd.read(buf) } fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result { self.fd.read_vectored(bufs) } } impl<'a> Write for RestoreTerminalStateOnDrop<'a> { #[inline(always)] fn write(&mut self, buf: &[u8]) -> io::Result { self.fd.write(buf) } #[inline(always)] fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { self.fd.write_vectored(bufs) } #[inline(always)] fn flush(&mut self) -> io::Result<()> { self.fd.flush() } } impl<'a> RestoreTerminalStateOnDrop<'a> { fn restore_term_state(mut self) -> Result<(), Error> { let state = self.state.take().expect("BUG: we exist only if something is saved"); termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state)?; Ok(()) } } impl<'a> Drop for RestoreTerminalStateOnDrop<'a> { fn drop(&mut self) { if let Some(state) = self.state.take() { termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state).ok(); } } } fn save_term_state_and_disable_echo( mut state: TermiosGuard<'_>, fd: File, ) -> Result, Error> { assert!( state.is_none(), "BUG: recursive calls are not possible and we restore afterwards" ); let prev = termios::tcgetattr(&fd)?; let mut new = prev.clone(); *state = prev.into(); new.local_modes &= !termios::LocalModes::ECHO; new.local_modes |= termios::LocalModes::ECHONL; termios::tcsetattr(&fd, termios::OptionalActions::Flush, &new)?; Ok(RestoreTerminalStateOnDrop { fd, state }) } }