Asynchronous key events

This commit is contained in:
Ticki 2016-03-15 20:32:25 +01:00
parent 73ae06124a
commit b65328c304
8 changed files with 185 additions and 41 deletions

View File

@ -24,7 +24,8 @@ Features
- Redox support. - Redox support.
- 256-color mode. - 256-color mode.
- Panic-free error handling. - Panic-free error handling.
- Special keys events. - Special keys events (modifiers, special keys, etc.).
- Asynchronous key events.
and much more. and much more.

35
examples/async.rs Normal file
View File

@ -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();
}
}

View File

@ -7,6 +7,7 @@ fn main() {
let stdout = stdout(); let stdout = stdout();
let mut stdout = stdout.lock().into_raw_mode().unwrap(); let mut stdout = stdout.lock().into_raw_mode().unwrap();
let stdin = stdin(); let stdin = stdin();
let stdin = stdin.lock();
stdout.goto(5, 5).unwrap(); stdout.goto(5, 5).unwrap();
stdout.clear().unwrap(); stdout.clear().unwrap();

75
src/async.rs Normal file
View File

@ -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<io::Result<u8>>,
}
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<usize> {
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();
}
}

View File

@ -1,4 +1,4 @@
use std::io::{Write, Result as IoResult}; use std::io::{self, Write};
use {Color, Style}; use {Color, Style};
/// Extension to the `Write` trait. /// Extension to the `Write` trait.
@ -8,39 +8,39 @@ use {Color, Style};
pub trait TermWrite { pub trait TermWrite {
/// Print the CSI (control sequence introducer) followed by a byte string. /// Print the CSI (control sequence introducer) followed by a byte string.
fn csi(&mut self, b: &[u8]) -> IoResult<usize>; fn csi(&mut self, b: &[u8]) -> io::Result<usize>;
/// Print OSC (operating system command) followed by a byte string. /// Print OSC (operating system command) followed by a byte string.
fn osc(&mut self, b: &[u8]) -> IoResult<usize>; fn osc(&mut self, b: &[u8]) -> io::Result<usize>;
/// Print OSC (device control string) followed by a byte string. /// Print OSC (device control string) followed by a byte string.
fn dsc(&mut self, b: &[u8]) -> IoResult<usize>; fn dsc(&mut self, b: &[u8]) -> io::Result<usize>;
/// Clear the entire screen. /// Clear the entire screen.
fn clear(&mut self) -> IoResult<usize> { fn clear(&mut self) -> io::Result<usize> {
self.csi(b"2J") self.csi(b"2J")
} }
/// Clear everything _after_ the cursor. /// Clear everything _after_ the cursor.
fn clear_after(&mut self) -> IoResult<usize> { fn clear_after(&mut self) -> io::Result<usize> {
self.csi(b"J") self.csi(b"J")
} }
/// Clear everything _before_ the cursor. /// Clear everything _before_ the cursor.
fn clear_before(&mut self) -> IoResult<usize> { fn clear_before(&mut self) -> io::Result<usize> {
self.csi(b"1J") self.csi(b"1J")
} }
/// Clear the current line. /// Clear the current line.
fn clear_line(&mut self) -> IoResult<usize> { fn clear_line(&mut self) -> io::Result<usize> {
self.csi(b"2K") self.csi(b"2K")
} }
/// Clear from the cursor until newline. /// Clear from the cursor until newline.
fn clear_until_newline(&mut self) -> IoResult<usize> { fn clear_until_newline(&mut self) -> io::Result<usize> {
self.csi(b"K") self.csi(b"K")
} }
/// Show the cursor. /// Show the cursor.
fn show_cursor(&mut self) -> IoResult<usize> { fn show_cursor(&mut self) -> io::Result<usize> {
self.csi(b"?25h") self.csi(b"?25h")
} }
/// Hide the cursor. /// Hide the cursor.
fn hide_cursor(&mut self) -> IoResult<usize> { fn hide_cursor(&mut self) -> io::Result<usize> {
self.csi(b"?25l") self.csi(b"?25l")
} }
@ -51,21 +51,21 @@ pub trait TermWrite {
/// Reset the rendition mode. /// Reset the rendition mode.
/// ///
/// This will reset both the current style and color. /// This will reset both the current style and color.
fn reset(&mut self) -> IoResult<usize> { fn reset(&mut self) -> io::Result<usize> {
self.csi(b"m") self.csi(b"m")
} }
/// Restore the defaults. /// Restore the defaults.
/// ///
/// This will reset color, position, cursor state, and so on. It is recommended that you use /// 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. /// this before you exit your program, to avoid messing up the user's terminal.
fn restore(&mut self) -> IoResult<usize> { fn restore(&mut self) -> io::Result<usize> {
Ok(try!(self.reset()) + try!(self.clear()) + try!(self.goto(0, 0)) + try!(self.show_cursor())) Ok(try!(self.reset()) + try!(self.clear()) + try!(self.goto(0, 0)) + try!(self.show_cursor()))
} }
/// Go to a given position. /// Go to a given position.
/// ///
/// The position is 0-based. /// The position is 0-based.
fn goto(&mut self, mut x: u16, mut y: u16) -> IoResult<usize> { fn goto(&mut self, mut x: u16, mut y: u16) -> io::Result<usize> {
x += 1; x += 1;
y += 1; y += 1;
@ -85,7 +85,7 @@ pub trait TermWrite {
]) ])
} }
/// Set graphic rendition. /// Set graphic rendition.
fn rendition(&mut self, r: u8) -> IoResult<usize> { fn rendition(&mut self, r: u8) -> io::Result<usize> {
self.csi(&[ self.csi(&[
b'0' + r / 100, b'0' + r / 100,
b'0' + r / 10 % 10, b'0' + r / 10 % 10,
@ -94,7 +94,7 @@ pub trait TermWrite {
]) ])
} }
/// Set foreground color /// Set foreground color
fn color(&mut self, color: Color) -> IoResult<usize> { fn color(&mut self, color: Color) -> io::Result<usize> {
let ansi = color.to_ansi_val(); let ansi = color.to_ansi_val();
self.csi(&[ self.csi(&[
b'3', b'3',
@ -109,7 +109,7 @@ pub trait TermWrite {
]) ])
} }
/// Set background color /// Set background color
fn bg_color(&mut self, color: Color) -> IoResult<usize> { fn bg_color(&mut self, color: Color) -> io::Result<usize> {
let ansi = color.to_ansi_val(); let ansi = color.to_ansi_val();
self.csi(&[ self.csi(&[
b'4', b'4',
@ -124,19 +124,19 @@ pub trait TermWrite {
]) ])
} }
/// Set rendition mode (SGR). /// Set rendition mode (SGR).
fn style(&mut self, mode: Style) -> IoResult<usize> { fn style(&mut self, mode: Style) -> io::Result<usize> {
self.rendition(mode as u8) self.rendition(mode as u8)
} }
} }
impl<W: Write> TermWrite for W { impl<W: Write> TermWrite for W {
fn csi(&mut self, b: &[u8]) -> IoResult<usize> { fn csi(&mut self, b: &[u8]) -> io::Result<usize> {
Ok(try!(self.write(b"\x1B[")) + try!(self.write(b))) Ok(try!(self.write(b"\x1B[")) + try!(self.write(b)))
} }
fn osc(&mut self, b: &[u8]) -> IoResult<usize> { fn osc(&mut self, b: &[u8]) -> io::Result<usize> {
Ok(try!(self.write(b"\x1B]")) + try!(self.write(b))) Ok(try!(self.write(b"\x1B]")) + try!(self.write(b)))
} }
fn dsc(&mut self, b: &[u8]) -> IoResult<usize> { fn dsc(&mut self, b: &[u8]) -> io::Result<usize> {
Ok(try!(self.write(b"\x1BP")) + try!(self.write(b))) Ok(try!(self.write(b"\x1BP")) + try!(self.write(b)))
} }
} }

View File

@ -1,11 +1,14 @@
use std::io::{Read, Write, Error, ErrorKind, Result as IoResult}; use std::io::{self, Read, Write};
use IntoRawMode; use std::thread;
use std::sync::mpsc;
use {IntoRawMode, AsyncReader};
#[cfg(feature = "nightly")] #[cfg(feature = "nightly")]
use std::io::{Chars, CharsError}; use std::io::{Chars, CharsError};
/// A key. /// A key.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] #[derive(Debug)]
pub enum Key { pub enum Key {
/// Backspace. /// Backspace.
Backspace, Backspace,
@ -27,9 +30,8 @@ pub enum Key {
Ctrl(char), Ctrl(char),
/// Invalid character code. /// Invalid character code.
Invalid, Invalid,
// TODO handle errors better?
/// IO error. /// IO error.
Error, Error(io::Error),
/// Null byte. /// Null byte.
Null, Null,
@ -69,7 +71,7 @@ impl<I: Iterator<Item = Result<char, CharsError>>> Iterator for Keys<I> {
None => None, None => None,
Some('\0') => Some(Key::Null), Some('\0') => Some(Key::Null),
Some(Ok(c)) => Some(Key::Char(c)), 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 /// EOT and ETX will abort the prompt, returning `None`. Newline or carriage return will
/// complete the password input. /// complete the password input.
fn read_passwd<W: Write>(&mut self, writer: &mut W) -> IoResult<Option<String>>; fn read_passwd<W: Write>(&mut self, writer: &mut W) -> io::Result<Option<String>>;
/// 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<R: Read> TermRead for R { impl<R: Read> TermRead for R {
@ -95,7 +102,7 @@ impl<R: Read> TermRead for R {
} }
} }
fn read_passwd<W: Write>(&mut self, writer: &mut W) -> IoResult<Option<String>> { fn read_passwd<W: Write>(&mut self, writer: &mut W) -> io::Result<Option<String>> {
let _raw = try!(writer.into_raw_mode()); let _raw = try!(writer.into_raw_mode());
let mut passbuf = Vec::with_capacity(30); let mut passbuf = Vec::with_capacity(30);
@ -108,10 +115,32 @@ impl<R: Read> 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)) 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)] #[cfg(test)]

