added mouse input

The event system has been reworked to allow the detection of mouse
events as well as key presses.
Xterm, rxvt and X10 emulated escape codes are supported, they are
enabled and disabled by sending the right escape codes when creating a
RawTerminal.

To allow for byte manipulation, which was necessary to implement those
features, the backend iterator has been changed from chars() to bytes()
(with specific treatment of unicode sequences), making the whole crate
not require nightly rustc.
This commit is contained in:
IGI-111 2016-07-19 02:31:34 +02:00
parent 12c0ad04db
commit cc9c32b981
8 changed files with 372 additions and 147 deletions

View File

@ -5,7 +5,3 @@ authors = ["Ticki <Ticki@users.noreply.github.com>"]
[target.'cfg(not(target_os = "redox"))'.dependencies]
libc = "0.2.8"
[features]
default = ["nightly"]
nightly = []

View File

@ -19,21 +19,11 @@ and this crate can generally be considered stable.
## Cargo.toml
For nightly, add
```toml
[dependencies.termion]
git = "https://github.com/ticki/termion.git"
```
For stable,
```toml
[dependencies.termion]
git = "https://github.com/ticki/termion.git"
default-features = false
```
## Features
- Raw mode.
@ -51,6 +41,7 @@ default-features = false
- Special keys events (modifiers, special keys, etc.).
- Allocation-free.
- Asynchronous key events.
- Mouse input
- Carefully tested.
and much more.
@ -92,10 +83,6 @@ For a more complete example, see [a minesweeper implementation](https://github.c
<img src="image.png" width="200">
## TODO
- Mouse input
## License
MIT/X11.

View File

@ -1,6 +1,5 @@
extern crate termion;
#[cfg(feature = "nightly")]
fn main() {
use termion::{TermRead, TermWrite, IntoRawMode, Key};
use std::io::{Write, stdout, stdin};
@ -27,7 +26,6 @@ fn main() {
Key::Up => println!(""),
Key::Down => println!(""),
Key::Backspace => println!("×"),
Key::Invalid => println!("???"),
_ => {},
}
stdout.flush().unwrap();
@ -35,8 +33,3 @@ fn main() {
stdout.show_cursor().unwrap();
}
#[cfg(not(feature = "nightly"))]
fn main() {
println!("To run this example, you need to enable the `nightly` feature. Use Rust nightly and compile with `--features nightly`.")
}

35
examples/test.rs Normal file
View File

@ -0,0 +1,35 @@
extern crate termion;
fn main() {
use termion::{TermRead, TermWrite, IntoRawMode, Key, Event};
use std::io::{Write, stdout, stdin};
let stdin = stdin();
let mut stdout = stdout().into_raw_mode().unwrap();
stdout.clear().unwrap();
stdout.goto(0, 0).unwrap();
stdout.write(b"q to exit. Type stuff, use alt, click around...").unwrap();
stdout.flush().unwrap();
let mut x = 0;
let mut y = 0;
for c in stdin.events() {
stdout.goto(5, 5).unwrap();
stdout.clear_line().unwrap();
match c.unwrap() {
Event::KeyEvent(Key::Char('q')) => break,
Event::MouseEvent(val, a, b) => {
x = a;
y = b;
println!("{:?}", Event::MouseEvent(val, a, b));
},
val => println!("{:?}", val),
}
stdout.goto(x, y).unwrap();
stdout.flush().unwrap();
}
stdout.show_cursor().unwrap();
}

277
src/event.rs Normal file
View File

@ -0,0 +1,277 @@
use std::io::{Error, ErrorKind};
use std::ascii::AsciiExt;
use std::str;
/// An event reported by the terminal.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Event {
/// A key press.
KeyEvent(Key),
/// A mouse button press, release or wheel use at specific coordinates.
MouseEvent(Mouse, u16, u16),
/// An event that cannot currently be evaluated.
Unsupported,
}
/// A mouse related event.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mouse {
/// A mouse button was pressed.
Press(MouseButton),
/// A mouse button was released.
Release,
}
/// A mouse button.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum MouseButton {
/// The left mouse button.
Left,
/// The right mouse button.
Right,
/// The middle mouse button.
Middle,
/// Mouse wheel is going up.
///
/// This event is typically only used with Mouse::Press.
WheelUp,
/// Mouse wheel is going down.
///
/// This event is typically only used with Mouse::Press.
WheelDown,
}
/// A key.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Key {
/// Backspace.
Backspace,
/// Left arrow.
Left,
/// Right arrow.
Right,
/// Up arrow.
Up,
/// Down arrow.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page Up key.
PageUp,
/// Page Down key.
PageDown,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// Function keys.
///
/// Only function keys 1 through 12 are supported.
F(u8),
/// Normal character.
Char(char),
/// Alt modified character.
Alt(char),
/// Ctrl modified character.
///
/// Note that certain keys may not be modifiable with `ctrl`, due to limitations of terminals.
Ctrl(char),
/// Null byte.
Null,
#[allow(missing_docs)]
#[doc(hidden)]
__IsNotComplete
}
pub fn parse_event<I>(item: Result<u8, Error>, iter: &mut I) -> Result<Event, Error>
where I: Iterator<Item = Result<u8, Error>>
{
let error = Err(Error::new(ErrorKind::Other, "Could not parse an event"));
match item {
Ok(b'\x1B') => {
Ok(match iter.next() {
Some(Ok(b'O')) => {
match iter.next() {
Some(Ok(b'P')) => Event::KeyEvent(Key::F(1)),
Some(Ok(b'Q')) => Event::KeyEvent(Key::F(2)),
Some(Ok(b'R')) => Event::KeyEvent(Key::F(3)),
Some(Ok(b'S')) => Event::KeyEvent(Key::F(4)),
_ => return error,
}
}
Some(Ok(b'[')) => {
match iter.next() {
Some(Ok(b'D')) => Event::KeyEvent(Key::Left),
Some(Ok(b'C')) => Event::KeyEvent(Key::Right),
Some(Ok(b'A')) => Event::KeyEvent(Key::Up),
Some(Ok(b'B')) => Event::KeyEvent(Key::Down),
Some(Ok(b'H')) => Event::KeyEvent(Key::Home),
Some(Ok(b'F')) => Event::KeyEvent(Key::End),
Some(Ok(b'M')) => {
// X10 emulation mouse encoding: ESC [ CB Cx Cy (6 characters only)
let cb = iter.next().unwrap().unwrap() as i8 - 32;
// (1, 1) are the coords for upper left
let cx = (iter.next().unwrap().unwrap() as u8 - 1).saturating_sub(32);
let cy = (iter.next().unwrap().unwrap() as u8 - 1).saturating_sub(32);
Event::MouseEvent(match cb & 0b11 {
0 => {
if cb & 64 != 0 {
Mouse::Press(MouseButton::WheelUp)
} else {
Mouse::Press(MouseButton::Left)
}
}
1 => {
if cb & 64 != 0 {
Mouse::Press(MouseButton::WheelDown)
} else {
Mouse::Press(MouseButton::Middle)
}
}
2 => Mouse::Press(MouseButton::Right),
3 => Mouse::Release,
_ => return error,
},
cx as u16,
cy as u16)
}
Some(Ok(b'<')) => {
// xterm mouse encoding: ESC [ < Cb ; Cx ; Cy ; (M or m)
let mut buf = Vec::new();
let mut c = iter.next().unwrap().unwrap();
while match c {
b'm' | b'M' => false,
_ => true,
} {
buf.push(c);
c = iter.next().unwrap().unwrap();
}
let str_buf = String::from_utf8(buf).unwrap();
let ref mut nums = str_buf.split(';');
let cb = nums.next().unwrap().parse::<u16>().unwrap();
let cx = nums.next().unwrap().parse::<u16>().unwrap() - 1;
let cy = nums.next().unwrap().parse::<u16>().unwrap() - 1;
let button = match cb {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
64 => MouseButton::WheelUp,
65 => MouseButton::WheelDown,
_ => return error,
};
Event::MouseEvent(match c {
b'M' => Mouse::Press(button),
b'm' => Mouse::Release,
_ => return error,
},
cx,
cy)
}
Some(Ok(c @ b'0'...b'9')) => {
// numbered escape code
let mut buf = Vec::new();
buf.push(c);
let mut c = iter.next().unwrap().unwrap();
while match c {
b'M' | b'~' => false,
_ => true,
} {
buf.push(c);
c = iter.next().unwrap().unwrap();
}
match c {
// rxvt mouse encoding: ESC [ Cb ; Cx ; Cy ; M
b'M' => {
let str_buf = String::from_utf8(buf).unwrap();
let ref mut nums = str_buf.split(';');
let cb = nums.next().unwrap().parse::<u16>().unwrap();
let cx = nums.next().unwrap().parse::<u16>().unwrap() - 1;
let cy = nums.next().unwrap().parse::<u16>().unwrap() - 1;
let event = match cb {
32 => Mouse::Press(MouseButton::Left),
33 => Mouse::Press(MouseButton::Middle),
34 => Mouse::Press(MouseButton::Right),
35 => Mouse::Release,
96 => Mouse::Press(MouseButton::WheelUp),
97 => Mouse::Press(MouseButton::WheelUp),
_ => return error,
};
Event::MouseEvent(event, cx, cy)
},
// special key code
b'~' => {
let num: u8 = String::from_utf8(buf).unwrap().parse().unwrap();
match num {
1 | 7 => Event::KeyEvent(Key::Home),
2 => Event::KeyEvent(Key::Insert),
3 => Event::KeyEvent(Key::Delete),
4 | 8 => Event::KeyEvent(Key::End),
5 => Event::KeyEvent(Key::PageUp),
6 => Event::KeyEvent(Key::PageDown),
v @ 11...15 => Event::KeyEvent(Key::F(v - 10)),
v @ 17...21 => Event::KeyEvent(Key::F(v - 11)),
v @ 23...24 => Event::KeyEvent(Key::F(v - 12)),
_ => return error,
}
}
_ => return error,
}
}
_ => return error,
}
}
Some(Ok(c)) => {
let ch = parse_utf8_char(c, iter);
Event::KeyEvent(Key::Alt(try!(ch)))
}
Some(Err(_)) | None => return error,
})
}
Ok(b'\n') | Ok(b'\r') => Ok(Event::KeyEvent(Key::Char('\n'))),
Ok(b'\t') => Ok(Event::KeyEvent(Key::Char('\t'))),
Ok(b'\x7F') => Ok(Event::KeyEvent(Key::Backspace)),
Ok(c @ b'\x01'...b'\x1A') => Ok(Event::KeyEvent(Key::Ctrl((c as u8 - 0x1 + b'a') as char))),
Ok(c @ b'\x1C'...b'\x1F') => {
Ok(Event::KeyEvent(Key::Ctrl((c as u8 - 0x1C + b'4') as char)))
}
Ok(b'\0') => Ok(Event::KeyEvent(Key::Null)),
Ok(c) => {
Ok({
let ch = parse_utf8_char(c, iter);
Event::KeyEvent(Key::Char(try!(ch)))
})
}
Err(e) => Err(e),
}
}
fn parse_utf8_char<I>(c: u8, iter: &mut I) -> Result<char, Error>
where I: Iterator<Item = Result<u8, Error>>
{
let error = Err(Error::new(ErrorKind::Other, "Input character is not valid UTF-8"));
if c.is_ascii() {
Ok(c as char)
} else {
let ref mut bytes = Vec::new();
bytes.push(c);
loop {
bytes.push(iter.next().unwrap().unwrap());
match str::from_utf8(bytes) {
Ok(st) => return Ok(st.chars().next().unwrap()),
Err(_) => {},
}
if bytes.len() >= 4 { return error; }
}
}
}

