diff --git a/Cargo.toml b/Cargo.toml index 4f0f1190984dcf7c3b163e56bf0b9fe136b1ce93..aa8505ad1dd7f9efbfa381dcc8eae80bcd9d7d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [] diff --git a/README.md b/README.md index 69560de7d307fb8826f9eae300d0e27af646dae4..cd8896d181a3082fbe060dd97a81253c54d4e02f 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 <img src="image.png" width="200"> -## TODO - -- Mouse input - ## License MIT/X11. diff --git a/examples/keys.rs b/examples/keys.rs index 55d2f831d6680a8c6c042c773588240ae7629768..4427d7f681824a13b63c4b8d7c5e5fc0f1650dfc 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 0000000000000000000000000000000000000000..48ba1f5ccdd594991ed6345f1850617da157f1da --- /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 0000000000000000000000000000000000000000..cd98b3cdcc685d199028c28ed7a2969d49bc292c --- /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<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; } + } + } +} diff --git a/src/input.rs b/src/input.rs index 824bb49d69570ff8003dd229c34528c314d4da07..32c51ed207be336e40af238851395ba137f660c3 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, +/// An iterator over input keys. +pub struct Keys<I> { + iter: Events<I>, +} - #[allow(missing_docs)] - #[doc(hidden)] - __IsNotComplete +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::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 keys. -#[cfg(feature = "nightly")] -pub struct Keys<I> { - chars: I, +/// An iterator over input events. +pub struct Events<I> { + bytes: I, } -#[cfg(feature = "nightly")] -impl<I: Iterator<Item = Result<char, io::CharsError>>> Iterator for Keys<I> { - type Item = Result<Key, io::CharsError>; - - 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, - }) +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(); diff --git a/src/lib.rs b/src/lib.rs index 8a789818131fa6fbb0ecc49afa9eb496ce905bbc..c1d84b3309ac156c09af1a2326f5e797624b9607 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 ca5215dc6dbd45c77d0a54805f58414bdc19f081..c861006809857aa5495a8dd3746153b00c449cac 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<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 }) } }