From b65328c304fe409fd185fbd7b5a4c8e35cd701d7 Mon Sep 17 00:00:00 2001 From: Ticki Date: Tue, 15 Mar 2016 20:32:25 +0100 Subject: [PATCH] Asynchronous key events --- README.md | 3 +- examples/async.rs | 35 ++++++++++++++++++++++ examples/simple.rs | 1 + src/async.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++ src/control.rs | 42 +++++++++++++------------- src/input.rs | 47 +++++++++++++++++++++++------ src/lib.rs | 3 ++ src/size.rs | 20 ++++++------- 8 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 examples/async.rs create mode 100644 src/async.rs diff --git a/README.md b/README.md index 757e958..b601438 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Features - Redox support. - 256-color mode. - Panic-free error handling. -- Special keys events. +- Special keys events (modifiers, special keys, etc.). +- Asynchronous key events. and much more. diff --git a/examples/async.rs b/examples/async.rs new file mode 100644 index 0000000..8053ee6 --- /dev/null +++ b/examples/async.rs @@ -0,0 +1,35 @@ +extern crate libterm; + +use libterm::{TermWrite, IntoRawMode, async_stdin}; +use std::io::{Read, Write, stdout, stdin}; +use std::thread; +use std::time::Duration; + +fn main() { + let stdout = stdout(); + let mut stdout = stdout.lock().into_raw_mode().unwrap(); + let mut stdin = async_stdin().bytes(); + + stdout.clear().unwrap(); + stdout.goto(0, 0).unwrap(); + + loop { + stdout.clear_line().unwrap(); + + let b = stdin.next(); + write!(stdout, "\r{:?} <- This demonstrates the async read input char. Between each update a 100 ms. is waited, simply to demonstrate the async fashion. \n\r", b).unwrap(); + if let Some(Ok(b'q')) = b { + break; + } + + stdout.flush().unwrap(); + + thread::sleep(Duration::from_millis(50)); + stdout.write(b"# ").unwrap(); + stdout.flush().unwrap(); + thread::sleep(Duration::from_millis(50)); + stdout.write(b"\r #").unwrap(); + stdout.goto(0, 0).unwrap(); + stdout.flush().unwrap(); + } +} diff --git a/examples/simple.rs b/examples/simple.rs index 61bd7ef..e1d577c 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -7,6 +7,7 @@ fn main() { let stdout = stdout(); let mut stdout = stdout.lock().into_raw_mode().unwrap(); let stdin = stdin(); + let stdin = stdin.lock(); stdout.goto(5, 5).unwrap(); stdout.clear().unwrap(); diff --git a/src/async.rs b/src/async.rs new file mode 100644 index 0000000..05f424c --- /dev/null +++ b/src/async.rs @@ -0,0 +1,75 @@ +use std::io::{self, Read}; +use std::sync::mpsc; +use std::thread; + +/// Construct an asynchronous handle to the standard input. +/// +/// This allows you to read from standard input _without blocking_ the current thread. +/// Specifically, it works by firing up another thread to handle the event stream, which will then +/// be buffered in a mpsc queue, which will eventually be read by the current thread. +/// +/// Note that this will acquire the Mutex lock on the standard input, making all future stdin +/// construction hang the program. +pub fn async_stdin() -> AsyncReader { + let (send, recv) = mpsc::channel(); + + thread::spawn(move || { + let stdin = io::stdin(); + for i in stdin.lock().bytes() { + if send.send(i).is_err() { + return; + } + } + }); + + AsyncReader { + recv: recv, + } +} + +/// An asynchronous reader. +pub struct AsyncReader { + /// The underlying mpsc receiver. + #[doc(hidden)] + pub recv: mpsc::Receiver>, +} + +impl Read for AsyncReader { + /// Read from the byte stream. + /// + /// This will never block, but try to drain the event queue until empty. If the total number of + /// bytes written is lower than the buffer's length, the event queue is empty or that the event + /// stream halted. + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let mut total = 0; + + loop { + match self.recv.try_recv() { + Ok(Ok(b)) => { + buf[total] = b; + total += 1; + + if total == buf.len() { + break; + } + }, + Ok(Err(e)) => return Err(e), + Err(_) => break, + } + } + + Ok(total) + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::io::Read; + + #[test] + fn test_async_stdin() { + let stdin = async_stdin(); + stdin.bytes().next(); + } +} diff --git a/src/control.rs b/src/control.rs index 1bcd41d..57211e5 100644 --- a/src/control.rs +++ b/src/control.rs @@ -1,4 +1,4 @@ -use std::io::{Write, Result as IoResult}; +use std::io::{self, Write}; use {Color, Style}; /// Extension to the `Write` trait. @@ -8,39 +8,39 @@ use {Color, Style}; pub trait TermWrite { /// Print the CSI (control sequence introducer) followed by a byte string. - fn csi(&mut self, b: &[u8]) -> IoResult; + fn csi(&mut self, b: &[u8]) -> io::Result; /// Print OSC (operating system command) followed by a byte string. - fn osc(&mut self, b: &[u8]) -> IoResult; + fn osc(&mut self, b: &[u8]) -> io::Result; /// Print OSC (device control string) followed by a byte string. - fn dsc(&mut self, b: &[u8]) -> IoResult; + fn dsc(&mut self, b: &[u8]) -> io::Result; /// Clear the entire screen. - fn clear(&mut self) -> IoResult { + fn clear(&mut self) -> io::Result { self.csi(b"2J") } /// Clear everything _after_ the cursor. - fn clear_after(&mut self) -> IoResult { + fn clear_after(&mut self) -> io::Result { self.csi(b"J") } /// Clear everything _before_ the cursor. - fn clear_before(&mut self) -> IoResult { + fn clear_before(&mut self) -> io::Result { self.csi(b"1J") } /// Clear the current line. - fn clear_line(&mut self) -> IoResult { + fn clear_line(&mut self) -> io::Result { self.csi(b"2K") } /// Clear from the cursor until newline. - fn clear_until_newline(&mut self) -> IoResult { + fn clear_until_newline(&mut self) -> io::Result { self.csi(b"K") } /// Show the cursor. - fn show_cursor(&mut self) -> IoResult { + fn show_cursor(&mut self) -> io::Result { self.csi(b"?25h") } /// Hide the cursor. - fn hide_cursor(&mut self) -> IoResult { + fn hide_cursor(&mut self) -> io::Result { self.csi(b"?25l") } @@ -51,21 +51,21 @@ pub trait TermWrite { /// Reset the rendition mode. /// /// This will reset both the current style and color. - fn reset(&mut self) -> IoResult { + fn reset(&mut self) -> io::Result { self.csi(b"m") } /// Restore the defaults. /// /// This will reset color, position, cursor state, and so on. It is recommended that you use /// this before you exit your program, to avoid messing up the user's terminal. - fn restore(&mut self) -> IoResult { + fn restore(&mut self) -> io::Result { Ok(try!(self.reset()) + try!(self.clear()) + try!(self.goto(0, 0)) + try!(self.show_cursor())) } /// Go to a given position. /// /// The position is 0-based. - fn goto(&mut self, mut x: u16, mut y: u16) -> IoResult { + fn goto(&mut self, mut x: u16, mut y: u16) -> io::Result { x += 1; y += 1; @@ -85,7 +85,7 @@ pub trait TermWrite { ]) } /// Set graphic rendition. - fn rendition(&mut self, r: u8) -> IoResult { + fn rendition(&mut self, r: u8) -> io::Result { self.csi(&[ b'0' + r / 100, b'0' + r / 10 % 10, @@ -94,7 +94,7 @@ pub trait TermWrite { ]) } /// Set foreground color - fn color(&mut self, color: Color) -> IoResult { + fn color(&mut self, color: Color) -> io::Result { let ansi = color.to_ansi_val(); self.csi(&[ b'3', @@ -109,7 +109,7 @@ pub trait TermWrite { ]) } /// Set background color - fn bg_color(&mut self, color: Color) -> IoResult { + fn bg_color(&mut self, color: Color) -> io::Result { let ansi = color.to_ansi_val(); self.csi(&[ b'4', @@ -124,19 +124,19 @@ pub trait TermWrite { ]) } /// Set rendition mode (SGR). - fn style(&mut self, mode: Style) -> IoResult { + fn style(&mut self, mode: Style) -> io::Result { self.rendition(mode as u8) } } impl TermWrite for W { - fn csi(&mut self, b: &[u8]) -> IoResult { + fn csi(&mut self, b: &[u8]) -> io::Result { Ok(try!(self.write(b"\x1B[")) + try!(self.write(b))) } - fn osc(&mut self, b: &[u8]) -> IoResult { + fn osc(&mut self, b: &[u8]) -> io::Result { Ok(try!(self.write(b"\x1B]")) + try!(self.write(b))) } - fn dsc(&mut self, b: &[u8]) -> IoResult { + fn dsc(&mut self, b: &[u8]) -> io::Result { Ok(try!(self.write(b"\x1BP")) + try!(self.write(b))) } } diff --git a/src/input.rs b/src/input.rs index 5045b62..0095f50 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,11 +1,14 @@ -use std::io::{Read, Write, Error, ErrorKind, Result as IoResult}; -use IntoRawMode; +use std::io::{self, Read, Write}; +use std::thread; +use std::sync::mpsc; + +use {IntoRawMode, AsyncReader}; #[cfg(feature = "nightly")] use std::io::{Chars, CharsError}; /// A key. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +#[derive(Debug)] pub enum Key { /// Backspace. Backspace, @@ -27,9 +30,8 @@ pub enum Key { Ctrl(char), /// Invalid character code. Invalid, - // TODO handle errors better? /// IO error. - Error, + Error(io::Error), /// Null byte. Null, @@ -69,7 +71,7 @@ impl>> Iterator for Keys { None => None, Some('\0') => Some(Key::Null), Some(Ok(c)) => Some(Key::Char(c)), - Some(Err(_)) => Some(Key::Error), + Some(Err(e)) => Some(Key::Error(e)), } } } @@ -84,7 +86,12 @@ pub trait TermRead { /// /// EOT and ETX will abort the prompt, returning `None`. Newline or carriage return will /// complete the password input. - fn read_passwd(&mut self, writer: &mut W) -> IoResult>; + fn read_passwd(&mut self, writer: &mut W) -> io::Result>; + + /// Turn the reader into a asynchronous reader. + /// + /// This will spawn up another thread listening for event, buffering them in a mpsc queue. + fn into_async(self) -> AsyncReader where Self: Send; } impl TermRead for R { @@ -95,7 +102,7 @@ impl TermRead for R { } } - fn read_passwd(&mut self, writer: &mut W) -> IoResult> { + fn read_passwd(&mut self, writer: &mut W) -> io::Result> { let _raw = try!(writer.into_raw_mode()); let mut passbuf = Vec::with_capacity(30); @@ -108,10 +115,32 @@ impl TermRead for R { } } - let passwd = try!(String::from_utf8(passbuf).map_err(|e| Error::new(ErrorKind::InvalidData, e))); + let passwd = try!(String::from_utf8(passbuf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))); Ok(Some(passwd)) } + + fn into_async(self) -> AsyncReader where R: Send + 'static { + let (send, recv) = mpsc::channel(); + + thread::spawn(move || { + let mut reader = self; + loop { + let mut buf = [0]; + if send.send(if let Err(k) = reader.read(&mut buf) { + Err(k) + } else { + Ok(buf[0]) + }).is_err() { + return; + }; + } + }); + + AsyncReader { + recv: recv, + } + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 9f6f301..c459fe7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,9 @@ mod termios; mod control; pub use control::TermWrite; +mod async; +pub use async::{AsyncReader, async_stdin}; + mod input; pub use input::{TermRead, Key}; #[cfg(feature = "nightly")] diff --git a/src/size.rs b/src/size.rs index 671edfb..f7e9af9 100644 --- a/src/size.rs +++ b/src/size.rs @@ -1,4 +1,4 @@ -use std::io::{Error, ErrorKind, Result as IoResult}; +use std::io; #[cfg(not(target_os = "redox"))] use libc::c_ushort; @@ -27,7 +27,7 @@ fn tiocgwinsz() -> u32 { /// Get the size of the terminal. #[cfg(not(target_os = "redox"))] -pub fn terminal_size() -> IoResult<(usize, usize)> { +pub fn terminal_size() -> io::Result<(usize, usize)> { use libc::ioctl; use libc::STDOUT_FILENO; @@ -38,22 +38,22 @@ pub fn terminal_size() -> IoResult<(usize, usize)> { if ioctl(STDOUT_FILENO, tiocgwinsz(), &mut size as *mut _) == 0 { Ok((size.col as usize, size.row as usize)) } else { - Err(Error::new(ErrorKind::Other, "Unable to get the terminal size.")) + Err(io::Error::new(io::ErrorKind::Other, "Unable to get the terminal size.")) } } } /// Get the size of the terminal. #[cfg(target_os = "redox")] -pub fn terminal_size() -> IoResult<(usize, usize), TerminalError> { - fn get_int(s: &'static str) -> IoResult { - use std::env::{VarError, var}; +pub fn terminal_size() -> io::Result<(usize, usize)> { + fn get_int(s: &'static str) -> io::Result { + use std::env; - var(s).map_err(|e| match e { - VarError::NotPresent => Error::new(ErrorKind::NotFound, e), - VarError::NotUnicode(u) => Error::new(ErrorKind::InvalidData, u), + env::var(s).map_err(|e| match e { + env::VarError::NotPresent => io::Error::new(io::ErrorKind::NotFound, e), + env::VarError::NotUnicode(u) => io::Error::new(io::ErrorKind::InvalidData, u), }).and_then(|x| { - x.parse().map_err(|e| Error::new(ErrorKind::InvalidData, e)) + x.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) }) }