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/mouse.rs b/examples/mouse.rs new file mode 100644 index 0000000..9d8a598 --- /dev/null +++ b/examples/mouse.rs @@ -0,0 +1,41 @@ +extern crate termion; + +fn main() { + use termion::{TermRead, TermWrite, IntoRawMode, Key, Event, MouseEvent}; + use std::io::{Write, stdout, stdin}; + + let stdin = stdin(); + let mut stdout = stdout().into_raw_mode().unwrap().with_mouse().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(); + let evt = c.unwrap(); + match evt { + Event::Key(Key::Char('q')) => break, + Event::Mouse(me) => { + match me { + MouseEvent::Press(_, a, b) | + MouseEvent::Release(a, b) => { + x = a; + y = b; + } + } + } + _ => {} + } + println!("{:?}", evt); + 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..fde57f4 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,285 @@ +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. + Key(Key), + /// A mouse button press, release or wheel use at specific coordinates. + Mouse(MouseEvent), + /// An event that cannot currently be evaluated. + Unsupported, +} + +/// A mouse related event. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum MouseEvent { + /// A mouse button was pressed. + Press(MouseButton, u16, u16), + /// A mouse button was released. + Release(u16, u16), +} + +/// 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 +} + +/// Parse an Event from `item` and possibly subsequent bytes through `iter`. +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(val @ b'P' ... b'S')) => Event::Key(Key::F(1 + val - b'P')), + _ => return error, + } + } + Some(Ok(b'[')) => { + match iter.next() { + Some(Ok(b'D')) => Event::Key(Key::Left), + Some(Ok(b'C')) => Event::Key(Key::Right), + Some(Ok(b'A')) => Event::Key(Key::Up), + Some(Ok(b'B')) => Event::Key(Key::Down), + Some(Ok(b'H')) => Event::Key(Key::Home), + Some(Ok(b'F')) => Event::Key(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) as u16; + let cy = (iter.next().unwrap().unwrap() as u8 - 1).saturating_sub(32) as u16; + Event::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), + _ => return error, + }) + } + 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::Mouse(match c { + b'M' => MouseEvent::Press(button, cx, cy), + b'm' => MouseEvent::Release(cx, cy), + _ => return error, + + }) + } + 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 => 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), + 96 => MouseEvent::Press(MouseButton::WheelUp, cx, cy), + 97 => MouseEvent::Press(MouseButton::WheelUp, cx, cy), + _ => return error, + }; + + Event::Mouse(event) + }, + // Special key code. + b'~' => { + let num: u8 = String::from_utf8(buf).unwrap().parse().unwrap(); + match num { + 1 | 7 => Event::Key(Key::Home), + 2 => Event::Key(Key::Insert), + 3 => Event::Key(Key::Delete), + 4 | 8 => Event::Key(Key::End), + 5 => Event::Key(Key::PageUp), + 6 => Event::Key(Key::PageDown), + v @ 11...15 => Event::Key(Key::F(v - 10)), + v @ 17...21 => Event::Key(Key::F(v - 11)), + v @ 23...24 => Event::Key(Key::F(v - 12)), + _ => return error, + } + } + _ => return error, + } + } + _ => return error, + } + } + Some(Ok(c)) => { + let ch = parse_utf8_char(c, iter); + Event::Key(Key::Alt(try!(ch))) + } + Some(Err(_)) | None => return error, + }) + } + Ok(b'\n') | Ok(b'\r') => Ok(Event::Key(Key::Char('\n'))), + Ok(b'\t') => Ok(Event::Key(Key::Char('\t'))), + Ok(b'\x7F') => Ok(Event::Key(Key::Backspace)), + Ok(c @ b'\x01'...b'\x1A') => Ok(Event::Key(Key::Ctrl((c as u8 - 0x1 + b'a') as char))), + Ok(c @ b'\x1C'...b'\x1F') => { + Ok(Event::Key(Key::Ctrl((c as u8 - 0x1C + b'4') as char))) + } + Ok(b'\0') => Ok(Event::Key(Key::Null)), + Ok(c) => { + Ok({ + let ch = parse_utf8_char(c, iter); + Event::Key(Key::Char(try!(ch))) + }) + } + Err(e) => Err(e), + } +} + +/// Parse `c` as either a single byte ASCII char or a variable size UTF-8 char. +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; } + } + } +} + +#[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!(c == parse_utf8_char(b, bytes).unwrap()); + } +} diff --git a/src/input.rs b/src/input.rs index 824bb49..e2f2ea0 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::Key(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(), } } @@ -169,8 +100,8 @@ impl TermRead for R { mod test { use super::*; use std::io; + use event::{Key, Event, MouseEvent, MouseButton}; - #[cfg(feature = "nightly")] #[test] fn test_keys() { let mut i = b"\x1Bayo\x7F\x1B[D".keys(); @@ -183,7 +114,25 @@ mod test { assert!(i.next().is_none()); } - #[cfg(feature = "nightly")] + #[test] + fn test_events() { + let mut i = b"\x1B[\x00bc\x7F\x1B[D\ + \x1B[M\x00\x22\x24\x1B[<0;2;4;M\x1B[32;2;4M\x1B[<0;2;4;m\x1B[35;2;4Mb".events(); + + assert_eq!(i.next().unwrap().unwrap(), Event::Unsupported); + assert_eq!(i.next().unwrap().unwrap(), Event::Key(Key::Char('b'))); + assert_eq!(i.next().unwrap().unwrap(), Event::Key(Key::Char('c'))); + assert_eq!(i.next().unwrap().unwrap(), Event::Key(Key::Backspace)); + assert_eq!(i.next().unwrap().unwrap(), Event::Key(Key::Left)); + assert_eq!(i.next().unwrap().unwrap(), Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, 1, 3))); + assert_eq!(i.next().unwrap().unwrap(), Event::Mouse(MouseEvent::Press(MouseButton::Left, 1, 3))); + assert_eq!(i.next().unwrap().unwrap(), Event::Mouse(MouseEvent::Press(MouseButton::Left, 1, 3))); + assert_eq!(i.next().unwrap().unwrap(), Event::Mouse(MouseEvent::Release(1, 3))); + assert_eq!(i.next().unwrap().unwrap(), Event::Mouse(MouseEvent::Release(1, 3))); + assert_eq!(i.next().unwrap().unwrap(), Event::Key(Key::Char('b'))); + assert!(i.next().is_none()); + } + #[test] fn test_function_keys() { let mut st = b"\x1BOP\x1BOQ\x1BOR\x1BOS".keys(); @@ -198,7 +147,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..165cd15 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,12 +24,13 @@ 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, MouseEvent, MouseButton, Event}; mod raw; -pub use raw::{IntoRawMode, RawTerminal}; +pub use raw::{IntoRawMode, RawTerminal, MouseTerminal}; mod size; pub use size::terminal_size; diff --git a/src/raw.rs b/src/raw.rs index ca5215d..2bb9991 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -26,6 +26,17 @@ pub struct RawTerminal { output: W, } +#[cfg(not(target_os = "redox"))] +impl RawTerminal + where W: Write +{ + /// Enable mouse support. + pub fn with_mouse(mut self) -> io::Result> { + try!(self.write(ENTER_MOUSE_SEQUENCE)); + Ok(MouseTerminal { term: self }) + } +} + #[cfg(not(target_os = "redox"))] impl Drop for RawTerminal { fn drop(&mut self) { @@ -62,9 +73,9 @@ impl Write for RawTerminal { pub trait IntoRawMode: Write + Sized { /// Switch to raw mode. /// - /// Raw mode means that stdin won't be printed (it will instead have to be written manually by the - /// program). Furthermore, the input isn't canonicalised or buffered (that is, you can read from - /// stdin one byte of a time). The output is neither modified in any way. + /// Raw mode means that stdin won't be printed (it will instead have to be written manually by + /// the program). Furthermore, the input isn't canonicalised or buffered (that is, you can + /// read from stdin one byte of a time). The output is neither modified in any way. fn into_raw_mode(self) -> io::Result>; } @@ -86,10 +97,11 @@ 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 res = RawTerminal { prev_ios: prev_ios, output: self, - }) + }; + Ok(res) } } @@ -97,12 +109,63 @@ impl IntoRawMode for W { fn into_raw_mode(mut self) -> io::Result> { use control::TermWrite; - self.csi(b"r").map(|_| RawTerminal { - output: self, + self.csi(b"r").map(|_| { + let mut res = RawTerminal { output: self }; + res }) } } +/// A sequence of escape codes to enable terminal mouse support. +const ENTER_MOUSE_SEQUENCE: &'static [u8] = b"\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h"; + +/// A sequence of escape codes to disable terminal mouse support. +const EXIT_MOUSE_SEQUENCE: &'static [u8] = b"\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l"; + +/// A `RawTerminal` with added mouse support. +/// +/// To get such a terminal handle use `RawTerminal`'s +/// [`with_mouse()`](../termion/struct.RawTerminal.html#method.with_mouse) method. +#[cfg(not(target_os = "redox"))] +pub struct MouseTerminal { + term: RawTerminal, +} + +#[cfg(not(target_os = "redox"))] +impl Drop for MouseTerminal { + fn drop(&mut self) { + self.term.write(EXIT_MOUSE_SEQUENCE).unwrap(); + } +} + +#[cfg(not(target_os = "redox"))] +impl Deref for MouseTerminal { + type Target = W; + + fn deref(&self) -> &W { + self.term.deref() + } +} + +#[cfg(not(target_os = "redox"))] +impl DerefMut for MouseTerminal { + fn deref_mut(&mut self) -> &mut W { + self.term.deref_mut() + } +} + +#[cfg(not(target_os = "redox"))] +impl Write for MouseTerminal { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.term.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.term.flush() + } +} + + #[cfg(test)] mod test { use super::*; @@ -114,4 +177,10 @@ mod test { out.write(b"this is a test, muahhahahah").unwrap(); } + + #[test] + fn test_enable_mouse() { + let mut out = stdout().into_raw_mode().unwrap().with_mouse().unwrap(); + out.write(b"abcde\x1B[<1;1;0;Mfgh").unwrap(); + } }