console/
unix_term.rs

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