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