use signal_hook::consts::SIGWINCH; use signal_hook::low_level::pipe; use termion::event::{parse_event, Event, Key, MouseEvent}; use std::io; use std::io::{stdin, Read, Stdin}; use std::os::unix::io::AsRawFd; use std::os::unix::net::UnixStream; const POLL_INFINITE_TIMEOUT: i32 = -1; const SIGWINCH_PIPE_INDEX: usize = 0; const BUFFER_SIZE: usize = 1024; const ESCAPE: u8 = 0o33; pub fn remap_dev_tty_to_stdin() { // The readline library we use, rustyline, always gets its input from STDIN. // If jless accepts its input from STDIN, then rustyline can't accept input. // To fix this, we open up /dev/tty, and remap it to STDIN, as suggested in // this StackOverflow post: // // https://stackoverflow.com/questions/29689034/piped-stdin-and-keyboard-same-time-in-c // // rustyline may add its own fix to support reading from /dev/tty: // // https://github.com/kkawakam/rustyline/issues/599 unsafe { // freopen(3) docs: https://linux.die.net/man/3/freopen let filename = std::ffi::CString::new("/dev/tty").unwrap(); let path = std::ffi::CString::new("r").unwrap(); let _ = libc::freopen(filename.as_ptr(), path.as_ptr(), libc_stdhandle::stdin()); } } pub fn get_input() -> impl Iterator> { let (sigwinch_read, sigwinch_write) = UnixStream::pair().unwrap(); // NOTE: This overrides the SIGWINCH handler registered by rustyline. // We should maybe get a reference to the existing signal handler // and call it when appropriate, but it seems to only be used to handle // line wrapping, and it seems to work fine without it. pipe::register(SIGWINCH, sigwinch_write).unwrap(); TuiInput::new(stdin(), sigwinch_read) } fn read_and_retry_on_interrupt(input: &mut Stdin, buf: &mut [u8]) -> io::Result { loop { match input.read(buf) { res @ Ok(_) => { return res; } Err(err) => { if err.kind() != io::ErrorKind::Interrupted { return Err(err); } // Otherwise just try again } } } } struct BufferedInput { input: Stdin, buffer: [u8; N], buffer_size: usize, buffer_index: usize, might_have_more_data: bool, } impl BufferedInput { fn new(input: Stdin) -> BufferedInput { BufferedInput { input, buffer: [0; N], buffer_size: 0, buffer_index: 0, might_have_more_data: false, } } fn next_u8(&mut self) -> u8 { if self.buffer_index >= self.buffer_size { panic!("No data in buffer"); } let val = self.buffer[self.buffer_index]; self.buffer_index += 1; val } fn clear(&mut self) { // Clear buffer in debug mode. if cfg!(debug_assertions) { for elem in self.buffer.iter_mut() { *elem = 0; } } self.buffer_size = 0; self.buffer_index = 0; self.might_have_more_data = false; } fn might_have_buffered_data(&self) -> bool { self.might_have_more_data || self.has_buffered_data() } fn has_buffered_data(&self) -> bool { self.buffer_index < self.buffer_size } fn take_pure_escape(&mut self) -> bool { if self.buffer_index == 0 && self.buffer_size == 1 && self.buffer[0] == ESCAPE { // This will set self.might_have_more_data = true, which is fine, // because that only gets set to true when buffer_size == N, but // we just checked that it is 1 and not N. self.clear(); return true; } false } fn read_more_if_needed(&mut self) -> Option { if self.has_buffered_data() { return None; } self.clear(); match read_and_retry_on_interrupt(&mut self.input, &mut self.buffer) { Ok(bytes_read) => { self.buffer_size = bytes_read; self.might_have_more_data = bytes_read == N; None } Err(err) => Some(err), } } } impl Iterator for BufferedInput { type Item = io::Result; fn next(&mut self) -> Option> { if !self.has_buffered_data() { return None; } Some(Ok(self.next_u8())) } } struct TuiInput { poll_fds: [libc::pollfd; 2], sigwinch_pipe: UnixStream, buffered_input: BufferedInput, } impl TuiInput { fn new(input: Stdin, sigwinch_pipe: UnixStream) -> TuiInput { let sigwinch_fd = sigwinch_pipe.as_raw_fd(); let stdin_fd = input.as_raw_fd(); let poll_fds: [libc::pollfd; 2] = [ libc::pollfd { fd: sigwinch_fd, events: libc::POLLIN, revents: 0, }, libc::pollfd { fd: stdin_fd, events: libc::POLLIN, revents: 0, }, ]; TuiInput { poll_fds, sigwinch_pipe, buffered_input: BufferedInput::new(input), } } fn get_event_from_buffered_input(&mut self) -> Option> { if !self.buffered_input.has_buffered_data() { if let Some(err) = self.buffered_input.read_more_if_needed() { return Some(Err(err)); } } if self.buffered_input.take_pure_escape() { return Some(Ok(TuiEvent::KeyEvent(Key::Esc))); } match self.buffered_input.next() { Some(Ok(byte)) => match parse_event(byte, &mut self.buffered_input) { Ok(Event::Key(k)) => Some(Ok(TuiEvent::KeyEvent(k))), Ok(Event::Mouse(m)) => Some(Ok(TuiEvent::MouseEvent(m))), Ok(Event::Unsupported(bytes)) => Some(Ok(TuiEvent::Unknown(bytes))), Err(err) => Some(Err(err)), }, Some(Err(err)) => Some(Err(err)), None => None, } } } impl Iterator for TuiInput { type Item = io::Result; fn next(&mut self) -> Option> { if self.buffered_input.might_have_buffered_data() { return self.get_event_from_buffered_input(); } let poll_res: Option; loop { match unsafe { libc::poll(self.poll_fds.as_mut_ptr(), 2, POLL_INFINITE_TIMEOUT) } { -1 => { let err = io::Error::last_os_error(); if err.kind() != io::ErrorKind::Interrupted { poll_res = Some(err); break; } // Try poll again. } _ => { poll_res = None; break; } }; } if let Some(poll_err) = poll_res { return Some(Err(poll_err)); } if self.poll_fds[SIGWINCH_PIPE_INDEX].revents & libc::POLLIN != 0 { // Just make this big enough to absorb a bunch of unacknowledged SIGWINCHes. let mut buf = [0; 32]; let _ = self.sigwinch_pipe.read(&mut buf); return Some(Ok(TuiEvent::WinChEvent)); } self.get_event_from_buffered_input() } } #[derive(Debug)] pub enum TuiEvent { WinChEvent, KeyEvent(Key), MouseEvent(MouseEvent), Unknown(Vec), }