diff options
Diffstat (limited to 'vendor/console/src/unix_term.rs')
-rw-r--r-- | vendor/console/src/unix_term.rs | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/vendor/console/src/unix_term.rs b/vendor/console/src/unix_term.rs new file mode 100644 index 0000000..8e1e592 --- /dev/null +++ b/vendor/console/src/unix_term.rs @@ -0,0 +1,362 @@ +use std::env; +use std::fmt::Display; +use std::fs; +use std::io; +use std::io::{BufRead, BufReader}; +use std::mem; +use std::os::unix::io::AsRawFd; +use std::ptr; +use std::str; + +use crate::kb::Key; +use crate::term::Term; + +pub use crate::common_term::*; + +pub const DEFAULT_WIDTH: u16 = 80; + +#[inline] +pub fn is_a_terminal(out: &Term) -> bool { + unsafe { libc::isatty(out.as_raw_fd()) != 0 } +} + +pub fn is_a_color_terminal(out: &Term) -> bool { + if !is_a_terminal(out) { + return false; + } + + if env::var("NO_COLOR").is_ok() { + return false; + } + + match env::var("TERM") { + Ok(term) => term != "dumb", + Err(_) => false, + } +} + +pub fn c_result<F: FnOnce() -> libc::c_int>(f: F) -> io::Result<()> { + let res = f(); + if res != 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { + unsafe { + if libc::isatty(libc::STDOUT_FILENO) != 1 { + return None; + } + + let mut winsize: libc::winsize = std::mem::zeroed(); + + // FIXME: ".into()" used as a temporary fix for a libc bug + // https://github.com/rust-lang/libc/pull/704 + #[allow(clippy::useless_conversion)] + libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize); + if winsize.ws_row > 0 && winsize.ws_col > 0 { + Some((winsize.ws_row as u16, winsize.ws_col as u16)) + } else { + None + } + } +} + +pub fn read_secure() -> io::Result<String> { + let f_tty; + let fd = unsafe { + if libc::isatty(libc::STDIN_FILENO) == 1 { + f_tty = None; + libc::STDIN_FILENO + } else { + let f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + let fd = f.as_raw_fd(); + f_tty = Some(BufReader::new(f)); + fd + } + }; + + let mut termios = core::mem::MaybeUninit::uninit(); + c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; + let mut termios = unsafe { termios.assume_init() }; + let original = termios; + termios.c_lflag &= !libc::ECHO; + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) })?; + let mut rv = String::new(); + + let read_rv = if let Some(mut f) = f_tty { + f.read_line(&mut rv) + } else { + io::stdin().read_line(&mut rv) + }; + + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &original) })?; + + read_rv.map(|_| { + let len = rv.trim_end_matches(&['\r', '\n'][..]).len(); + rv.truncate(len); + rv + }) +} + +fn poll_fd(fd: i32, timeout: i32) -> io::Result<bool> { + let mut pollfd = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) }; + if ret < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(pollfd.revents & libc::POLLIN != 0) + } +} + +#[cfg(target_os = "macos")] +fn select_fd(fd: i32, timeout: i32) -> io::Result<bool> { + unsafe { + let mut read_fd_set: libc::fd_set = mem::zeroed(); + + let mut timeout_val; + let timeout = if timeout < 0 { + ptr::null_mut() + } else { + timeout_val = libc::timeval { + tv_sec: (timeout / 1000) as _, + tv_usec: (timeout * 1000) as _, + }; + &mut timeout_val + }; + + libc::FD_ZERO(&mut read_fd_set); + libc::FD_SET(fd, &mut read_fd_set); + let ret = libc::select( + fd + 1, + &mut read_fd_set, + ptr::null_mut(), + ptr::null_mut(), + timeout, + ); + if ret < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(libc::FD_ISSET(fd, &read_fd_set)) + } + } +} + +fn select_or_poll_term_fd(fd: i32, timeout: i32) -> io::Result<bool> { + // There is a bug on macos that ttys cannot be polled, only select() + // works. However given how problematic select is in general, we + // normally want to use poll there too. + #[cfg(target_os = "macos")] + { + if unsafe { libc::isatty(fd) == 1 } { + return select_fd(fd, timeout); + } + } + poll_fd(fd, timeout) +} + +fn read_single_char(fd: i32) -> io::Result<Option<char>> { + // timeout of zero means that it will not block + let is_ready = select_or_poll_term_fd(fd, 0)?; + + if is_ready { + // if there is something to be read, take 1 byte from it + let mut buf: [u8; 1] = [0]; + + read_bytes(fd, &mut buf, 1)?; + Ok(Some(buf[0] as char)) + } else { + //there is nothing to be read + Ok(None) + } +} + +// Similar to libc::read. Read count bytes into slice buf from descriptor fd. +// If successful, return the number of bytes read. +// Will return an error if nothing was read, i.e when called at end of file. +fn read_bytes(fd: i32, buf: &mut [u8], count: u8) -> io::Result<u8> { + let read = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, count as usize) }; + if read < 0 { + Err(io::Error::last_os_error()) + } else if read == 0 { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Reached end of file", + )) + } else if buf[0] == b'\x03' { + Err(io::Error::new( + io::ErrorKind::Interrupted, + "read interrupted", + )) + } else { + Ok(read as u8) + } +} + +fn read_single_key_impl(fd: i32) -> Result<Key, io::Error> { + loop { + match read_single_char(fd)? { + Some('\x1b') => { + // Escape was read, keep reading in case we find a familiar key + break if let Some(c1) = read_single_char(fd)? { + if c1 == '[' { + if let Some(c2) = read_single_char(fd)? { + match c2 { + 'A' => Ok(Key::ArrowUp), + 'B' => Ok(Key::ArrowDown), + 'C' => Ok(Key::ArrowRight), + 'D' => Ok(Key::ArrowLeft), + 'H' => Ok(Key::Home), + 'F' => Ok(Key::End), + 'Z' => Ok(Key::BackTab), + _ => { + let c3 = read_single_char(fd)?; + if let Some(c3) = c3 { + if c3 == '~' { + match c2 { + '1' => Ok(Key::Home), // tmux + '2' => Ok(Key::Insert), + '3' => Ok(Key::Del), + '4' => Ok(Key::End), // tmux + '5' => Ok(Key::PageUp), + '6' => Ok(Key::PageDown), + '7' => Ok(Key::Home), // xrvt + '8' => Ok(Key::End), // xrvt + _ => Ok(Key::UnknownEscSeq(vec![c1, c2, c3])), + } + } else { + Ok(Key::UnknownEscSeq(vec![c1, c2, c3])) + } + } else { + // \x1b[ and 1 more char + Ok(Key::UnknownEscSeq(vec![c1, c2])) + } + } + } + } else { + // \x1b[ and no more input + Ok(Key::UnknownEscSeq(vec![c1])) + } + } else { + // char after escape is not [ + Ok(Key::UnknownEscSeq(vec![c1])) + } + } else { + //nothing after escape + Ok(Key::Escape) + }; + } + Some(c) => { + let byte = c as u8; + let mut buf: [u8; 4] = [byte, 0, 0, 0]; + + break if byte & 224u8 == 192u8 { + // a two byte unicode character + read_bytes(fd, &mut buf[1..], 1)?; + Ok(key_from_utf8(&buf[..2])) + } else if byte & 240u8 == 224u8 { + // a three byte unicode character + read_bytes(fd, &mut buf[1..], 2)?; + Ok(key_from_utf8(&buf[..3])) + } else if byte & 248u8 == 240u8 { + // a four byte unicode character + read_bytes(fd, &mut buf[1..], 3)?; + Ok(key_from_utf8(&buf[..4])) + } else { + Ok(match c { + '\n' | '\r' => Key::Enter, + '\x7f' => Key::Backspace, + '\t' => Key::Tab, + '\x01' => Key::Home, // Control-A (home) + '\x05' => Key::End, // Control-E (end) + '\x08' => Key::Backspace, // Control-H (8) (Identical to '\b') + _ => Key::Char(c), + }) + }; + } + None => { + // there is no subsequent byte ready to be read, block and wait for input + // negative timeout means that it will block indefinitely + match select_or_poll_term_fd(fd, -1) { + Ok(_) => continue, + Err(_) => break Err(io::Error::last_os_error()), + } + } + } + } +} + +pub fn read_single_key() -> io::Result<Key> { + let tty_f; + let fd = unsafe { + if libc::isatty(libc::STDIN_FILENO) == 1 { + libc::STDIN_FILENO + } else { + tty_f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + tty_f.as_raw_fd() + } + }; + let mut termios = core::mem::MaybeUninit::uninit(); + c_result(|| unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) })?; + let mut termios = unsafe { termios.assume_init() }; + let original = termios; + unsafe { libc::cfmakeraw(&mut termios) }; + termios.c_oflag = original.c_oflag; + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?; + let rv: io::Result<Key> = read_single_key_impl(fd); + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &original) })?; + + // if the user hit ^C we want to signal SIGINT to outselves. + if let Err(ref err) = rv { + if err.kind() == io::ErrorKind::Interrupted { + unsafe { + libc::raise(libc::SIGINT); + } + } + } + + rv +} + +pub fn key_from_utf8(buf: &[u8]) -> Key { + if let Ok(s) = str::from_utf8(buf) { + if let Some(c) = s.chars().next() { + return Key::Char(c); + } + } + Key::Unknown +} + +#[cfg(not(target_os = "macos"))] +lazy_static::lazy_static! { + static ref IS_LANG_UTF8: bool = match std::env::var("LANG") { + Ok(lang) => lang.to_uppercase().ends_with("UTF-8"), + _ => false, + }; +} + +#[cfg(target_os = "macos")] +pub fn wants_emoji() -> bool { + true +} + +#[cfg(not(target_os = "macos"))] +pub fn wants_emoji() -> bool { + *IS_LANG_UTF8 +} + +pub fn set_title<T: Display>(title: T) { + print!("\x1b]0;{}\x07", title); +} |