View File

@ -19,6 +19,9 @@ mod termios;
mod control; mod control;
pub use control::TermWrite; pub use control::TermWrite;
mod async;
pub use async::{AsyncReader, async_stdin};
mod input; mod input;
pub use input::{TermRead, Key}; pub use input::{TermRead, Key};
#[cfg(feature = "nightly")] #[cfg(feature = "nightly")]

View File

@ -1,4 +1,4 @@
use std::io::{Error, ErrorKind, Result as IoResult}; use std::io;
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
use libc::c_ushort; use libc::c_ushort;
@ -27,7 +27,7 @@ fn tiocgwinsz() -> u32 {
/// Get the size of the terminal. /// Get the size of the terminal.
#[cfg(not(target_os = "redox"))] #[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::ioctl;
use libc::STDOUT_FILENO; 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 { if ioctl(STDOUT_FILENO, tiocgwinsz(), &mut size as *mut _) == 0 {
Ok((size.col as usize, size.row as usize)) Ok((size.col as usize, size.row as usize))
} else { } 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. /// Get the size of the terminal.
#[cfg(target_os = "redox")] #[cfg(target_os = "redox")]
pub fn terminal_size() -> IoResult<(usize, usize), TerminalError> { pub fn terminal_size() -> io::Result<(usize, usize)> {
fn get_int(s: &'static str) -> IoResult<usize> { fn get_int(s: &'static str) -> io::Result<usize> {
use std::env::{VarError, var}; use std::env;
var(s).map_err(|e| match e { env::var(s).map_err(|e| match e {
VarError::NotPresent => Error::new(ErrorKind::NotFound, e), env::VarError::NotPresent => io::Error::new(io::ErrorKind::NotFound, e),
VarError::NotUnicode(u) => Error::new(ErrorKind::InvalidData, u), env::VarError::NotUnicode(u) => io::Error::new(io::ErrorKind::InvalidData, u),
}).and_then(|x| { }).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))
}) })
} }