gix_prompt/
unix.rs

1/// The path to the default TTY on linux
2pub 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    /// Ask the user given a `prompt`, returning the result.
20    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}