View File

@ -1,126 +1,53 @@
use std::io::{self, Read, Write};
use event::{parse_event, Event, Key};
use IntoRawMode;
/// A key.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Key {
/// Backspace.
Backspace,
/// Left arrow.
Left,
/// Right arrow.
Right,
/// Up arrow.
Up,
/// Down arrow.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page Up key.
PageUp,
/// Page Down key.
PageDown,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// Function keys.
///
/// Only function keys 1 through 12 are supported.
F(u8),
/// Normal character.
Char(char),
/// Alt modified character.
Alt(char),
/// Ctrl modified character.
///
/// Note that certain keys may not be modifiable with `ctrl`, due to limitations of terminals.
Ctrl(char),
/// Invalid character code.
Invalid,
/// Null byte.
Null,
#[allow(missing_docs)]
#[doc(hidden)]
__IsNotComplete
}
/// An iterator over input keys.
#[cfg(feature = "nightly")]
pub struct Keys<I> {
chars: I,
iter: Events<I>,
}
#[cfg(feature = "nightly")]
impl<I: Iterator<Item = Result<char, io::CharsError>>> Iterator for Keys<I> {
type Item = Result<Key, io::CharsError>;
impl<I: Iterator<Item = Result<u8, io::Error>>> Iterator for Keys<I> {
type Item = Result<Key, io::Error>;
fn next(&mut self) -> Option<Result<Key, io::CharsError>> {
Some(match self.chars.next() {
Some(Ok('\x1B')) => Ok(match self.chars.next() {
Some(Ok('O')) => match self.chars.next() {
Some(Ok('P')) => Key::F(1),
Some(Ok('Q')) => Key::F(2),
Some(Ok('R')) => Key::F(3),
Some(Ok('S')) => Key::F(4),
_ => Key::Invalid,
},
Some(Ok('[')) => match self.chars.next() {
Some(Ok('D')) => Key::Left,
Some(Ok('C')) => Key::Right,
Some(Ok('A')) => Key::Up,
Some(Ok('B')) => Key::Down,
Some(Ok('H')) => Key::Home,
Some(Ok('F')) => Key::End,
Some(Ok(c @ '1' ... '8')) => match self.chars.next() {
Some(Ok('~')) => match c {
'1' | '7' => Key::Home,
'2'=> Key::Insert,
'3' => Key::Delete,
'4' | '8' => Key::End,
'5' => Key::PageUp,
'6' => Key::PageDown,
_ => Key::Invalid,
},
Some(Ok(k @ '0' ... '9')) => match self.chars.next() {
Some(Ok('~')) => match 10 * (c as u8 - b'0') + (k as u8 - b'0') {
v @ 11 ... 15 => Key::F(v - 10),
v @ 17 ... 21 => Key::F(v - 11),
v @ 23 ... 24 => Key::F(v - 12),
_ => Key::Invalid,
},
_ => Key::Invalid,
},
_ => Key::Invalid,
},
_ => Key::Invalid,
},
Some(Ok(c)) => Key::Alt(c),
Some(Err(_)) | None => Key::Invalid,
}),
Some(Ok('\n')) | Some(Ok('\r')) => Ok(Key::Char('\n')),
Some(Ok('\t')) => Ok(Key::Char('\t')),
Some(Ok('\x7F')) => Ok(Key::Backspace),
Some(Ok(c @ '\x01' ... '\x1A')) => Ok(Key::Ctrl((c as u8 - 0x1 + b'a') as char)),
Some(Ok(c @ '\x1C' ... '\x1F')) => Ok(Key::Ctrl((c as u8 - 0x1C + b'4') as char)),
Some(Ok('\0')) => Ok(Key::Null),
Some(Ok(c)) => Ok(Key::Char(c)),
Some(Err(e)) => Err(e),
None => return None,
})
fn next(&mut self) -> Option<Result<Key, io::Error>> {
loop {
match self.iter.next() {
Some(Ok(Event::KeyEvent(k))) => return Some(Ok(k)),
Some(Ok(_)) => continue,
e @ Some(Err(_)) => e,
None => return None,
};
}
}
}
/// An iterator over input events.
pub struct Events<I> {
bytes: I,
}
impl<I: Iterator<Item = Result<u8, io::Error>>> Iterator for Events<I> {
type Item = Result<Event, io::Error>;
fn next(&mut self) -> Option<Result<Event, io::Error>> {
let ref mut iter = self.bytes;
match iter.next() {
Some(item) => Some(parse_event(item, iter).or(Ok(Event::Unsupported))),
None => None,
}
}
}
/// Extension to `Read` trait.
pub trait TermRead {
/// An iterator over input events.
fn events(self) -> Events<io::Bytes<Self>> where Self: Sized;
/// An iterator over key inputs.
#[cfg(feature = "nightly")]
fn keys(self) -> Keys<io::Chars<Self>> where Self: Sized;
fn keys(self) -> Keys<io::Bytes<Self>> where Self: Sized;
/// Read a line.
///
@ -140,10 +67,14 @@ pub trait TermRead {
impl<R: Read> TermRead for R {
#[cfg(feature = "nightly")]
fn keys(self) -> Keys<io::Chars<R>> {
fn events(self) -> Events<io::Bytes<R>> {
Events {
bytes: self.bytes(),
}
}
fn keys(self) -> Keys<io::Bytes<R>> {
Keys {
chars: self.chars(),
iter: self.events(),
}
}
@ -170,7 +101,6 @@ mod test {
use super::*;
use std::io;
#[cfg(feature = "nightly")]
#[test]
fn test_keys() {
let mut i = b"\x1Bayo\x7F\x1B[D".keys();
@ -183,7 +113,6 @@ mod test {
assert!(i.next().is_none());
}
#[cfg(feature = "nightly")]
#[test]
fn test_function_keys() {
let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys();
@ -198,7 +127,6 @@ mod test {
}
}
#[cfg(feature = "nightly")]
#[test]
fn test_special_keys() {
let mut st = b"\x1B[2~\x1B[H\x1B[7~\x1B[5~\x1B[3~\x1B[F\x1B[8~\x1B[6~".keys();

View File

@ -11,9 +11,6 @@
//! For more information refer to the [README](https://github.com/ticki/termion).
#![warn(missing_docs)]
#![cfg_attr(feature = "nightly", feature(io))]
#[cfg(not(target_os = "redox"))]
extern crate libc;
@ -27,9 +24,10 @@ mod async;
pub use async::{AsyncReader, async_stdin};
mod input;
pub use input::{TermRead, Key};
#[cfg(feature = "nightly")]
pub use input::Keys;
pub use input::{TermRead, Events, Keys};
mod event;
pub use event::{Key, Mouse, MouseButton, Event};
mod raw;
pub use raw::{IntoRawMode, RawTerminal};

View File

@ -1,6 +1,9 @@
use std::io::{self, Write};
use std::ops::{Deref, DerefMut};
const ENTER_MOUSE_SEQUENCE: &'static[u8] = b"\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h";
const EXIT_MOUSE_SEQUENCE: &'static[u8] = b"\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l";
/// A terminal restorer, which keeps the previous state of the terminal, and restores it, when
/// dropped.
#[cfg(target_os = "redox")]
@ -12,6 +15,7 @@ pub struct RawTerminal<W: Write> {
impl<W: Write> Drop for RawTerminal<W> {
fn drop(&mut self) {
use control::TermWrite;
try!(self.write(EXIT_MOUSE_SEQUENCE));
self.csi(b"R").unwrap();
}
}
@ -30,6 +34,7 @@ pub struct RawTerminal<W: Write> {
impl<W: Write> Drop for RawTerminal<W> {
fn drop(&mut self) {
use termios::set_terminal_attr;
self.write(EXIT_MOUSE_SEQUENCE).unwrap();
set_terminal_attr(&mut self.prev_ios as *mut _);
}
}
@ -86,10 +91,12 @@ impl<W: Write> IntoRawMode for W {
if set_terminal_attr(&mut ios as *mut _) != 0 {
Err(io::Error::new(io::ErrorKind::Other, "Unable to set Termios attribute."))
} else {
Ok(RawTerminal {
let mut res = RawTerminal {
prev_ios: prev_ios,
output: self,
})
};
try!(res.write(ENTER_MOUSE_SEQUENCE));
Ok(res)
}
}
@ -97,8 +104,12 @@ impl<W: Write> IntoRawMode for W {
fn into_raw_mode(mut self) -> io::Result<RawTerminal<W>> {
use control::TermWrite;
self.csi(b"r").map(|_| RawTerminal {
self.csi(b"r").map(|_| {
let mut res = RawTerminal {
output: self,
};
try!(res.write(ENTER_MOUSE_SEQUENCE));
res
})
}
}