minicrossterm/crossterm_input/src/input/input.rs
Timon 1e332daaed
Refactor and API stabilization (#115)
- Major refactor and cleanup.
- Improved performance; 
    - No locking when writing to stdout. 
    - UNIX doesn't have any dynamic dispatch anymore. 
    - Windows has improved the way to check if ANSI modes are enabled.
    - Removed lot's of complex API calls: `from_screen`, `from_output`
    - Removed `Arc<TerminalOutput>` from all internal Api's. 
- Removed termios dependency for UNIX systems.
- Upgraded deps.
- Removed about 1000 lines of code
    - `TerminalOutput` 
    - `Screen`
    - unsafe code
    - Some duplicated code introduced by a previous refactor.
- Raw modes UNIX systems improved     
- Added `NoItalic` attribute
2019-04-10 23:46:30 +02:00

412 lines
15 KiB
Rust

//! A module that contains all the actions related to reading input from the terminal.
//! Like reading a line, reading a character and reading asynchronously.
use super::*;
use std::{io, str};
/// Allows you to read user input.
///
/// # Features:
///
/// - Read character
/// - Read line
/// - Read async
/// - Read async until
/// - Read sync
/// - Wait for key event (terminal pause)
///
/// Check `/examples/` in the library for more specific examples.
pub struct TerminalInput {
#[cfg(windows)]
input: WindowsInput,
#[cfg(unix)]
input: UnixInput,
}
impl TerminalInput {
/// Create a new instance of `TerminalInput` whereon input related actions could be preformed.
pub fn new() -> TerminalInput {
#[cfg(windows)]
let input = WindowsInput::new();
#[cfg(unix)]
let input = UnixInput::new();
TerminalInput { input }
}
/// Read one line from the user input.
///
/// # Remark
/// This function is not work when raw screen is turned on.
/// When you do want to read a line in raw mode please, checkout `read_async`, `read_async_until` or `read_sync`.
/// Not sure what 'raw mode' is, checkout the 'crossterm_screen' crate.
///
/// # Example
/// ```rust
/// let input = input();
/// match input.read_line() {
/// Ok(s) => println!("string typed: {}", s),
/// Err(e) => println!("error: {}", e),
/// }
/// ```
pub fn read_line(&self) -> io::Result<String> {
let mut rv = String::new();
io::stdin().read_line(&mut rv)?;
let len = rv.trim_right_matches(&['\r', '\n'][..]).len();
rv.truncate(len);
Ok(rv)
}
/// Read one character from the user input
///
/// ```rust
/// let input = input();
///
/// match input.read_char() {
/// Ok(c) => println!("character pressed: {}", c),
/// Err(e) => println!("error: {}", e),
/// }
/// ```
pub fn read_char(&self) -> io::Result<char> {
self.input.read_char()
}
/// Read the input asynchronously, which means that input events are gathered on the background and will be queued for you to read.
///
/// If you want a blocking, or less resource consuming read to happen use `read_sync()`, this will leave a way all the thread and queueing and will be a blocking read.
///
/// This is the same as `read_async()` but stops reading when a certain character is hit.
///
/// # Remarks
/// - Readings won't be blocking calls.
/// A thread will be fired to read input, on unix systems from TTY and on windows WinApi
/// `ReadConsoleW` will be used.
/// - Input events read from the user will be queued on a MPSC-channel.
/// - The reading thread will be cleaned up when it drops.
/// - Requires 'raw screen to be enabled'.
/// Not sure what this is? Please checkout the 'crossterm_screen' crate.
///
/// # Examples
/// Please checkout the example folder in the repository.
pub fn read_async(&self) -> AsyncReader {
self.input.read_async()
}
/// Read the input asynchronously until a certain character is hit, which means that input events are gathered on the background and will be queued for you to read.
///
/// If you want a blocking, or less resource consuming read to happen use `read_sync()`, this will leave a way all the thread and queueing and will be a blocking read.
///
/// This is the same as `read_async()` but stops reading when a certain character is hit.
///
/// # Remarks
/// - Readings won't be blocking calls.
/// A thread will be fired to read input, on unix systems from TTY and on windows WinApi
/// `ReadConsoleW` will be used.
/// - Input events read from the user will be queued on a MPSC-channel.
/// - The reading thread will be cleaned up when it drops.
/// - Requires 'raw screen to be enabled'.
/// Not sure what this is? Please checkout the 'crossterm_screen' crate.
///
/// # Examples
/// Please checkout the example folder in the repository.
pub fn read_until_async(&self, delimiter: u8) -> AsyncReader {
self.input.read_until_async(delimiter)
}
/// Read the input synchronously from the user, which means that reading call wil be blocking ones.
/// It also uses less resources than the `AsyncReader` because background thread and queues are left away.
///
/// In case you don't want the reading to block your program you could consider `read_async`.
///
/// # Remark
/// - Readings will be blocking calls.
///
/// # Examples
/// Please checkout the example folder in the repository.
pub fn read_sync(&self) -> SyncReader {
self.input.read_sync()
}
/// Enable mouse events to be captured.
///
/// When enabling mouse input you will be able to capture, mouse movements, pressed buttons and locations.
///
/// # Remark
/// - Mouse events will be send over the reader created with `read_async`, `read_async_until`, `read_sync`.
pub fn enable_mouse_mode(&self) -> Result<()> {
self.input.enable_mouse_mode()
}
/// Disable mouse events to be captured.
///
/// When disabling mouse input you won't be able to capture, mouse movements, pressed buttons and locations anymore.
pub fn disable_mouse_mode(&self) -> Result<()> {
self.input.disable_mouse_mode()
}
}
/// Get a `TerminalInput` instance whereon input related actions can be performed.
pub fn input() -> TerminalInput {
TerminalInput::new()
}
/// Parse an Event from `item` and possibly subsequent bytes through `iter`.
pub(crate) fn parse_event<I>(item: u8, iter: &mut I) -> Result<InputEvent>
where
I: Iterator<Item = u8>,
{
let error = ErrorKind::IoError(io::Error::new(
io::ErrorKind::Other,
"Could not parse an event",
));
let input_event = match item {
b'\x1B' => {
let a = iter.next();
// This is an escape character, leading a control sequence.
match a {
Some(b'O') => {
match iter.next() {
// F1-F4
Some(val @ b'P'...b'S') => {
InputEvent::Keyboard(KeyEvent::F(1 + val - b'P'))
}
_ => return Err(error),
}
}
Some(b'[') => {
// This is a CSI sequence.
parse_csi(iter)
}
Some(b'\x1B') => InputEvent::Keyboard(KeyEvent::Esc),
Some(c) => {
let ch = parse_utf8_char(c, iter);
InputEvent::Keyboard(KeyEvent::Alt(ch?))
}
None => InputEvent::Keyboard(KeyEvent::Esc),
}
}
b'\n' | b'\r' => InputEvent::Keyboard(KeyEvent::Char('\n')),
b'\t' => InputEvent::Keyboard(KeyEvent::Char('\t')),
b'\x7F' => InputEvent::Keyboard(KeyEvent::Backspace),
c @ b'\x01'...b'\x1A' => {
InputEvent::Keyboard(KeyEvent::Ctrl((c as u8 - 0x1 + b'a') as char))
}
c @ b'\x1C'...b'\x1F' => {
InputEvent::Keyboard(KeyEvent::Ctrl((c as u8 - 0x1C + b'4') as char))
}
b'\0' => InputEvent::Keyboard(KeyEvent::Null),
c => {
let ch = parse_utf8_char(c, iter);
InputEvent::Keyboard(KeyEvent::Char(ch?))
}
};
Ok(input_event)
}
/// Parses a CSI sequence, just after reading ^[
/// Returns Event::Unknown if an unrecognized sequence is found.
/// Most of this parsing code is been taken over from 'termion`.
fn parse_csi<I>(iter: &mut I) -> InputEvent
where
I: Iterator<Item = u8>,
{
match iter.next() {
Some(b'[') => match iter.next() {
// NOTE (@imdaveho): cannot find when this occurs;
// having another '[' after ESC[ not a likely scenario
Some(val @ b'A'...b'E') => InputEvent::Keyboard(KeyEvent::F(1 + val - b'A')),
_ => InputEvent::Unknown,
},
Some(b'D') => InputEvent::Keyboard(KeyEvent::Left),
Some(b'C') => InputEvent::Keyboard(KeyEvent::Right),
Some(b'A') => InputEvent::Keyboard(KeyEvent::Up),
Some(b'B') => InputEvent::Keyboard(KeyEvent::Down),
Some(b'H') => InputEvent::Keyboard(KeyEvent::Home),
Some(b'F') => InputEvent::Keyboard(KeyEvent::End),
Some(b'M') => {
// X10 emulation mouse encoding: ESC [ CB Cx Cy (6 characters only).
// NOTE (@imdaveho): cannot find documentation on this
let mut next = || iter.next().unwrap();
let cb = next() as i8 - 32;
// (1, 1) are the coords for upper left.
let cx = next().saturating_sub(32) as u16;
let cy = next().saturating_sub(32) as u16;
InputEvent::Mouse(match cb & 0b11 {
0 => {
if cb & 0x40 != 0 {
MouseEvent::Press(MouseButton::WheelUp, cx, cy)
} else {
MouseEvent::Press(MouseButton::Left, cx, cy)
}
}
1 => {
if cb & 0x40 != 0 {
MouseEvent::Press(MouseButton::WheelDown, cx, cy)
} else {
MouseEvent::Press(MouseButton::Middle, cx, cy)
}
}
2 => MouseEvent::Press(MouseButton::Right, cx, cy),
3 => MouseEvent::Release(cx, cy),
_ => MouseEvent::Unknown,
})
}
Some(b'<') => {
// xterm mouse handling:
// ESC [ < Cb ; Cx ; Cy (;) (M or m)
let mut buf = Vec::new();
let mut c = iter.next().unwrap();
while match c {
b'm' | b'M' => false,
_ => true,
} {
buf.push(c);
c = iter.next().unwrap();
}
let str_buf = String::from_utf8(buf).unwrap();
let nums = &mut str_buf.split(';');
let cb = nums.next().unwrap().parse::<u16>().unwrap();
let cx = nums.next().unwrap().parse::<u16>().unwrap();
let cy = nums.next().unwrap().parse::<u16>().unwrap();
match cb {
0...2 | 64...65 => {
let button = match cb {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
64 => MouseButton::WheelUp,
65 => MouseButton::WheelDown,
_ => unreachable!(),
};
match c {
b'M' => InputEvent::Mouse(MouseEvent::Press(button, cx, cy)),
b'm' => InputEvent::Mouse(MouseEvent::Release(cx, cy)),
_ => InputEvent::Unknown,
}
}
32 => InputEvent::Mouse(MouseEvent::Hold(cx, cy)),
3 => InputEvent::Mouse(MouseEvent::Release(cx, cy)),
_ => InputEvent::Unknown,
}
}
Some(c @ b'0'...b'9') => {
// Numbered escape code.
let mut buf = Vec::new();
buf.push(c);
let mut character = iter.next().unwrap();
// The final byte of a CSI sequence can be in the range 64-126, so
// let's keep reading anything else.
while character < 64 || character > 126 {
buf.push(character);
character = iter.next().unwrap();
}
match character {
// rxvt mouse encoding:
// ESC [ Cb ; Cx ; Cy ; M
b'M' => {
let str_buf = String::from_utf8(buf).unwrap();
let nums: Vec<u16> = str_buf.split(';').map(|n| n.parse().unwrap()).collect();
let cb = nums[0];
let cx = nums[1];
let cy = nums[2];
let event = match cb {
32 => MouseEvent::Press(MouseButton::Left, cx, cy),
33 => MouseEvent::Press(MouseButton::Middle, cx, cy),
34 => MouseEvent::Press(MouseButton::Right, cx, cy),
35 => MouseEvent::Release(cx, cy),
64 => MouseEvent::Hold(cx, cy),
96 | 97 => MouseEvent::Press(MouseButton::WheelUp, cx, cy),
_ => MouseEvent::Unknown,
};
InputEvent::Mouse(event)
}
// Special key code.
b'~' => {
let str_buf = String::from_utf8(buf).unwrap();
// This CSI sequence can be a list of semicolon-separated numbers.
let nums: Vec<u8> = str_buf.split(';').map(|n| n.parse().unwrap()).collect();
if nums.is_empty() {
return InputEvent::Unknown;
}
// TODO: handle multiple values for key modifiers (ex: values [3, 2] means Shift+Delete)
if nums.len() > 1 {
return InputEvent::Unknown;
}
match nums[0] {
1 | 7 => InputEvent::Keyboard(KeyEvent::Home),
2 => InputEvent::Keyboard(KeyEvent::Insert),
3 => InputEvent::Keyboard(KeyEvent::Delete),
4 | 8 => InputEvent::Keyboard(KeyEvent::End),
5 => InputEvent::Keyboard(KeyEvent::PageUp),
6 => InputEvent::Keyboard(KeyEvent::PageDown),
v @ 11...15 => InputEvent::Keyboard(KeyEvent::F(v - 10)),
v @ 17...21 => InputEvent::Keyboard(KeyEvent::F(v - 11)),
v @ 23...24 => InputEvent::Keyboard(KeyEvent::F(v - 12)),
_ => InputEvent::Unknown,
}
}
_ => InputEvent::Unknown,
}
}
_ => InputEvent::Unknown,
}
}
/// Parse `c` as either a single byte ASCII char or a variable size UTF-8 char.
fn parse_utf8_char<I>(c: u8, iter: &mut I) -> Result<char>
where
I: Iterator<Item = u8>,
{
let error = Err(ErrorKind::IoError(io::Error::new(
io::ErrorKind::Other,
"Input character is not valid UTF-8",
)));
if c.is_ascii() {
Ok(c as char)
} else {
let mut bytes = Vec::new();
bytes.push(c);
while let Some(next) = iter.next() {
bytes.push(next);
if let Ok(st) = str::from_utf8(&bytes) {
return Ok(st.chars().next().unwrap());
}
if bytes.len() >= 4 {
return error;
}
}
return error;
}
}
#[cfg(test)]
#[test]
fn test_parse_utf8() {
let st = "abcéŷ¤£€ù%323";
let ref mut bytes = st.bytes().map(|x| Ok(x));
let chars = st.chars();
for c in chars {
let b = bytes.next().unwrap().unwrap();
assert_eq!(c, parse_utf8_char(b, bytes).unwrap());
}
}