Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid manual polling #231

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 107 additions & 165 deletions src/unix_term.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::env;
use std::fmt::Display;
use std::fs;
use std::io::{self, BufRead, BufReader};
use std::io::{self, BufRead, BufReader, Read};
use std::mem;
use std::os::fd::{AsRawFd, RawFd};
use std::str;
Expand Down Expand Up @@ -94,6 +94,15 @@ impl Input<fs::File> {
}
}

impl<T: Read> Read for Input<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Stdin(s) => s.read(buf),
Self::File(f) => f.read(buf),
}
}
}

// NB: this is not a full BufRead implementation because io::Stdin does not implement BufRead.
impl<T: BufRead> Input<T> {
fn read_line(&mut self, buf: &mut String) -> io::Result<usize> {
Expand Down Expand Up @@ -144,199 +153,132 @@ pub(crate) fn read_secure() -> io::Result<String> {
})
}

fn poll_fd(fd: RawFd, 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: RawFd, 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 {
std::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,
std::ptr::null_mut(),
std::ptr::null_mut(),
timeout,
);
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(libc::FD_ISSET(fd, &read_fd_set))
fn read_single_char<T: Read + AsRawFd>(input: &mut T) -> io::Result<Option<char>> {
let original = unsafe { libc::fcntl(input.as_raw_fd(), libc::F_GETFL) };
c_result(|| unsafe {
libc::fcntl(
input.as_raw_fd(),
libc::F_SETFL,
original | libc::O_NONBLOCK,
)
})?;
let mut buf = [0u8; 1];
let result = read_bytes(input, &mut buf);
c_result(|| unsafe { libc::fcntl(input.as_raw_fd(), libc::F_SETFL, original) })?;
match result {
Ok(()) => {
let [byte] = buf;
Ok(Some(byte as char))
}
}
}

fn select_or_poll_term_fd(fd: RawFd, 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);
Err(err) => {
if err.kind() == io::ErrorKind::WouldBlock {
Ok(None)
} else {
Err(err)
}
}
}
poll_fd(fd, timeout)
}

fn read_single_char(fd: RawFd) -> 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: RawFd, 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' {
fn read_bytes(input: &mut impl Read, buf: &mut [u8]) -> io::Result<()> {
input.read_exact(buf)?;
if buf.starts_with(b"\x03") {
Err(io::Error::new(
io::ErrorKind::Interrupted,
"read interrupted",
))
} else {
Ok(read as u8)
Ok(())
}
}

fn read_single_key_impl(fd: RawFd) -> 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]))
fn read_single_key_impl<T: Read + AsRawFd>(fd: &mut T) -> Result<Key, io::Error> {
// NB: this doesn't use `read_single_char` because we want a blocking read here.
let mut buf = [0u8; 1];
read_bytes(fd, &mut buf)?;
let [byte] = buf;
match byte {
b'\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])),
}
} else {
// \x1b[ and 1 more char
Ok(Key::UnknownEscSeq(vec![c1, c2]))
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 [
// \x1b[ and no more input
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()),
// char after escape is not [
Ok(Key::UnknownEscSeq(vec![c1]))
}
} else {
//nothing after escape
Ok(Key::Escape)
}
}
byte => {
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 byte as char {
'\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')
c => Key::Char(c),
})
}
}
}
}

pub(crate) fn read_single_key(ctrlc_key: bool) -> io::Result<Key> {
let input = Input::unbuffered()?;
let mut input = Input::unbuffered()?;

let mut termios = core::mem::MaybeUninit::uninit();
c_result(|| unsafe { libc::tcgetattr(input.as_raw_fd(), termios.as_mut_ptr()) })?;
Expand All @@ -345,7 +287,7 @@ pub(crate) fn read_single_key(ctrlc_key: bool) -> io::Result<Key> {
unsafe { libc::cfmakeraw(&mut termios) };
termios.c_oflag = original.c_oflag;
c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSADRAIN, &termios) })?;
let rv = read_single_key_impl(input.as_raw_fd());
let rv = read_single_key_impl(&mut input);
c_result(|| unsafe { libc::tcsetattr(input.as_raw_fd(), libc::TCSADRAIN, &original) })?;

// if the user hit ^C we want to signal SIGINT to ourselves.
Expand Down
Loading