diff --git a/src/unix_term.rs b/src/unix_term.rs index fa00ba03..c3898697 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -3,7 +3,9 @@ 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; @@ -69,7 +71,10 @@ pub fn read_secure() -> io::Result { f_tty = None; libc::STDIN_FILENO } else { - let f = fs::File::open("/dev/tty")?; + 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 @@ -99,20 +104,69 @@ pub fn read_secure() -> io::Result { }) } -fn read_single_char(fd: i32) -> io::Result> { +fn poll_fd(fd: i32, timeout: i32) -> io::Result { let mut pollfd = libc::pollfd { fd, events: libc::POLLIN, revents: 0, }; - - // timeout of zero means that it will not block - let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, 0) }; + let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, timeout) }; if ret < 0 { - return Err(io::Error::last_os_error()); + 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 { + 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 { + // 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) +} - let is_ready = pollfd.revents & libc::POLLIN != 0; +fn read_single_char(fd: i32) -> io::Result> { + // 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 @@ -154,7 +208,10 @@ pub fn read_single_key() -> io::Result { if libc::isatty(libc::STDIN_FILENO) == 1 { libc::STDIN_FILENO } else { - tty_f = fs::File::open("/dev/tty")?; + tty_f = fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; tty_f.as_raw_fd() } }; @@ -165,103 +222,97 @@ pub fn read_single_key() -> io::Result { unsafe { libc::cfmakeraw(&mut termios) }; c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &termios) })?; - let rv = match read_single_char(fd)? { - Some('\x1b') => { - // Escape was read, keep reading in case we find a familiar key - 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])), + let rv: io::Result = 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 { - Ok(Key::UnknownEscSeq(vec![c1, c2, c3])) + // \x1b[ and 1 more char + Ok(Key::UnknownEscSeq(vec![c1, c2])) } - } else { - // \x1b[ and 1 more char - Ok(Key::UnknownEscSeq(vec![c1, c2])) } } + } else { + // \x1b[ and no more input + Ok(Key::UnknownEscSeq(vec![c1])) } } else { - // \x1b[ and no more input + // char after escape is not [ Ok(Key::UnknownEscSeq(vec![c1])) } } else { - // char after escape is not [ - Ok(Key::UnknownEscSeq(vec![c1])) - } - } else { - //nothing after escape - Ok(Key::Escape) + //nothing after escape + Ok(Key::Escape) + }; } - } - Some(c) => { - let byte = c as u8; - let mut buf: [u8; 4] = [byte, 0, 0, 0]; - - 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), - }) + 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 - - let mut pollfd = libc::pollfd { - fd, - events: libc::POLLIN, - revents: 0, - }; - - // negative timeout means that it will block indefinitely - let ret = unsafe { libc::poll(&mut pollfd as *mut _, 1, -1) }; - if ret < 0 { - return Err(io::Error::last_os_error()); + 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()), + } } - - read_single_key() } }; + c_result(|| unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, &original) })?; // if the user hit ^C we want to signal SIGINT to outselves.