1pub const TTY_PATH: &str = "/dev/tty";
3
4#[cfg(unix)]
5pub(crate) mod imp {
6 use std::{
7 fs::File,
8 io,
9 io::{BufRead, Read, Write},
10 };
11
12 use parking_lot::{const_mutex, lock_api::MutexGuard, Mutex, RawMutex};
13 use rustix::termios::{self, Termios};
14
15 use crate::{unix::TTY_PATH, Error, Mode, Options};
16
17 static TERM_STATE: Mutex<Option<Termios>> = const_mutex(None);
18
19 pub(crate) fn ask(prompt: &str, Options { mode, .. }: &Options<'_>) -> Result<String, Error> {
21 match mode {
22 Mode::Disable => Err(Error::Disabled),
23 Mode::Hidden => {
24 let state = TERM_STATE.lock();
25 let mut in_out = save_term_state_and_disable_echo(
26 state,
27 std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?,
28 )?;
29 in_out.write_all(prompt.as_bytes())?;
30
31 let mut buf_read = std::io::BufReader::with_capacity(64, in_out);
32 let mut out = String::with_capacity(64);
33 buf_read.read_line(&mut out)?;
34
35 out.pop();
36 if out.ends_with('\r') {
37 out.pop();
38 }
39 buf_read.into_inner().restore_term_state()?;
40 Ok(out)
41 }
42 Mode::Visible => {
43 let mut in_out = std::fs::OpenOptions::new().write(true).read(true).open(TTY_PATH)?;
44 in_out.write_all(prompt.as_bytes())?;
45
46 let mut buf_read = std::io::BufReader::with_capacity(64, in_out);
47 let mut out = String::with_capacity(64);
48 buf_read.read_line(&mut out)?;
49 Ok(out.trim_end().to_owned())
50 }
51 }
52 }
53
54 type TermiosGuard<'a> = MutexGuard<'a, RawMutex, Option<Termios>>;
55
56 struct RestoreTerminalStateOnDrop<'a> {
57 state: TermiosGuard<'a>,
58 fd: File,
59 }
60
61 impl Read for RestoreTerminalStateOnDrop<'_> {
62 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
63 self.fd.read(buf)
64 }
65
66 fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
67 self.fd.read_vectored(bufs)
68 }
69 }
70
71 impl Write for RestoreTerminalStateOnDrop<'_> {
72 #[inline(always)]
73 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
74 self.fd.write(buf)
75 }
76
77 #[inline(always)]
78 fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
79 self.fd.write_vectored(bufs)
80 }
81
82 #[inline(always)]
83 fn flush(&mut self) -> io::Result<()> {
84 self.fd.flush()
85 }
86 }
87
88 impl RestoreTerminalStateOnDrop<'_> {
89 fn restore_term_state(mut self) -> Result<(), Error> {
90 let state = self.state.take().expect("BUG: we exist only if something is saved");
91 termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state)?;
92 Ok(())
93 }
94 }
95
96 impl Drop for RestoreTerminalStateOnDrop<'_> {
97 fn drop(&mut self) {
98 if let Some(state) = self.state.take() {
99 termios::tcsetattr(&self.fd, termios::OptionalActions::Flush, &state).ok();
100 }
101 }
102 }
103
104 fn save_term_state_and_disable_echo(
105 mut state: TermiosGuard<'_>,
106 fd: File,
107 ) -> Result<RestoreTerminalStateOnDrop<'_>, Error> {
108 assert!(
109 state.is_none(),
110 "BUG: recursive calls are not possible and we restore afterwards"
111 );
112
113 let prev = termios::tcgetattr(&fd)?;
114 let mut new = prev.clone();
115 *state = prev.into();
116
117 new.local_modes &= !termios::LocalModes::ECHO;
118 new.local_modes |= termios::LocalModes::ECHONL;
119 termios::tcsetattr(&fd, termios::OptionalActions::Flush, &new)?;
120
121 Ok(RestoreTerminalStateOnDrop { fd, state })
122 }
123}