diff --git a/src/ansi.rs b/src/ansi.rs index d0dbd3f..c3ff436 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -73,7 +73,11 @@ pub fn move_cursor_to_column(column: u16) -> String { /// Origin is 0, 0 #[must_use] pub fn move_cursor_to_position(column: u16, line: u16) -> String { - format!("\x1b[{};{}H", line.saturating_add(1), column.saturating_add(1)) + format!( + "\x1b[{};{}H", + line.saturating_add(1), + column.saturating_add(1) + ) } /// Sends input when terminal is in focus diff --git a/src/input.rs b/src/input.rs index 1e6cdb7..2d1c78c 100644 --- a/src/input.rs +++ b/src/input.rs @@ -2,106 +2,58 @@ //! //! Very incomplete currently +#[cfg(unix)] +#[path = "unix.rs"] +mod unix_input; -// pub(crate) fn parse_event( -// buffer: &[u8], -// input_available: bool, -// ) -> io::Result> { -// if buffer.is_empty() { -// return Ok(None); -// } +#[cfg(unix)] +pub use unix_input::*; -// match buffer[0] { -// b'\x1B' => { -// if buffer.len() == 1 { -// if input_available { -// // Possible Esc sequence -// Ok(None) -// } else { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) -// } -// } else { -// match buffer[1] { -// b'O' => { -// if buffer.len() == 2 { -// Ok(None) -// } else { -// match buffer[2] { -// b'D' => { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) -// } -// b'C' => Ok(Some(InternalEvent::Event(Event::Key( -// KeyCode::Right.into(), -// )))), -// b'A' => { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into())))) -// } -// b'B' => { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into())))) -// } -// b'H' => { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into())))) -// } -// b'F' => { -// Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into())))) -// } -// // F1-F4 -// val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( -// KeyCode::F(1 + val - b'P').into(), -// )))), -// _ => Err(could_not_parse_event_error()), -// } -// } -// } -// b'[' => parse_csi(buffer), -// b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), -// _ => parse_event(&buffer[1..], input_available).map(|event_option| { -// event_option.map(|event| { -// if let InternalEvent::Event(Event::Key(key_event)) = event { -// let mut alt_key_event = key_event; -// alt_key_event.modifiers |= KeyModifiers::ALT; -// InternalEvent::Event(Event::Key(alt_key_event)) -// } else { -// event -// } -// }) -// }), -// } -// } -// } -// b'\r' => Ok(Some(InternalEvent::Event(Event::Key( -// KeyCode::Enter.into(), -// )))), -// // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get -// // newlines as input is because the terminal converts \r into \n for us. When we -// // enter raw mode, we disable that, so \n no longer has any meaning - it's better to -// // use Ctrl+J. Waiting to handle it here means it gets picked up later -// b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event( -// Event::Key(KeyCode::Enter.into()), -// ))), -// b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))), -// b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key( -// KeyCode::Backspace.into(), -// )))), -// c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( -// KeyCode::Char((c - 0x1 + b'a') as char), -// KeyModifiers::CONTROL, -// ))))), -// c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( -// KeyCode::Char((c - 0x1C + b'4') as char), -// KeyModifiers::CONTROL, -// ))))), -// b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( -// KeyCode::Char(' '), -// KeyModifiers::CONTROL, -// ))))), -// _ => parse_utf8_char(buffer).map(|maybe_char| { -// maybe_char -// .map(KeyCode::Char) -// .map(char_code_to_event) -// .map(Event::Key) -// .map(InternalEvent::Event) -// }), -// } -// } +#[cfg(windows)] +#[path = "windows.rs"] +mod windows_input; +#[cfg(windows)] +pub use windows_input::*; + +pub enum Event { + Key(KeyEvent), + Mouse(MouseEvent) +} + +pub enum KeyEvent { + Backspace, + Up, + Down, + Left, + Right, + Home, + End, + PageUp, + PageDown, + Tab, + BackTab, + Delete, + Insert, + F(u8), + Char(char), + Ctrl(char), + Escape, + Null, +} + +pub enum MouseEvent { + Press(MouseButton, u16, u16), + Release(u16, u16), + Hold(u16, u16), +} + +pub enum MouseButton { + Left, + Right, + Middle, + WheelUp, + WheelDown, + WheelLeft, + WheelRight, +} diff --git a/src/input_sequences.md b/src/input_sequences.md deleted file mode 100644 index 963065f..0000000 --- a/src/input_sequences.md +++ /dev/null @@ -1,16 +0,0 @@ -* 0x1b = Escape -* 0x1b1b = Escape -* 0x1b41 = Up Arrow -* 0x1b42 = Down Arrow -* 0x1b43 = Right Arrow -* 0x1b44 = Left Arrow -* 0x1b46 = End -* 0x1b48 = Home -* 0x1b5b41 = Up Arrow -* 0x1b5b42 = Down Arrow -* 0x1b5b43 = Right Arrow -* 0x1b5b44 = Left Arrow -* 0x1b5b46 = End -* 0x1b5b48 = Home -* 0x20-7e = Char(n) - diff --git a/src/lib.rs b/src/lib.rs index 082d447..ba5c869 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![warn(clippy::all, clippy::pedantic)] pub mod ansi; -// pub mod input; +pub mod input; pub mod os; diff --git a/src/unix.rs b/src/unix.rs index 70a7d1b..231b6fe 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -48,7 +48,7 @@ struct Termios { /// This insures that you never exit with a terminal still in raw mode which is problematic for /// users pub struct RawTerminal { - orig_termios: Termios + orig_termios: Termios, } impl RawTerminal { @@ -58,7 +58,7 @@ impl RawTerminal { /// /// If there is no stdin, /// stdin is not a tty, - /// if it fails to change terminal settings + /// if it fails to change terminal settings pub fn new() -> io::Result { let mut orig_termios = Termios::default(); get_attributes(STDIN_FILENO, &mut orig_termios)?; diff --git a/src/unix_input.rs b/src/unix_input.rs new file mode 100644 index 0000000..0f61d56 --- /dev/null +++ b/src/unix_input.rs @@ -0,0 +1,218 @@ +use crate::input::{Event, KeyEvent, MouseEvent, MouseButton}; +use std::io; + +pub fn try_parse_event(item: u8, iter: &mut I) -> io::Result +where + I: Iterator>, +{ + match item { + b'\x1b' => { + try_parse_ansi_sequence(iter) + } + b'\n' | b'\r' => Ok(Event::Key(KeyEvent::Char('\n'))), + b'\t' => Ok(Event::Key(KeyEvent::Tab)), + b'\x7f' => Ok(Event::Key(KeyEvent::Backspace)), + b'\0' => Ok(Event::Key(KeyEvent::Null)), + c @ b'\x01'..=b'\x1a' => Ok(Event::Key(KeyEvent::Ctrl((c + 96) as char))), + c @ b'\x1c'..=b'\x1f' => Ok(Event::Key(KeyEvent::Ctrl((c + 24) as char))), + c => Ok(Event::Key(KeyEvent::Char(parse_utf8_char(c, iter)?))), + } +} + +fn parse_utf8_char(c: u8, iter: &mut I) -> io::Result +where + I: Iterator>, +{ + let error = || io::Error::new(io::ErrorKind::InvalidData, "Input char is not valid UTF-8"); + let mut bytes = vec![c]; + + for _ in 1..=4 { + if let Ok(string) = std::str::from_utf8(&bytes) { + return Ok(string.chars().next().unwrap()) + } + bytes.push(iter.next().ok_or_else(error)??); + } + Err(error()) +} + +fn try_parse_ansi_sequence(iter: &mut I) -> io::Result +where + I: Iterator>, +{ + let error = io::Error::new(io::ErrorKind::Other, "Could not parse event"); + match iter.next() { + Some(Ok(b'O')) => { + match iter.next() { + Some(Ok(val @ b'P'..=b's')) => Ok(Event::Key(KeyEvent::F(1 + val - b'P'))), + _ => Err(error) + } + } + Some(Ok(b'[')) => { + try_parse_csi_sequence(iter).ok_or(error) + } + _ => Err(error), + } +} + +fn try_parse_csi_sequence(iter: &mut I) -> Option +where + I: Iterator>, +{ + match iter.next() { + Some(Ok(b'[')) => { + match iter.next() { + Some(Ok(val @ b'A'..=b'E')) => Some(Event::Key(KeyEvent::F(1 + val - b'A'))), + _ => None + + } + } + Some(Ok(b'D')) => Some(Event::Key(KeyEvent::Left)), + Some(Ok(b'C')) => Some(Event::Key(KeyEvent::Right)), + Some(Ok(b'A')) => Some(Event::Key(KeyEvent::Up)), + Some(Ok(b'B')) => Some(Event::Key(KeyEvent::Down)), + Some(Ok(b'H')) => Some(Event::Key(KeyEvent::Home)), + Some(Ok(b'F')) => Some(Event::Key(KeyEvent::End)), + Some(Ok(b'Z')) => Some(Event::Key(KeyEvent::BackTab)), + Some(Ok(b'M')) => { + try_parse_x10_mouse(iter) + } + Some(Ok(b'<')) => { + try_parse_xterm_mouse(iter) + } + Some(Ok(c @ b'0'..=b'9')) => { + try_parse_rxvt_mouse(c, iter) + } + _ => None + + } +} + +fn try_parse_x10_mouse(iter: &mut I) -> Option +where + I: Iterator>, +{ + let cb = iter.next()?.ok()? - 32; + + let cx = u16::from(iter.next()?.ok()?.saturating_sub(33)); + let cy = u16::from(iter.next()?.ok()?.saturating_sub(33)); + match cb & 0x11 { + 0 => { + if cb & 0x40 != 0 { + Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelUp, cx, cy))) + } else { + Some(Event::Mouse(MouseEvent::Press(MouseButton::Left, cx, cy))) + } + } + 1 => { + if cb & 0x40 != 0 { + Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, cx, cy))) + + } else { + Some(Event::Mouse(MouseEvent::Press(MouseButton::Middle, cx, cy))) + } + } + 2 => { + if cb & 0x40 != 0 { + Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelLeft, cx, cy))) + } else { + Some(Event::Mouse(MouseEvent::Press(MouseButton::Right, cx, cy))) + } + } + 3 => { + if cb & 0x40 != 0 { + Some(Event::Mouse(MouseEvent::Press(MouseButton::WheelRight, cx, cy))) + } else { + Some(Event::Mouse(MouseEvent::Release(cx, cy))) + } + } + _ => None, + } +} + +fn try_parse_xterm_mouse(iter: &mut I) -> Option +where + I: Iterator>, +{ + let mut buf = Vec::new(); + let mut character = iter.next()?.ok()?; + while !matches!(character, b'm' | b'M' ) { + buf.push(character); + character = iter.next()?.ok()?; + } + let str_buf = String::from_utf8(buf).ok()?; + let nums = &mut str_buf.split(';'); + + let cb = nums.next()?.parse::().ok()?; + + let cx = nums.next()?.parse::().ok()?; + let cy = nums.next()?.parse::().ok()?; + + let event = match cb { + 0..=2 | 64..=67 => { + let button = match cb { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + 64 => MouseButton::WheelUp, + 65 => MouseButton::WheelDown, + 66 => MouseButton::WheelLeft, + 67 => MouseButton::WheelRight, + _ => unreachable!(), + }; + match character { + b'M' => MouseEvent::Press(button, cx, cy), + b'm' => MouseEvent::Release(cx, cy), + _ => return None, + } + }, + 32 | 3 => MouseEvent::Hold(cx, cy), + _ => return None, + }; + Some(Event::Mouse(event)) +} + +fn try_parse_rxvt_mouse(c: u8, iter: &mut I) -> Option +where + I: Iterator>, +{ + let mut buf = vec![c]; + let mut c = iter.next()?.ok()?; + while !(64..=126).contains(&c) { + buf.push(c); + c = iter.next()?.ok()?; + } + if c == b'M' { + let str_buf = String::from_utf8(buf).ok()?; + + let nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); + + let cb = nums[0]; + let cx = nums[1]; + let cy = nums[2]; + + 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), + 64 => MouseEvent::Hold(cx, cy), + 96 | 97 => MouseEvent::Press(MouseButton::WheelUp, cx, cy), + _ => return None, + }; + + return Some(Event::Mouse(event)); + } + None +} + +#[test] +fn test_parse_utf8() { + let string = "abcéŷ¤£€ù%323"; + let ref mut bytes = string.bytes().map(|x| Ok(x)); + let chars = string.chars(); + for c in chars { + let b = bytes.next().unwrap().unwrap(); + let character = parse_utf8_char(b, bytes).unwrap(); + assert!(c == character); + } +} diff --git a/src/windows.rs b/src/windows.rs index cf29b57..7eb9cb5 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -33,7 +33,7 @@ struct ConsoleScreenBufferInfo { /// /// This insures that you never exit with a terminal still in raw mode which is problematic for /// users -pub struct RawTerminal; +pub struct RawTerminal; impl RawTerminal { /// This constructs a terminal, automatically making it raw @@ -42,7 +42,7 @@ impl RawTerminal { /// /// If there is no stdin, /// stdin is not a tty, - /// if it fails to change terminal settings + /// if it fails to change terminal settings pub fn new() -> io::Result { let handle = get_std_handle(STD_INPUT_HANDLE)?; let mut mode = 0; diff --git a/src/windows_input.rs b/src/windows_input.rs new file mode 100644 index 0000000..e69de29