diff --git a/README.md b/README.md index 7112174..995c6bb 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,3 @@ it is most likely not without good reason - Other Kitty protocols (there are a lot of them) (Not planned) \* Do not have full support for advanced input - -## Structure -This library has multiple APIs - -Inside the `cli` module there are utilities for making something similar to Cargo or Git - -And inside the `tui` module there are utilities for making something similar to Neovim or Emacs - -And inside the rest of the library there are lower-level APIs in case you want to abstract over -this library yourself or if you need more precise control over the terminal diff --git a/examples/input.rs b/examples/input.rs index ab43e3f..255f577 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -11,10 +11,10 @@ fn print_line_style_reset(string: &str) { fn main() -> io::Result<()> { assert!(io::stdout().is_terminal(), "Not running in a terminal"); - let all_styles = format!("{STYLE_BOLD}{STYLE_ITALIC}{STYLE_UNDERLINE}"); - enable_ansi()?; enable_raw_mode()?; + enable_mouse_input()?; + // enable_kitty_keyboard(); println!("q to quit{}", move_cursor_to_column(0)); let next = |x: usize| (x + 1) % COLORS_FG.len(); @@ -26,23 +26,30 @@ fn main() -> io::Result<()> { let mut counter = 0; loop { - let mut input = Err(io::ErrorKind::Other.into()); - while input.is_err() { - input = poll_input(Duration::new(1, 0)); - } - let input = input.unwrap(); + let input = poll_input(Duration::new(1, 0)); let string = format!("{input:?}"); - print_line_style_reset(&format!( - "{all_styles}{}{}{string}", - COLORS_FG[counter], - COLORS_BG[next(counter)] - )); + match &input { + Err(e) => match e.kind() { + io::ErrorKind::TimedOut => {} + _ => { + print_line_style_reset(&string); + } + }, + Ok(_) => { + print_line_style_reset(&string); + } + } // q to quit - if input == Event::Key(Key::Char('q'), KeyType::Press, KeyMods::NONE) { + if input.is_ok() + && input.unwrap() == Event::Key(Key::Char('q'), ButtonType::Press, Modifiers::NONE) + { break; } counter = next(counter); } + + // disable_kitty_keyboard(); disable_raw_mode()?; + disable_mouse_input()?; Ok(()) } diff --git a/examples/raw_input.rs b/examples/raw_input.rs new file mode 100644 index 0000000..f1cd6bf --- /dev/null +++ b/examples/raw_input.rs @@ -0,0 +1,27 @@ +use neutuino::prelude::*; +use std::io::{self, IsTerminal, Read, Write}; + +fn main() -> io::Result<()> { + assert!(io::stdout().is_terminal(), "Not running in a terminal"); + enable_ansi()?; + enable_raw_mode()?; + // enable_kitty_keyboard(); + io::stdout().flush()?; + print!("\x1b[?1003h"); + + let mut buf = [0; 1]; + let mut stdin = io::stdin(); + while buf != [b'q'] { + _ = stdin.read(&mut buf); + if buf[0] > 127 || !(buf[0] as char).is_control() { + println!("{:x} \"{}\"\r", buf[0], buf[0] as char); + } else { + println!("{:x} ?\r", buf[0]); + } + } + + // disable_kitty_keyboard(); + disable_raw_mode()?; + print!("\x1b[?1003l"); + Ok(()) +} diff --git a/examples/simple.rs b/examples/simple.rs index 6b38b17..cbb1c7d 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -12,8 +12,8 @@ fn main() -> io::Result<()> { enable_ansi()?; // makes the terminal raw until this value is dropped - let _raw_terminal = RawModeHandler::new()?; - let _alt_screen = AltScreenHandler::new()?; + enable_raw_mode()?; + println!("{ALT_SCREEN_ENTER}"); // gets the size of the terminal let terminal_size = get_terminal_size()?; @@ -31,6 +31,9 @@ fn main() -> io::Result<()> { thread::sleep(time::Duration::new(3, 0)); + disable_raw_mode()?; + println!("{ALT_SCREEN_EXIT}"); + // no flush needed here as the program is about to end and it will be auto flushed Ok(()) } diff --git a/src/ansi.rs b/src/ansi.rs index 9c3cf0f..4917198 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -84,11 +84,6 @@ pub fn move_cursor_to_position(column: u16, line: u16) -> String { ) } -// /// Enables mouse input -// pub const ENABLE_MOUSE: &str = "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h"; -// /// Disables mouse input -// pub const DISABLE_MOUSE: &str = "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l""; - /// Saves the current cursor position pub const CURSOR_POSITION_SAVE: &str = "\x1b7"; /// Restores the saved cursor position diff --git a/src/control.rs b/src/control.rs index 4a45d97..4af902b 100644 --- a/src/control.rs +++ b/src/control.rs @@ -3,7 +3,13 @@ //! These are built to work on Windows, Linux, and MacOS #[cfg(unix)] -pub use crate::unix::{disable_raw_mode, enable_raw_mode, get_terminal_size}; +pub use crate::unix::{ + disable_kitty_keyboard, disable_mouse_input, disable_raw_mode, enable_kitty_keyboard, + enable_mouse_input, enable_raw_mode, get_terminal_size, +}; #[cfg(windows)] -pub use crate::windows::{disable_raw_mode, enable_raw_mode, get_terminal_size}; +pub use crate::windows::{ + disable_kitty_keyboard, disable_mouse_input, disable_raw_mode, enable_kitty_keyboard, + enable_mouse_input, enable_raw_mode, get_terminal_size, +}; diff --git a/src/input.rs b/src/input.rs index 8df10b7..dfc0807 100644 --- a/src/input.rs +++ b/src/input.rs @@ -12,13 +12,40 @@ #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Event { /// An event that happens upon a key being pressed - Key(Key, KeyType, KeyMods), + Key(Key, ButtonType, Modifiers), + /// An event that happens upon a mouse action + /// + /// The last two are the x and y position of the event, 0-based + Mouse(Modifiers, MouseButton, ButtonType, u16, u16), /// An event that happens upon focus to the terminal window being gained FocusGained, /// An event that happens upon focus to the terminal window being lost FocusLost, } +/// The key on the mouse that was pressed +#[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, + /// The mouse wheel going up + WheelUp, + /// The mouse wheel going down + WheelDown, + /// The mouse wheel going left + WheelLeft, + /// The mouse wheel going right + WheelRight, + /// The protocol does not specify, typically only on release/held buttons + Unknown, + /// No mouse button was pressed + None, +} + /// The base key that was pressed #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Key { @@ -56,24 +83,25 @@ pub enum Key { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] #[allow(clippy::struct_excessive_bools)] -pub struct KeyMods { +pub struct Modifiers { pub shift: bool, pub alt: bool, pub ctrl: bool, - pub meta: bool, } -impl KeyMods { +impl Modifiers { + pub const fn new(shift: bool, alt: bool, ctrl: bool) -> Self { + Self { shift, alt, ctrl } + } + pub const NONE: Self = Self { shift: false, alt: false, ctrl: false, - meta: false, }; pub const SHIFT: Self = Self::NONE.shift(true); pub const ALT: Self = Self::NONE.alt(true); pub const CTRL: Self = Self::NONE.ctrl(true); - pub const META: Self = Self::NONE.meta(true); #[must_use] pub const fn shift(mut self, on: bool) -> Self { self.shift = on; @@ -89,25 +117,45 @@ impl KeyMods { self.ctrl = on; self } - #[must_use] - pub const fn meta(mut self, on: bool) -> Self { - self.meta = on; - self - } } /// This is the type of the key that is sent to the terminal /// -/// This is implemented in Windows, and Kitty-like Terminals +/// This is implemented on keys on Windows and Kitty-like Terminals +/// +/// This is implemented on most mouse implementations #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum KeyType { +pub enum ButtonType { Press, - Repeat, + Held, Release, } -pub fn press_key(key: Key, key_mods: KeyMods) -> Event { - Event::Key(key, KeyType::Press, key_mods) +#[inline(always)] +pub(crate) const fn key_helper(mods: &str, key: Key) -> Event { + let mut key_mods = Modifiers::NONE; + let mut key_type = ButtonType::Press; + + let string = mods.as_bytes(); + let mut i = 0; + while i < string.len() { + key_mods.alt = key_mods.alt | (string[i] == b'A'); + key_mods.ctrl = key_mods.ctrl | (string[i] == b'C'); + key_mods.shift = key_mods.shift | (string[i] == b'S'); + if string[i] == b'-' { + key_type = ButtonType::Release; + } + if string[i] == b'*' { + key_type = ButtonType::Held; + } + i += 1; + } + Event::Key(key, key_type, key_mods) +} + +#[cfg(unix)] +pub(crate) const fn simple_key(key: Key, shift: bool, alt: bool, ctrl: bool) -> Event { + Event::Key(key, ButtonType::Press, Modifiers::new(shift, alt, ctrl)) } #[cfg(unix)] @@ -115,3 +163,16 @@ pub use crate::unix::poll_input; #[cfg(windows)] pub use crate::windows::poll_input; + +#[test] +fn test_key_helper() { + let event = key_helper("ACS*", Key::Char('c')); + assert_eq!( + event, + Event::Key( + Key::Char('c'), + ButtonType::Held, + Modifiers::SHIFT.ctrl(true).alt(true) + ) + ) +} diff --git a/src/unix.rs b/src/unix.rs index 35892ad..06ae7be 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -2,10 +2,16 @@ use std::ffi::{c_int, c_uint, c_ulong, c_ushort}; use std::io; use std::sync::LazyLock; -use crate::input::{Event, Key, KeyMods, KeyType, press_key}; +use crate::input::{ButtonType, Event, Key, Modifiers, MouseButton, key_helper, simple_key}; use std::ffi::{c_short, c_void}; use std::time::Duration; +const ENABLE_MOUSE: &str = "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h\x1b[?1003h"; +const DISABLE_MOUSE: &str = "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l\x1b[?1003l"; + +const ENABLE_KITTY_KEYBOARD: &str = "\x1b[>31u"; +const DISABLE_KITTY_KEYBOARD: &str = "\x1b[<31u"; + unsafe extern "C" { fn ioctl(fd: c_int, request: c_ulong, argp: *mut u8) -> c_int; fn cfmakeraw(termios: *mut Termios); @@ -27,8 +33,6 @@ const TIOCGWINSZ: c_ulong = 0x4008_7468; #[cfg(target_os = "macos")] const NCCS: usize = 0x14; -const ERROR_MAGIC: i32 = 68905; - #[repr(C)] #[derive(Default, Debug, Clone, Copy)] struct Winsize { @@ -71,10 +75,41 @@ static TERMIOS: LazyLock> = LazyLock::new(|| { let attributes = get_attributes(STDIN_FILENO, &mut orig_termios); match attributes { Ok(()) => Ok(orig_termios), - Err(e) => Err(e.raw_os_error().unwrap_or(ERROR_MAGIC)), + Err(e) => Err(e.raw_os_error().unwrap()), } }); +/// CSI>31u +/// Enable kitty comprehensive keyboard handling protocol +pub fn enable_kitty_keyboard() { + print!("{ENABLE_KITTY_KEYBOARD}"); +} + +/// Disable kitty comprehensive keyboard handling protocol +pub fn disable_kitty_keyboard() { + print!("{DISABLE_KITTY_KEYBOARD}"); +} + +/// Enable mouse input, if available +/// +/// # Errors +/// +/// Never currently +pub fn enable_mouse_input() -> io::Result<()> { + print!("{ENABLE_MOUSE}"); + Ok(()) +} + +/// Disable mouse input, if available +/// +/// # Errors +/// +/// Never currently +pub fn disable_mouse_input() -> io::Result<()> { + print!("{DISABLE_MOUSE}"); + Ok(()) +} + /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization /// /// # Errors @@ -83,12 +118,7 @@ static TERMIOS: LazyLock> = LazyLock::new(|| { /// stdin is not a tty, /// or it fails to change terminal settings pub fn enable_raw_mode() -> io::Result<()> { - let mut termios = (*TERMIOS).map_err(|e| { - if e == ERROR_MAGIC { - return io::Error::other("Failed to get terminal properties"); - } - io::Error::from_raw_os_error(e) - })?; + let mut termios = (*TERMIOS).map_err(|e| io::Error::from_raw_os_error(e))?; unsafe { cfmakeraw(&mut termios); } @@ -104,12 +134,7 @@ pub fn enable_raw_mode() -> io::Result<()> { /// stdin is not a tty, /// or it fails to change terminal settings pub fn disable_raw_mode() -> io::Result<()> { - let mut termios = (*TERMIOS).map_err(|e| { - if e == ERROR_MAGIC { - return io::Error::other("Failed to get terminal properties"); - } - io::Error::from_raw_os_error(e) - })?; + let mut termios = (*TERMIOS).map_err(|e| io::Error::from_raw_os_error(e))?; set_attributes(STDIN_FILENO, &mut termios)?; Ok(()) } @@ -160,20 +185,8 @@ pub fn get_terminal_size() -> io::Result<(u16, u16)> { /// If the timeout has expired or /// there was an error getting the data pub fn poll_input(timeout: Duration) -> io::Result { - let mut fds = [PollFD { - fd: STDIN_FILENO, - events: POLLIN, - revents: 0, - }]; - let result = unsafe { - #[allow(clippy::cast_possible_truncation)] - poll( - fds.as_mut_ptr(), - fds.len() as c_ulong, - timeout.as_millis() as c_int, - ) - }; - let mut read_iter = ReadIterator::new(STDIN_FILENO); + let result = poll_timeout(timeout); + let mut read_iter = ReadIterator::new(); let timed_out: io::Error = io::ErrorKind::TimedOut.into(); @@ -187,6 +200,22 @@ pub fn poll_input(timeout: Duration) -> io::Result { } } +fn poll_timeout(timeout: Duration) -> i32 { + let mut fds = [PollFD { + fd: STDIN_FILENO, + events: POLLIN, + revents: 0, + }]; + unsafe { + #[allow(clippy::cast_possible_truncation)] + poll( + fds.as_mut_ptr(), + fds.len() as c_ulong, + timeout.as_millis() as c_int, + ) + } +} + unsafe extern "C" { fn poll(fds: *mut PollFD, nfds: c_ulong, timeout: c_int) -> c_int; fn read(fd: c_int, buf: *mut c_void, count: c_ulong) -> c_short; @@ -201,13 +230,12 @@ struct PollFD { } struct ReadIterator { - fd: c_int, buf: u8, } impl ReadIterator { - fn new(fd: c_int) -> Self { - Self { fd, buf: 0 } + fn new() -> Self { + Self { buf: 0 } } } @@ -215,12 +243,18 @@ impl Iterator for ReadIterator { type Item = io::Result; fn next(&mut self) -> Option { - let bytes_read = unsafe { read(self.fd, (&raw mut self.buf).cast::(), 1) }; - - match bytes_read { - 1.. => Some(Ok(self.buf)), + let bytes_poll = poll_timeout(Duration::ZERO); + let bytes_read = match bytes_poll { + 1.. => Some(Ok(unsafe { + read(STDIN_FILENO, (&raw mut self.buf).cast::(), 1) + })), 0 => None, _ => Some(Err(io::Error::last_os_error())), + }; + match bytes_read? { + Ok(1..) => Some(Ok(self.buf)), + Ok(0) => None, + _ => Some(Err(io::Error::last_os_error())), } } } @@ -230,20 +264,20 @@ where I: Iterator>, { match item { - b'\x1b' => try_parse_ansi_sequence(iter), - b'\r' => Ok(press_key(Key::Char('\r'), KeyMods::NONE)), - b'\n' => Ok(press_key(Key::Char('j'), KeyMods::CTRL)), - b'\t' => Ok(press_key(Key::Char('\t'), KeyMods::NONE)), - b'\x7f' => Ok(press_key(Key::Backspace, KeyMods::NONE)), - b'\0' => Ok(press_key(Key::Char(' '), KeyMods::CTRL)), - c @ b'\x01'..=b'\x1a' => Ok(press_key(Key::Char((c + 96) as char), KeyMods::CTRL)), - c @ b'\x1c'..=b'\x1f' => Ok(press_key(Key::Char((c + 24) as char), KeyMods::CTRL)), + b'\x1b' => parse_ansi_sequence(iter), + b'\r' => Ok(key_helper("", Key::Char('\r'))), + b'\n' => Ok(key_helper("C", Key::Char('j'))), + b'\t' => Ok(key_helper("", Key::Char('\t'))), + b'\x7f' => Ok(key_helper("", Key::Backspace)), + b'\0' => Ok(key_helper("C", Key::Char(' '))), + c @ b'\x01'..=b'\x1a' => Ok(key_helper("C", Key::Char((c + 96) as char))), + c @ b'\x1c'..=b'\x1f' => Ok(key_helper("C", Key::Char((c + 24) as char))), c => { let character = parse_utf8_char(c, iter)?; Ok(Event::Key( Key::Char(character), - KeyType::Press, - KeyMods::NONE.shift(character.is_uppercase()), + ButtonType::Press, + Modifiers::NONE.shift(character.is_uppercase()), )) } } @@ -265,37 +299,32 @@ where Err(error()) } -fn try_parse_ansi_sequence(iter: &mut I) -> io::Result +fn parse_ansi_sequence(iter: &mut I) -> io::Result where I: Iterator>, { let error = io::Error::other("Could not parse event"); match iter.next() { + None => Ok(key_helper("", Key::Escape)), Some(Ok(b'O')) => match iter.next() { - Some(Ok(val @ b'P'..=b's')) => Ok(press_key(Key::F(1 + val - b'P'), KeyMods::NONE)), + Some(Ok(val @ b'P'..=b's')) => Ok(key_helper("", Key::F(1 + val - b'P'))), _ => Err(error), }, - Some(Ok(b'[')) => try_parse_csi_sequence(iter).ok_or(error), + Some(Ok(b'[')) => parse_csi_sequence(iter).ok_or(error), Some(Ok(c)) => match c { - b'\r' => Ok(press_key(Key::Char('\r'), KeyMods::ALT)), - b'\n' => Ok(press_key(Key::Char('j'), KeyMods::CTRL.alt(true))), - b'\t' => Ok(press_key(Key::Char('\t'), KeyMods::ALT)), - b'\x7f' => Ok(press_key(Key::Backspace, KeyMods::ALT)), - b'\0' => Ok(press_key(Key::Char(' '), KeyMods::CTRL.alt(true))), - c @ b'\x01'..=b'\x1a' => Ok(press_key( - Key::Char((c + 96) as char), - KeyMods::CTRL.alt(true), - )), - c @ b'\x1c'..=b'\x1f' => Ok(press_key( - Key::Char((c + 24) as char), - KeyMods::CTRL.alt(true), - )), + b'\r' => Ok(key_helper("A", Key::Char('\r'))), + b'\n' => Ok(key_helper("CA", Key::Char('j'))), + b'\t' => Ok(key_helper("A", Key::Char('\t'))), + b'\x7f' => Ok(key_helper("A", Key::Backspace)), + b'\0' => Ok(key_helper("CA", Key::Char(' '))), + c @ b'\x01'..=b'\x1a' => Ok(key_helper("CA", Key::Char((c + 96) as char))), + c @ b'\x1c'..=b'\x1f' => Ok(key_helper("CA", Key::Char((c + 24) as char))), c => { let character = parse_utf8_char(c, iter)?; Ok(Event::Key( Key::Char(character), - KeyType::Press, - KeyMods::NONE.shift(character.is_uppercase()).alt(true), + ButtonType::Press, + Modifiers::NONE.shift(character.is_uppercase()).alt(true), )) } }, @@ -303,26 +332,255 @@ where } } -fn try_parse_csi_sequence(iter: &mut I) -> Option +fn 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(press_key(Key::F(1 + val - b'A'), KeyMods::NONE)), + Some(Ok(val @ b'A'..=b'E')) => Some(key_helper("", Key::F(1 + val - b'A'))), _ => None, }, - Some(Ok(b'D')) => Some(press_key(Key::Left, KeyMods::NONE)), - Some(Ok(b'C')) => Some(press_key(Key::Right, KeyMods::NONE)), - Some(Ok(b'A')) => Some(press_key(Key::Up, KeyMods::NONE)), - Some(Ok(b'B')) => Some(press_key(Key::Down, KeyMods::NONE)), - Some(Ok(b'H')) => Some(press_key(Key::Home, KeyMods::NONE)), - Some(Ok(b'F')) => Some(press_key(Key::End, KeyMods::NONE)), - Some(Ok(b'Z')) => Some(press_key(Key::Tab, KeyMods::SHIFT)), + Some(Ok(b'D')) => Some(key_helper("", Key::Left)), + Some(Ok(b'C')) => Some(key_helper("", Key::Right)), + Some(Ok(b'A')) => Some(key_helper("", Key::Up)), + Some(Ok(b'B')) => Some(key_helper("", Key::Down)), + Some(Ok(b'H')) => Some(key_helper("", Key::Home)), + Some(Ok(b'F')) => Some(key_helper("", Key::End)), + Some(Ok(b'Z')) => Some(key_helper("", Key::Tab)), + Some(Ok(b'<')) => parse_xterm_mouse(iter), + Some(Ok(b'M')) => parse_x10_mouse(iter), + Some(Ok(c @ b'0'..=b'9')) => parse_numbered_escape(iter, c), + None => Some(key_helper("A", Key::Char('['))), _ => None, } } +fn parse_numbered_escape(iter: &mut I, c: u8) -> Option +where + I: Iterator>, +{ + let mut buf = Vec::new(); + buf.push(c); + let mut c = iter.next().unwrap().unwrap(); + // The final byte of a CSI sequence can be in the range 64-126, so let's keep reading + // anything else. + while c < 64 || c > 126 { + 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 nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); + + let cb = nums[0]; + let cx = nums[1]; + let cy = nums[2]; + + let mods = Modifiers::NONE; + + let event = match cb { + 32 => Event::Mouse(mods, MouseButton::Left, ButtonType::Press, cx, cy), + 33 => Event::Mouse(mods, MouseButton::Middle, ButtonType::Press, cx, cy), + 34 => Event::Mouse(mods, MouseButton::Right, ButtonType::Press, cx, cy), + 35 => Event::Mouse(mods, MouseButton::Unknown, ButtonType::Release, cx, cy), + 64 => Event::Mouse(mods, MouseButton::Unknown, ButtonType::Held, cx, cy), + 96 | 97 => Event::Mouse(mods, MouseButton::WheelUp, ButtonType::Press, cx, cy), + _ => return None, + }; + + Some(event) + } + // Special key code. + b'~' => { + let str_buf = String::from_utf8(buf).unwrap(); + + // This CSI sequence can be a list of semicolon-separated + // numbers. + let nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); + + if nums.is_empty() { + return None; + } + + // TODO: handle multiple values for key modififiers (ex: values + // [3, 2] means Shift+Delete) + if nums.len() > 1 { + return None; + } + + match nums[0] { + 1 | 7 => Some(key_helper("", Key::Home)), + 2 => Some(key_helper("", Key::Insert)), + 3 => Some(key_helper("", Key::Delete)), + 4 | 8 => Some(key_helper("", Key::End)), + 5 => Some(key_helper("", Key::PageUp)), + 6 => Some(key_helper("", Key::PageDown)), + v @ 11..=15 => Some(key_helper("", Key::F(v - 10))), + v @ 17..=21 => Some(key_helper("", Key::F(v - 11))), + v @ 23..=24 => Some(key_helper("", Key::F(v - 12))), + _ => return None, + } + } + b'u' => { + let str_buf = String::from_utf8(buf).unwrap(); + let mut iter = str_buf.split(';'); + let key_code: u32 = iter.next()?.parse().ok()?; + let mut iter = iter.next().unwrap_or("0:1").split(':'); + let modifier: u32 = iter.next()?.parse().ok()?; + let key_type: u32 = iter.next().unwrap_or("1").parse().ok()?; + println!("{str_buf}\r"); + println!("{modifier}\r"); + + let char = char::from_u32(key_code); + // let shift = modifier & 1 == 1; + // let alt = modifier & 2 == 2; + // let ctrl = modifier & 4 == 4; + let button_type = match key_type { + 1 => ButtonType::Press, + 2 => ButtonType::Held, + 3 => ButtonType::Release, + _ => return None, + }; + + Some(Event::Key(Key::Char(char?), button_type, Modifiers::NONE)) + } + b'A' | b'B' | b'C' | b'D' | b'F' | b'H' => { + let str_buf = String::from_utf8(buf).unwrap(); + + // This CSI sequence can be a list of semicolon-separated + // numbers. + let nums: Vec = str_buf.split(';').map(|n| n.parse().unwrap()).collect(); + + if !(nums.len() == 2 && nums[0] == 1) { + return None; + } + let mods = nums[1] - 1; + let shift = mods & 1 == 1; + let alt = mods & 2 == 2; + let ctrl = mods & 4 == 4; + match c { + b'D' => Some(simple_key(Key::Left, shift, alt, ctrl)), + b'C' => Some(simple_key(Key::Right, shift, alt, ctrl)), + b'A' => Some(simple_key(Key::Up, shift, alt, ctrl)), + b'B' => Some(simple_key(Key::Down, shift, alt, ctrl)), + b'H' => Some(simple_key(Key::Home, shift, alt, ctrl)), + b'F' => Some(simple_key(Key::End, shift, alt, ctrl)), + _ => return None, + } + } + + _ => None, + } +} + +fn parse_x10_mouse(iter: &mut I) -> Option +where + I: Iterator>, +{ + // X10 emulation mouse encoding: ESC [ CB Cx Cy (6 characters only). + let mut next = || iter.next().unwrap().unwrap(); + + let cb = next() as i8 - 32; + // (0, 0) are the coords for upper left. + let cx = next().saturating_sub(33) as u16; + let cy = next().saturating_sub(33) as u16; + + let mods = Modifiers::NONE; + Some(match cb & 0b11 { + 0 => { + if cb & 0x40 != 0 { + Event::Mouse(mods, MouseButton::WheelUp, ButtonType::Press, cx, cy) + } else { + Event::Mouse(mods, MouseButton::WheelUp, ButtonType::Press, cx, cy) + } + } + 1 => { + if cb & 0x40 != 0 { + Event::Mouse(mods, MouseButton::WheelDown, ButtonType::Press, cx, cy) + } else { + Event::Mouse(mods, MouseButton::Middle, ButtonType::Press, cx, cy) + } + } + 2 => { + if cb & 0x40 != 0 { + Event::Mouse(mods, MouseButton::WheelLeft, ButtonType::Press, cx, cy) + } else { + Event::Mouse(mods, MouseButton::Right, ButtonType::Press, cx, cy) + } + } + 3 => { + if cb & 0x40 != 0 { + Event::Mouse(mods, MouseButton::WheelRight, ButtonType::Press, cx, cy) + } else { + Event::Mouse(mods, MouseButton::Unknown, ButtonType::Release, cx, cy) + } + } + _ => unreachable!(), + }) +} + +fn parse_xterm_mouse(iter: &mut I) -> Option +where + I: Iterator>, +{ + // 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 nums = &mut str_buf.split(';'); + + let cb = nums.next()?.parse::().unwrap(); + let cx = nums.next()?.parse::().unwrap().saturating_sub(1); + let cy = nums.next()?.parse::().unwrap().saturating_sub(1); + + let shift = cb & 4 == 4; + let alt = cb & 8 == 8; + let ctrl = cb & 16 == 16; + let mods = Modifiers::new(shift, alt, ctrl); + let trimmed_cb = cb ^ (cb & 0b00011100); + + let event = match trimmed_cb { + 0..=2 | 64..=67 => { + let button = match trimmed_cb { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + 64 => MouseButton::WheelUp, + 65 => MouseButton::WheelDown, + 66 => MouseButton::WheelLeft, + 67 => MouseButton::WheelRight, + _ => unreachable!(), + }; + match c { + b'M' => Event::Mouse(mods, button, ButtonType::Press, cx, cy), + b'm' => Event::Mouse(mods, button, ButtonType::Release, cx, cy), + _ => return None, + } + } + 32 => Event::Mouse(mods, MouseButton::Left, ButtonType::Held, cx, cy), + 33 => Event::Mouse(mods, MouseButton::Middle, ButtonType::Held, cx, cy), + 34 => Event::Mouse(mods, MouseButton::Right, ButtonType::Held, cx, cy), + 35 => Event::Mouse(mods, MouseButton::None, ButtonType::Held, cx, cy), + 3 => Event::Mouse(mods, MouseButton::Unknown, ButtonType::Release, cx, cy), + _ => return None, + }; + + Some(event) +} + #[test] fn test_parse_utf8() { let string = "abcéŷ¤£€ù%323"; diff --git a/src/windows.rs b/src/windows.rs index 4483a70..d3d30b9 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,4 +1,4 @@ -use crate::input::{Event, Key, KeyMods, KeyType, press_key}; +use crate::input::{Event, Key, key_helper}; use std::os::windows::raw::HANDLE; use std::{io, mem, time::Duration}; @@ -64,6 +64,30 @@ fn get_console_mode(handle: HANDLE, mode: &mut u32) -> io::Result<()> { } } +/// Enable kitty comprehensive keyboard handling protocol +pub fn enable_kitty_keyboard() {} + +/// Disable kitty comprehensive keyboard handling protocol +pub fn disable_kitty_keyboard() {} + +/// Enable mouse input, if available +/// +/// # Errors +/// +/// Never currently +pub fn enable_mouse_input() -> io::Result<()> { + Ok(()) +} + +/// Disable mouse input, if available +/// +/// # Errors +/// +/// Never currently +pub fn disable_mouse_input() -> io::Result<()> { + Ok(()) +} + /// Enables raw mode, which disables line buffering, input echoing, and output canonicalization /// /// # Errors @@ -244,36 +268,36 @@ fn parse_key_event(event: &KeyEventRecord) -> Event { let shift = event.control_key_state & 0x0010 != 0; // SHIFT_PRESSED match event.virtual_key_code { - 0x08 => press_key(Key::Backspace, KeyMods::NONE), + 0x08 => key_helper("", Key::Backspace), 0x09 => { if shift { - press_key(Key::Tab, KeyMods::SHIFT) + key_helper("S", Key::Tab) } else { - press_key(Key::Tab, KeyMods::NONE) + key_helper("", Key::Tab) } } - 0x0D => press_key(Key::Char('\n'), KeyMods::NONE), - 0x1B => press_key(Key::Escape, KeyMods::NONE), - 0x21 => press_key(Key::PageUp, KeyMods::NONE), - 0x22 => press_key(Key::PageDown, KeyMods::NONE), - 0x23 => press_key(Key::End, KeyMods::NONE), - 0x24 => press_key(Key::Home, KeyMods::NONE), - 0x25 => press_key(Key::Left, KeyMods::NONE), - 0x26 => press_key(Key::Up, KeyMods::NONE), - 0x27 => press_key(Key::Right, KeyMods::NONE), - 0x28 => press_key(Key::Down, KeyMods::NONE), - 0x2D => press_key(Key::Insert, KeyMods::NONE), - 0x2E => press_key(Key::Delete, KeyMods::NONE), + 0x0D => key_helper("", Key::Char('\n')), + 0x1B => key_helper("", Key::Escape), + 0x21 => key_helper("", Key::PageUp), + 0x22 => key_helper("", Key::PageDown), + 0x23 => key_helper("", Key::End), + 0x24 => key_helper("", Key::Home), + 0x25 => key_helper("", Key::Left), + 0x26 => key_helper("", Key::Up), + 0x27 => key_helper("", Key::Right), + 0x28 => key_helper("", Key::Down), + 0x2D => key_helper("", Key::Insert), + 0x2E => key_helper("", Key::Delete), // I don't think anybody is going to try to press F256 clippy #[allow(clippy::cast_possible_truncation)] - 0x70..=0x87 => press_key(Key::F(event.virtual_key_code - 0x6F) as u8), KeyMods::NONE), + 0x70..=0x87 => key_helper("", Key::F((event.virtual_key_code - 0x6F) as u8)), _ => { let num = u32::from(unsafe { event.u_char.unicode_char }); let c = char::from_u32(num).unwrap_or(' '); if ctrl && c.is_ascii_alphabetic() { - press_key(Key::Char(c), KeyMods::CTRL) + key_helper("C", Key::Char(c)) } else { - press_key(Key::Char(c), KeyMods::NONE) + key_helper("", Key::Char(c)) } } }