From 86e3e8c121bdc4ff2a2cf1a4430d563e0b6c46b1 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 7 Mar 2021 17:47:10 -0500 Subject: sketch out some input handling --- Cargo.toml | 2 +- examples/tmux.rs | 182 ++++++++++--------------- src/input.rs | 396 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 + 4 files changed, 474 insertions(+), 110 deletions(-) create mode 100644 src/input.rs diff --git a/Cargo.toml b/Cargo.toml index 544d2a1..4ef7ca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] itoa = "0.4" +nix = "0.20" terminal_size = "0.1" vt100 = "0.11" @@ -18,6 +19,5 @@ async = ["blocking", "futures-lite"] [dev-dependencies] libc = "0.2" -nix = "0.20" pty-process = { version = "0.1", features = ["backend-smol"] } smol = "1.2" diff --git a/examples/tmux.rs b/examples/tmux.rs index 1bd2c3f..3d12c65 100644 --- a/examples/tmux.rs +++ b/examples/tmux.rs @@ -1,47 +1,14 @@ use pty_process::Command as _; use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; -use std::os::unix::io::AsRawFd as _; use textmode::TextmodeExt as _; -pub struct RawGuard { - termios: nix::sys::termios::Termios, -} - -#[allow(clippy::new_without_default)] -impl RawGuard { - pub fn new() -> Self { - let stdin = std::io::stdin().as_raw_fd(); - let termios = nix::sys::termios::tcgetattr(stdin).unwrap(); - let mut termios_raw = termios.clone(); - nix::sys::termios::cfmakeraw(&mut termios_raw); - nix::sys::termios::tcsetattr( - stdin, - nix::sys::termios::SetArg::TCSANOW, - &termios_raw, - ) - .unwrap(); - Self { termios } - } -} - -impl Drop for RawGuard { - fn drop(&mut self) { - let stdin = std::io::stdin().as_raw_fd(); - let _ = nix::sys::termios::tcsetattr( - stdin, - nix::sys::termios::SetArg::TCSANOW, - &self.termios, - ); - } -} - enum Command { NewWindow, NextWindow, } enum Event { - Input(Vec), + Input(textmode::Key), Output, WindowExit(usize), Command(Command), @@ -123,23 +90,75 @@ impl State { .detach(); } - fn spawn_input_task(&self, ex: &smol::Executor<'_>) { + fn spawn_input_task( + &self, + ex: &smol::Executor<'_>, + mut input: textmode::Input, + ) { let notify = self.wevents.clone(); ex.spawn(async move { let mut waiting_for_command = false; - let mut stdin = smol::Unblock::new(std::io::stdin()); - let mut buf = [0u8; 4096]; loop { - match stdin.read(&mut buf).await { - Ok(bytes) => { - waiting_for_command = Self::handle_input( - &buf[..bytes], - notify.clone(), - waiting_for_command, - ) - .await; + let want_single_char = waiting_for_command; + let key_input = smol::unblock(move || { + if want_single_char { + let key = input.read_key_char(); + (input, key) + } else { + let key = input.read_keys(); + (input, key) } - Err(e) => { + }); + match key_input.await { + (returned_input, Ok(Some(key))) => { + if waiting_for_command { + match key { + textmode::Key::Ctrl(b'n') => { + notify + .send(Event::Input(key)) + .await + .unwrap(); + } + textmode::Key::Char('c') => { + notify + .send(Event::Command( + Command::NewWindow, + )) + .await + .unwrap(); + } + textmode::Key::Char('n') => { + notify + .send(Event::Command( + Command::NextWindow, + )) + .await + .unwrap(); + } + _ => { + // ignore + } + } + waiting_for_command = false; + } else { + match key { + textmode::Key::Ctrl(b'n') => { + waiting_for_command = true; + } + _ => { + notify + .send(Event::Input(key)) + .await + .unwrap(); + } + } + } + input = returned_input; + } + (_, Ok(None)) => { + break; + } + (_, Err(e)) => { eprintln!("{}", e); break; } @@ -194,62 +213,6 @@ impl State { .detach(); } - async fn handle_input( - buf: &[u8], - notify: smol::channel::Sender, - mut waiting_for_command: bool, - ) -> bool { - let bytes = buf.len(); - let mut real_buf = Vec::with_capacity(bytes); - for &c in buf { - if waiting_for_command { - match c { - // ^N - 14 => { - real_buf.push(c); - } - // c - 99 => { - notify - .send(Event::Command(Command::NewWindow)) - .await - .unwrap(); - } - // n - 110 => { - notify - .send(Event::Command(Command::NextWindow)) - .await - .unwrap(); - } - _ => {} - } - waiting_for_command = false; - } else { - match c { - // ^N - 14 => { - if !real_buf.is_empty() { - notify - .send(Event::Input(real_buf.clone())) - .await - .unwrap(); - real_buf.clear(); - } - waiting_for_command = true; - } - _ => { - real_buf.push(c); - } - } - } - } - if !real_buf.is_empty() { - notify.send(Event::Input(real_buf.clone())).await.unwrap(); - } - return waiting_for_command; - } - async fn redraw_current_window(&mut self, tm: &mut textmode::Textmode) { let window = self.current_window(); tm.clear(); @@ -346,28 +309,28 @@ impl State { #[must_use] struct Tmux { - _raw: RawGuard, + input: textmode::Input, tm: textmode::Textmode, state: State, } impl Tmux { async fn new() -> Self { - let _raw = RawGuard::new(); + let input = textmode::Input::new(); let tm = textmode::Textmode::new().await.unwrap(); let state = State::new(); - Self { _raw, tm, state } + Self { input, tm, state } } async fn run(self, ex: &smol::Executor<'_>) { let Self { - _raw, + mut input, mut tm, mut state, } = self; state.new_window(ex, state.wevents.clone()); - state.spawn_input_task(ex); + state.spawn_input_task(ex, input.clone()); ex.run(async { loop { @@ -375,12 +338,12 @@ impl Tmux { Ok(Event::Output) => { state.update_current_window(&mut tm).await; } - Ok(Event::Input(buf)) => { + Ok(Event::Input(key)) => { state .current_window() .child .pty() - .write_all(&buf) + .write_all(&key.into_bytes()) .await .unwrap(); } @@ -430,6 +393,7 @@ impl Tmux { .await; tm.cleanup().await.unwrap(); + input.cleanup(); } } diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..a14e643 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,396 @@ +use std::io::Read as _; +use std::os::unix::io::AsRawFd as _; + +pub enum Key { + String(String), + Char(char), + Bytes(Vec), + Byte(u8), + Ctrl(u8), + Meta(u8), + Backspace, + Up, + Down, + Right, + Left, + KeypadUp, + KeypadDown, + KeypadRight, + KeypadLeft, + Home, + End, + Insert, + Delete, + PageUp, + PageDown, + F(u8), +} + +impl Key { + pub fn into_bytes(self) -> Vec { + use Key::*; + match self { + String(s) => s.into_bytes(), + Char(c) => c.to_string().into_bytes(), + Bytes(s) => s, + Byte(c) => vec![c], + Ctrl(c) => vec![c - b'a' + 1], + Meta(c) => vec![b'\x1b', c], + Backspace => b"\x7f".to_vec(), + Up => b"\x1b[A".to_vec(), + Down => b"\x1b[B".to_vec(), + Right => b"\x1b[C".to_vec(), + Left => b"\x1b[D".to_vec(), + KeypadUp => b"\x1bOA".to_vec(), + KeypadDown => b"\x1bOB".to_vec(), + KeypadRight => b"\x1bOC".to_vec(), + KeypadLeft => b"\x1bOD".to_vec(), + Home => b"\x1b[H".to_vec(), + End => b"\x1b[F".to_vec(), + Insert => b"\x1b[2~".to_vec(), + Delete => b"\x1b[3~".to_vec(), + PageUp => b"\x1b[5~".to_vec(), + PageDown => b"\x1b[6~".to_vec(), + F(c) => match c { + 1 => b"\x1bOP".to_vec(), + 2 => b"\x1bOQ".to_vec(), + 3 => b"\x1bOR".to_vec(), + 4 => b"\x1bOS".to_vec(), + 5 => b"\x1b[15~".to_vec(), + 6 => b"\x1b[17~".to_vec(), + 7 => b"\x1b[18~".to_vec(), + 8 => b"\x1b[19~".to_vec(), + 9 => b"\x1b[20~".to_vec(), + 10 => b"\x1b[21~".to_vec(), + 11 => b"\x1b[23~".to_vec(), + 12 => b"\x1b[24~".to_vec(), + 13 => b"\x1b[25~".to_vec(), + 14 => b"\x1b[26~".to_vec(), + 15 => b"\x1b[28~".to_vec(), + 16 => b"\x1b[29~".to_vec(), + 17 => b"\x1b[31~".to_vec(), + 18 => b"\x1b[32~".to_vec(), + 19 => b"\x1b[33~".to_vec(), + 20 => b"\x1b[34~".to_vec(), + _ => vec![], + }, + } + } +} + +#[derive(Clone)] +pub struct Input { + termios: nix::sys::termios::Termios, + buf: Vec, +} + +#[allow(clippy::new_without_default)] +impl Input { + pub fn new() -> Self { + let stdin = std::io::stdin().as_raw_fd(); + let termios = nix::sys::termios::tcgetattr(stdin).unwrap(); + let mut termios_raw = termios.clone(); + nix::sys::termios::cfmakeraw(&mut termios_raw); + nix::sys::termios::tcsetattr( + stdin, + nix::sys::termios::SetArg::TCSANOW, + &termios_raw, + ) + .unwrap(); + Self { + termios, + buf: Vec::with_capacity(4096), + } + } + + pub fn read_keys(&mut self) -> std::io::Result> { + self.real_read_key(true, false) + } + + pub fn read_keys_utf8(&mut self) -> std::io::Result> { + self.real_read_key(true, true) + } + + pub fn read_key(&mut self) -> std::io::Result> { + self.real_read_key(false, false) + } + + pub fn read_key_char(&mut self) -> std::io::Result> { + self.real_read_key(false, true) + } + + pub fn cleanup(&mut self) { + let stdin = std::io::stdin().as_raw_fd(); + let _ = nix::sys::termios::tcsetattr( + stdin, + nix::sys::termios::SetArg::TCSANOW, + &self.termios, + ); + } + + fn real_read_key( + &mut self, + combine: bool, + utf8: bool, + ) -> std::io::Result> { + match self.next_byte(true)? { + Some(c @ 32..=126) | Some(c @ 128..=255) => { + self.parse_text(c, combine, utf8) + } + Some(c @ 1..=26) => Ok(Some(Key::Ctrl(b'a' + c - 1))), + Some(27) => self.parse_escape_sequence(), + Some(c @ 0) | Some(c @ 28..=31) => { + self.parse_unknown_char(c, combine) + } + Some(127) => Ok(Some(Key::Backspace)), + None => Ok(None), + } + } + + fn parse_text( + &mut self, + c: u8, + combine: bool, + utf8: bool, + ) -> std::io::Result> { + if combine { + let idx = self + .buf + .iter() + .take_while(|&c| { + (32..=126).contains(c) || (128..=255).contains(c) + }) + .count(); + let mut rest = self.buf.split_off(idx); + std::mem::swap(&mut self.buf, &mut rest); + rest.insert(0, c); + if utf8 { + match std::string::String::from_utf8(rest) { + Ok(s) => Ok(Some(Key::String(s))), + Err(e) => Ok(Some(Key::Bytes(e.into_bytes()))), + } + } else { + Ok(Some(Key::Bytes(rest))) + } + } else { + if utf8 { + self.parse_utf8_char(c) + } else { + Ok(Some(Key::Byte(c))) + } + } + } + + #[allow(clippy::unnecessary_wraps)] + fn parse_unknown_char( + &mut self, + c: u8, + combine: bool, + ) -> std::io::Result> { + if combine { + let idx = self + .buf + .iter() + .take_while(|&c| *c == 0 || (28..=31).contains(c)) + .count(); + let mut rest = self.buf.split_off(idx); + std::mem::swap(&mut self.buf, &mut rest); + rest.insert(0, c); + Ok(Some(Key::Bytes(rest))) + } else { + Ok(Some(Key::Byte(c))) + } + } + + fn parse_escape_sequence(&mut self) -> std::io::Result> { + let mut seen = vec![b'\x1b']; + macro_rules! next_byte { + () => { + match self.next_byte(false)? { + Some(c) => c, + None => return Ok(Some(Key::Bytes(seen))), + } + }; + } + enum EscapeState { + Escape, + CSI(Vec), + CKM(Vec), + } + let mut state = EscapeState::Escape; + loop { + let c = next_byte!(); + seen.push(c); + match state { + EscapeState::Escape => match c { + b'[' => { + state = EscapeState::CSI(vec![]); + } + b'O' => { + state = EscapeState::CKM(vec![]); + } + _ => { + return Ok(Some(Key::Meta(c))); + } + }, + EscapeState::CSI(ref mut param) => match c { + b'A' => return Ok(Some(Key::Up)), + b'B' => return Ok(Some(Key::Down)), + b'C' => return Ok(Some(Key::Right)), + b'D' => return Ok(Some(Key::Left)), + b'H' => return Ok(Some(Key::Home)), + b'F' => return Ok(Some(Key::End)), + b'0'..=b'9' => { + param.push(c); + state = EscapeState::CSI(param.to_vec()); + } + b'~' => match param.as_slice() { + [b'2'] => return Ok(Some(Key::Insert)), + [b'3'] => return Ok(Some(Key::Delete)), + [b'5'] => return Ok(Some(Key::PageUp)), + [b'6'] => return Ok(Some(Key::PageDown)), + [b'1', b'5'] => return Ok(Some(Key::F(5))), + [b'1', b'7'] => return Ok(Some(Key::F(6))), + [b'1', b'8'] => return Ok(Some(Key::F(7))), + [b'1', b'9'] => return Ok(Some(Key::F(8))), + [b'2', b'0'] => return Ok(Some(Key::F(9))), + [b'2', b'1'] => return Ok(Some(Key::F(10))), + [b'2', b'3'] => return Ok(Some(Key::F(11))), + [b'2', b'4'] => return Ok(Some(Key::F(12))), + [b'2', b'5'] => return Ok(Some(Key::F(13))), + [b'2', b'6'] => return Ok(Some(Key::F(14))), + [b'2', b'8'] => return Ok(Some(Key::F(15))), + [b'2', b'9'] => return Ok(Some(Key::F(16))), + [b'3', b'1'] => return Ok(Some(Key::F(17))), + [b'3', b'2'] => return Ok(Some(Key::F(18))), + [b'3', b'3'] => return Ok(Some(Key::F(19))), + [b'3', b'4'] => return Ok(Some(Key::F(20))), + _ => { + let mut seq = vec![b'\x1b', b'[']; + seq.extend(param.iter()); + seq.push(b'~'); + return Ok(Some(Key::Bytes(seq))); + } + }, + _ => { + let mut seq = vec![b'\x1b', b'[']; + seq.extend(param.iter()); + seq.push(c); + return Ok(Some(Key::Bytes(seq))); + } + }, + EscapeState::CKM(ref mut param) => match c { + b'A' => return Ok(Some(Key::KeypadUp)), + b'B' => return Ok(Some(Key::KeypadDown)), + b'C' => return Ok(Some(Key::KeypadRight)), + b'D' => return Ok(Some(Key::KeypadLeft)), + b'P' => return Ok(Some(Key::F(1))), + b'Q' => return Ok(Some(Key::F(2))), + b'R' => return Ok(Some(Key::F(3))), + b'S' => return Ok(Some(Key::F(4))), + _ => { + let mut seq = vec![b'\x1b', b'O']; + seq.extend(param.iter()); + seq.push(c); + return Ok(Some(Key::Bytes(seq))); + } + }, + } + } + } + + fn parse_utf8_char( + &mut self, + initial: u8, + ) -> std::io::Result> { + let mut buf = vec![initial]; + + macro_rules! next_byte { + () => { + match self.next_byte(true)? { + Some(c) => { + if (0b1000_0000..=0b1011_1111).contains(&c) { + c + } else { + self.buf = buf + .iter() + .skip(1) + .copied() + .chain(self.buf.iter().copied()) + .collect(); + return Ok(Some(Key::Byte(initial))); + } + } + None => return Ok(None), + } + }; + } + + match initial { + 0b0000_0000..=0b0111_1111 => {} + 0b1100_0000..=0b1101_1111 => { + buf.push(next_byte!()); + } + 0b1110_0000..=0b1110_1111 => { + buf.push(next_byte!()); + buf.push(next_byte!()); + } + 0b1111_0000..=0b1111_0111 => { + buf.push(next_byte!()); + buf.push(next_byte!()); + buf.push(next_byte!()); + } + _ => { + return Ok(Some(Key::Bytes(buf))); + } + } + match std::string::String::from_utf8(buf) { + Ok(s) => Ok(Some(Key::Char(s.chars().next().unwrap()))), + Err(e) => { + let buf = e.into_bytes(); + self.buf = buf + .iter() + .skip(1) + .copied() + .chain(self.buf.iter().copied()) + .collect(); + Ok(Some(Key::Byte(initial))) + } + } + } + + fn next_byte(&mut self, fill: bool) -> std::io::Result> { + if self.buf.is_empty() { + if !fill || !self.fill_buf()? { + return Ok(None); + } + } + let c = self.buf.remove(0); + Ok(Some(c)) + } + + fn fill_buf(&mut self) -> std::io::Result { + self.buf.resize(4096, 0); + // can't use self.read here because the borrow checker can't tell + // that our read implementation doesn't actually need to mutably + // borrow self + let bytes = std::io::stdin().read(&mut self.buf)?; + if bytes == 0 { + return Ok(false); + } + self.buf.truncate(bytes); + Ok(true) + } +} + +impl std::io::Read for Input { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + std::io::stdin().read(buf) + } +} + +impl Drop for Input { + fn drop(&mut self) { + self.cleanup(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 486597d..e63f208 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ +#![allow(clippy::collapsible_if)] + pub mod color; pub mod blocking; +mod input; +pub use input::{Input, Key}; #[cfg(feature = "async")] pub mod r#async; -- cgit v1.2.3