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] [target.'cfg(not(target_os = "redox"))'.dependencies]
libc = "0.2.8" libc = "0.2.8"
[features]
default = ["nightly"]
nightly = []

View File

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

View File

@ -1,6 +1,5 @@
extern crate termion; extern crate termion;
#[cfg(feature = "nightly")]
fn main() { fn main() {
use termion::{TermRead, TermWrite, IntoRawMode, Key}; use termion::{TermRead, TermWrite, IntoRawMode, Key};
use std::io::{Write, stdout, stdin}; use std::io::{Write, stdout, stdin};
@ -27,7 +26,6 @@ fn main() {
Key::Up => println!(""), Key::Up => println!(""),
Key::Down => println!(""), Key::Down => println!(""),
Key::Backspace => println!("×"), Key::Backspace => println!("×"),
Key::Invalid => println!("???"),
_ => {}, _ => {},
} }
stdout.flush().unwrap(); stdout.flush().unwrap();
@ -35,8 +33,3 @@ fn main() {
stdout.show_cursor().unwrap(); 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 std::io::{self, Read, Write};
use event::{parse_event, Event, Key};
use IntoRawMode; 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. /// An iterator over input keys.
#[cfg(feature = "nightly")]
pub struct Keys<I> { pub struct Keys<I> {
chars: I, iter: Events<I>,
} }
#[cfg(feature = "nightly")] impl<I: Iterator<Item = Result<u8, io::Error>>> Iterator for Keys<I> {
impl<I: Iterator<Item = Result<char, io::CharsError>>> Iterator for Keys<I> { type Item = Result<Key, io::Error>;
type Item = Result<Key, io::CharsError>;
fn next(&mut self) -> Option<Result<Key, io::CharsError>> { fn next(&mut self) -> Option<Result<Key, io::Error>> {
Some(match self.chars.next() { loop {
Some(Ok('\x1B')) => Ok(match self.chars.next() { match self.iter.next() {
Some(Ok('O')) => match self.chars.next() { Some(Ok(Event::KeyEvent(k))) => return Some(Ok(k)),
Some(Ok('P')) => Key::F(1), Some(Ok(_)) => continue,
Some(Ok('Q')) => Key::F(2), e @ Some(Err(_)) => e,
Some(Ok('R')) => Key::F(3), None => return None,
Some(Ok('S')) => Key::F(4), };
_ => Key::Invalid, }
}, }
Some(Ok('[')) => match self.chars.next() { }
Some(Ok('D')) => Key::Left,
Some(Ok('C')) => Key::Right, /// An iterator over input events.
Some(Ok('A')) => Key::Up, pub struct Events<I> {
Some(Ok('B')) => Key::Down, bytes: I,
Some(Ok('H')) => Key::Home, }
Some(Ok('F')) => Key::End,
Some(Ok(c @ '1' ... '8')) => match self.chars.next() { impl<I: Iterator<Item = Result<u8, io::Error>>> Iterator for Events<I> {
Some(Ok('~')) => match c { type Item = Result<Event, io::Error>;
'1' | '7' => Key::Home,
'2'=> Key::Insert, fn next(&mut self) -> Option<Result<Event, io::Error>> {
'3' => Key::Delete, let ref mut iter = self.bytes;
'4' | '8' => Key::End, match iter.next() {
'5' => Key::PageUp, Some(item) => Some(parse_event(item, iter).or(Ok(Event::Unsupported))),
'6' => Key::PageDown, None => None,
_ => 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,
})
} }
} }
/// Extension to `Read` trait. /// Extension to `Read` trait.
pub trait TermRead { pub trait TermRead {
/// An iterator over input events.
fn events(self) -> Events<io::Bytes<Self>> where Self: Sized;
/// An iterator over key inputs. /// An iterator over key inputs.
#[cfg(feature = "nightly")] fn keys(self) -> Keys<io::Bytes<Self>> where Self: Sized;
fn keys(self) -> Keys<io::Chars<Self>> where Self: Sized;
/// Read a line. /// Read a line.
/// ///
@ -140,10 +67,14 @@ pub trait TermRead {
impl<R: Read> TermRead for R { impl<R: Read> TermRead for R {
#[cfg(feature = "nightly")] fn events(self) -> Events<io::Bytes<R>> {
fn keys(self) -> Keys<io::Chars<R>> { Events {
bytes: self.bytes(),
}
}
fn keys(self) -> Keys<io::Bytes<R>> {
Keys { Keys {
chars: self.chars(), iter: self.events(),
} }
} }
@ -170,7 +101,6 @@ mod test {
use super::*; use super::*;
use std::io; use std::io;
#[cfg(feature = "nightly")]
#[test] #[test]
fn test_keys() { fn test_keys() {
let mut i = b"\x1Bayo\x7F\x1B[D".keys(); let mut i = b"\x1Bayo\x7F\x1B[D".keys();
@ -183,7 +113,6 @@ mod test {
assert!(i.next().is_none()); assert!(i.next().is_none());
} }
#[cfg(feature = "nightly")]
#[test] #[test]
fn test_function_keys() { fn test_function_keys() {
let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys(); let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys();
@ -198,7 +127,6 @@ mod test {
} }
} }
#[cfg(feature = "nightly")]
#[test] #[test]
fn test_special_keys() { 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(); 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). //! For more information refer to the [README](https://github.com/ticki/termion).
#![warn(missing_docs)] #![warn(missing_docs)]
#![cfg_attr(feature = "nightly", feature(io))]
#[cfg(not(target_os = "redox"))] #[cfg(not(target_os = "redox"))]
extern crate libc; extern crate libc;
@ -27,9 +24,10 @@ mod async;
pub use async::{AsyncReader, async_stdin}; pub use async::{AsyncReader, async_stdin};
mod input; mod input;
pub use input::{TermRead, Key}; pub use input::{TermRead, Events, Keys};
#[cfg(feature = "nightly")]
pub use input::Keys; mod event;
pub use event::{Key, Mouse, MouseButton, Event};
mod raw; mod raw;
pub use raw::{IntoRawMode, RawTerminal}; pub use raw::{IntoRawMode, RawTerminal};

View File

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