From cc9c32b981363771c7a6e011aa103255277d157b Mon Sep 17 00:00:00 2001 From: IGI-111 Date: Tue, 19 Jul 2016 02:31:34 +0200 Subject: [PATCH] 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. --- Cargo.toml | 4 - README.md | 15 +-- examples/keys.rs | 7 -- examples/test.rs | 35 ++++++ src/event.rs | 277 +++++++++++++++++++++++++++++++++++++++++++++++ src/input.rs | 154 +++++++------------------- src/lib.rs | 10 +- src/raw.rs | 17 ++- 8 files changed, 372 insertions(+), 147 deletions(-) create mode 100644 examples/test.rs create mode 100644 src/event.rs diff --git a/Cargo.toml b/Cargo.toml index 4f0f119..aa8505a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,3 @@ authors = ["Ticki "] [target.'cfg(not(target_os = "redox"))'.dependencies] libc = "0.2.8" - -[features] -default = ["nightly"] -nightly = [] diff --git a/README.md b/README.md index 69560de..cd8896d 100644 --- a/README.md +++ b/README.md @@ -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 -## TODO - -- Mouse input - ## License MIT/X11. diff --git a/examples/keys.rs b/examples/keys.rs index 55d2f83..4427d7f 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -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`.") -} diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 0000000..48ba1f5 --- /dev/null +++ b/examples/test.rs @@ -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(); +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..cd98b3c --- /dev/null +++ b/src/event.rs @@ -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(item: Result, iter: &mut I) -> Result +where I: Iterator> +{ + 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::().unwrap(); + let cx = nums.next().unwrap().parse::().unwrap() - 1; + let cy = nums.next().unwrap().parse::().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::().unwrap(); + let cx = nums.next().unwrap().parse::().unwrap() - 1; + let cy = nums.next().unwrap().parse::().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(c: u8, iter: &mut I) -> Result +where I: Iterator> +{ + 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; } + } + } +} diff --git a/src/input.rs b/src/input.rs index 824bb49..32c51ed 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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 { - chars: I, + iter: Events, } -#[cfg(feature = "nightly")] -impl>> Iterator for Keys { - type Item = Result; +impl>> Iterator for Keys { + type Item = Result; - fn next(&mut self) -> Option> { - 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> { + 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 { + bytes: I, +} + +impl>> Iterator for Events { + type Item = Result; + + fn next(&mut self) -> Option> { + 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> where Self: Sized; + /// An iterator over key inputs. - #[cfg(feature = "nightly")] - fn keys(self) -> Keys> where Self: Sized; + fn keys(self) -> Keys> where Self: Sized; /// Read a line. /// @@ -140,10 +67,14 @@ pub trait TermRead { impl TermRead for R { - #[cfg(feature = "nightly")] - fn keys(self) -> Keys> { + fn events(self) -> Events> { + Events { + bytes: self.bytes(), + } + } + fn keys(self) -> Keys> { 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(); diff --git a/src/lib.rs b/src/lib.rs index 8a78981..c1d84b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/raw.rs b/src/raw.rs index ca5215d..c861006 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -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 { impl Drop for RawTerminal { 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 { impl Drop for RawTerminal { 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 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 IntoRawMode for W { fn into_raw_mode(mut self) -> io::Result> { 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 }) } }