console/
unix_term.rs

1use std::env;
2use std::fmt::Display;
3use std::fs;
4use std::io::{self, BufRead, BufReader};
5use std::mem;
6use std::os::fd::{AsRawFd, RawFd};
7use std::str;
8
9#[cfg(not(target_os = "macos"))]
10use once_cell::sync::Lazy;
11
12use crate::kb::Key;
13use crate::term::Term;
14
15pub(crate) use crate::common_term::*;
16
17pub(crate) const DEFAULT_WIDTH: u16 = 80;
18
19#[inline]
20pub(crate) fn is_a_terminal(out: &impl AsRawFd) -> bool {
21    unsafe { libc::isatty(out.as_raw_fd()) != 0 }
22}
23
24pub(crate) fn is_a_color_terminal(out: &Term) -> bool {
25    if !is_a_terminal(out) {
26        return false;
27    }
28
29    if env::var("NO_COLOR").is_ok() {
30        return false;
31    }
32
33    match env::var("TERM") {
34        Ok(term) => term != "dumb",
35        Err(_) => false,
36    }
37}
38
39fn c_result<F: FnOnce() -> libc::c_int>(f: F) -> io::Result<()> {
40    let res = f();
41    if res != 0 {
42        Err(io::Error::last_os_error())
43    } else {
44        Ok(())
45    }
46}
47
48pub(crate) fn terminal_size(out: &Term) -> Option<(u16, u16)> {
49    if !is_a_terminal(out) {
50        return None;
51    }
52    let winsize = unsafe {
53        let mut winsize: libc::winsize = mem::zeroed();
54
55        // FIXME: ".into()" used as a temporary fix for a libc bug
56        // https://github.com/rust-lang/libc/pull/704
57        #[allow(clippy::useless_conversion)]
58        libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize);
59        winsize
60    };
61    if winsize.ws_row > 0 && winsize.ws_col > 0 {
62        Some((winsize.ws_row as u16, winsize.ws_col as u16))
63    } else {
64        None
65    }
66}
67
68enum Input<T> {
69    Stdin(io::Stdin),
70    File(T),
71}
72
73impl Input<BufReader<fs::File>> {
74    fn buffered() -> io::Result<Self> {
75        Ok(match Input::unbuffered()? {
76            Input::Stdin(s) => Input::Stdin(s),
77            Input::File(f) => Input::File(BufReader::new(f)),
78        })
79    }
80}
81
82impl Input<fs::File> {
83    fn unbuffered() -> io::Result<Self> {
84        let stdin = io::stdin();
85        if is_a_terminal(&stdin) {
86            Ok(Input::Stdin(stdin))
87        } else {
88            let f = fs::OpenOptions::new()
89                .read(true)
90                .write(true)
91                .open("/dev/tty")?;
92            Ok(Input::File(f))
93        }
94    }
95}
96
97// NB: this is not a full BufRead implementation because io::Stdin does not implement BufRead.
98impl<T: BufRead> Input<T> {
99    fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
100        match self {
101            Self::Stdin(s) => s.read_line(buf),
102            Self::File(f) => f.read_line(buf),
103        }
104    }
105}
106
107impl AsRawFd for Input<fs::File> {
108    fn as_raw_fd(&self) -> RawFd {
109        match self {
110            Self::Stdin(s) => s.as_raw_fd(),
111            Self::File(f) => f.as_raw_fd(),
112        }
113    }
114}
115
116impl AsRawFd for Input<BufReader<fs::File>> {
117    fn as_raw_fd(&self) -> RawFd {
118        match self {
119            Self::Stdin(s) => s.as_raw_fd(),
120            Self::File(f) => f.get_ref().as_raw_fd(),
121        }
122    }
123}
124
125pub(crate) fn read_secure() -> io::Result<String> {
126    let mut input = Input::buffered()?;
127
128    let mut termios = mem::MaybeUninit::uninit();
129    c_result(|| unsafe { libc::tcgetattr(input.as_raw_fd(), termios.as_mut_ptr()) })?;
130    let mut termios = unsafe { termios.assume_init() };
131    let original = termios;
132    termios.c_lflag &= !libc::ECHO;
133    c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSAFLUSH, &termios) })?;
134    let mut rv = String::new();
135
136    let read_rv = input.read_line(&mut rv);
137
138    c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSAFLUSH, &original) })?;
139
140    read_rv.map(|_| {
141        let len = rv.trim_end_matches(&['\r', '\n'][..]).len();
142        rv.truncate(len);
143        rv
144    })
145}
146
147fn poll_fd(fd: RawFd, timeout: i32) -> io::Result<bool> {
148    let mut pollfd = libc::pollfd {
149        fd,
150        events: libc::POLLIN,
151        revents: 0,
152    };
153    let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) };
154    if ret < 0 {
155        Err(io::Error::last_os_error())
156    } else {
157        Ok(pollfd.revents & libc::POLLIN != 0)
158    }
159}
160
161#[cfg(target_os = "macos")]
162fn select_fd(fd: RawFd, timeout: i32) -> io::Result<bool> {
163    unsafe {
164        let mut read_fd_set: libc::fd_set = mem::zeroed();
165
166        let mut timeout_val;
167        let timeout = if timeout < 0 {
168            std::ptr::null_mut()
169        } else {
170            timeout_val = libc::timeval {
171                tv_sec: (timeout / 1000) as _,
172                tv_usec: (timeout * 1000) as _,
173            };
174            &mut timeout_val
175        };
176
177        libc::FD_ZERO(&mut read_fd_set);
178        libc::FD_SET(fd, &mut read_fd_set);
179        let ret = libc::select(
180            fd + 1,
181            &mut read_fd_set,
182            std::ptr::null_mut(),
183            std::ptr::null_mut(),
184            timeout,
185        );
186        if ret < 0 {
187            Err(io::Error::last_os_error())
188        } else {
189            Ok(libc::FD_ISSET(fd, &read_fd_set))
190        }
191    }
192}
193
194fn select_or_poll_term_fd(fd: RawFd, timeout: i32) -> io::Result<bool> {
195    // There is a bug on macos that ttys cannot be polled, only select()
196    // works.  However given how problematic select is in general, we
197    // normally want to use poll there too.
198    #[cfg(target_os = "macos")]
199    {
200        if unsafe { libc::isatty(fd) == 1 } {
201            return select_fd(fd, timeout);
202        }
203    }
204    poll_fd(fd, timeout)
205}
206
207fn read_single_char(fd: RawFd) -> io::Result<Option<char>> {
208    // timeout of zero means that it will not block
209    let is_ready = select_or_poll_term_fd(fd, 0)?;
210
211    if is_ready {
212        // if there is something to be read, take 1 byte from it
213        let mut buf: [u8; 1] = [0];
214
215        read_bytes(fd, &mut buf, 1)?;
216        Ok(Some(buf[0] as char))
217    } else {
218        //there is nothing to be read
219        Ok(None)
220    }
221}
222
223// Similar to libc::read. Read count bytes into slice buf from descriptor fd.
224// If successful, return the number of bytes read.
225// Will return an error if nothing was read, i.e when called at end of file.
226fn read_bytes(fd: RawFd, buf: &mut [u8], count: u8) -> io::Result<u8> {
227    let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) };
228    if read < 0 {
229        Err(io::Error::last_os_error())
230    } else if read == 0 {
231        Err(io::Error::new(
232            io::ErrorKind::UnexpectedEof,
233            "Reached end of file",
234        ))
235    } else if buf[0] == b'\x03' {
236        Err(io::Error::new(
237            io::ErrorKind::Interrupted,
238            "read interrupted",
239        ))
240    } else {
241        Ok(read as u8)
242    }
243}
244
245fn read_single_key_impl(fd: RawFd) -> Result<Key, io::Error> {
246    loop {
247        match read_single_char(fd)? {
248            Some('\x1b') => {
249                // Escape was read, keep reading in case we find a familiar key
250                break if let Some(c1) = read_single_char(fd)? {
251                    if c1 == '[' {
252                        if let Some(c2) = read_single_char(fd)? {
253                            match c2 {
254                                'A' => Ok(Key::ArrowUp),
255                                'B' => Ok(Key::ArrowDown),
256                                'C' => Ok(Key::ArrowRight),
257                                'D' => Ok(Key::ArrowLeft),
258                                'H' => Ok(Key::Home),
259                                'F' => Ok(Key::End),
260                                'Z' => Ok(Key::BackTab),
261                                _ => {
262                                    let c3 = read_single_char(fd)?;
263                                    if let Some(c3) = c3 {
264                                        if c3 == '~' {
265                                            match c2 {
266                                                '1' => Ok(Key::Home), // tmux
267                                                '2' => Ok(Key::Insert),
268                                                '3' => Ok(Key::Del),
269                                                '4' => Ok(Key::End), // tmux
270                                                '5' => Ok(Key::PageUp),
271                                                '6' => Ok(Key::PageDown),
272                                                '7' => Ok(Key::Home), // xrvt
273                                                '8' => Ok(Key::End),  // xrvt
274                                                _ => Ok(Key::UnknownEscSeq(vec![c1, c2, c3])),
275                                            }
276                                        } else {
277                                            Ok(Key::UnknownEscSeq(vec![c1, c2, c3]))
278                                        }
279                                    } else {
280                                        // \x1b[ and 1 more char
281                                        Ok(Key::UnknownEscSeq(vec![c1, c2]))
282                                    }
283                                }
284                            }
285                        } else {
286                            // \x1b[ and no more input
287                            Ok(Key::UnknownEscSeq(vec![c1]))
288                        }
289                    } else {
290                        // char after escape is not [
291                        Ok(Key::UnknownEscSeq(vec![c1]))
292                    }
293                } else {
294                    //nothing after escape
295                    Ok(Key::Escape)
296                };
297            }
298            Some(c) => {
299                let byte = c as u8;
300                let mut buf: [u8; 4] = [byte, 0, 0, 0];
301
302                break if byte & 224u8 == 192u8 {
303                    // a two byte unicode character
304                    read_bytes(fd, &mut buf[1..], 1)?;
305                    Ok(key_from_utf8(&buf[..2]))
306                } else if byte & 240u8 == 224u8 {
307                    // a three byte unicode character
308                    read_bytes(fd, &mut buf[1..], 2)?;
309                    Ok(key_from_utf8(&buf[..3]))
310                } else if byte & 248u8 == 240u8 {
311                    // a four byte unicode character
312                    read_bytes(fd, &mut buf[1..], 3)?;
313                    Ok(key_from_utf8(&buf[..4]))
314                } else {
315                    Ok(match c {
316                        '\n' | '\r' => Key::Enter,
317                        '\x7f' => Key::Backspace,
318                        '\t' => Key::Tab,
319                        '\x01' => Key::Home,      // Control-A (home)
320                        '\x05' => Key::End,       // Control-E (end)
321                        '\x08' => Key::Backspace, // Control-H (8) (Identical to '\b')
322                        _ => Key::Char(c),
323                    })
324                };
325            }
326            None => {
327                // there is no subsequent byte ready to be read, block and wait for input
328                // negative timeout means that it will block indefinitely
329                match select_or_poll_term_fd(fd, -1) {
330                    Ok(_) => continue,
331                    Err(_) => break Err(io::Error::last_os_error()),
332                }
333            }
334        }
335    }
336}
337
338pub(crate) fn read_single_key(ctrlc_key: bool) -> io::Result<Key> {
339    let input = Input::unbuffered()?;
340
341    let mut termios = core::mem::MaybeUninit::uninit();
342    c_result(|| unsafe { libc::tcgetattr(input.as_raw_fd(), termios.as_mut_ptr()) })?;
343    let mut termios = unsafe { termios.assume_init() };
344    let original = termios;
345    unsafe { libc::cfmakeraw(&mut termios) };
346    termios.c_oflag = original.c_oflag;
347    c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSADRAIN, &termios) })?;
348    let rv = read_single_key_impl(input.as_raw_fd());
349    c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSADRAIN, &original) })?;
350
351    // if the user hit ^C we want to signal SIGINT to ourselves.
352    if let Err(ref err) = rv {
353        if err.kind() == io::ErrorKind::Interrupted {
354            if !ctrlc_key {
355                unsafe {
356                    libc::raise(libc::SIGINT);
357                }
358            } else {
359                return Ok(Key::CtrlC);
360            }
361        }
362    }
363
364    rv
365}
366
367fn key_from_utf8(buf: &[u8]) -> Key {
368    if let Ok(s) = str::from_utf8(buf) {
369        if let Some(c) = s.chars().next() {
370            return Key::Char(c);
371        }
372    }
373    Key::Unknown
374}
375
376#[cfg(not(target_os = "macos"))]
377static IS_LANG_UTF8: Lazy<bool> = Lazy::new(|| match std::env::var("LANG") {
378    Ok(lang) => lang.to_uppercase().ends_with("UTF-8"),
379    _ => false,
380});
381
382#[cfg(target_os = "macos")]
383pub(crate) fn wants_emoji() -> bool {
384    true
385}
386
387#[cfg(not(target_os = "macos"))]
388pub(crate) fn wants_emoji() -> bool {
389    *IS_LANG_UTF8
390}
391
392pub(crate) fn set_title<T: Display>(title: T) {
393    print!("\x1b]0;{}\x07", title);
394}