add unix input

This commit is contained in:
2025-03-30 12:06:51 -04:00
parent 25f2697eec
commit a4991f1582
8 changed files with 280 additions and 122 deletions

View File

@@ -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

View File

@@ -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<Option<InternalEvent>> {
// 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,
}

View File

@@ -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)

View File

@@ -1,5 +1,5 @@
#![warn(clippy::all, clippy::pedantic)]
pub mod ansi;
// pub mod input;
pub mod input;
pub mod os;

View File

@@ -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 {

218
src/unix_input.rs Normal file
View File

@@ -0,0 +1,218 @@
use crate::input::{Event, KeyEvent, MouseEvent, MouseButton};
use std::io;
pub fn try_parse_event<I>(item: u8, iter: &mut I) -> io::Result<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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<I>(c: u8, iter: &mut I) -> io::Result<char>
where
I: Iterator<Item = io::Result<u8>>,
{
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<I>(iter: &mut I) -> io::Result<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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<I>(iter: &mut I) -> Option<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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<I>(iter: &mut I) -> Option<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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<I>(iter: &mut I) -> Option<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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::<u16>().ok()?;
let cx = nums.next()?.parse::<u16>().ok()?;
let cy = nums.next()?.parse::<u16>().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<I>(c: u8, iter: &mut I) -> Option<Event>
where
I: Iterator<Item = io::Result<u8>>,
{
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<u16> = 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);
}
}

0
src/windows_input.rs Normal file
View File