From 00223b32c98a11e6c207f19bb1d29fd088d33317 Mon Sep 17 00:00:00 2001 From: Joe Wilm Date: Thu, 23 Jun 2016 09:48:31 -0700 Subject: [PATCH] Implement special input handling There's a number of keys/combinations that should emit escape sequences to the PTY when triggered. This commit adds a framework to support that. The input::Processor is a type which tracks state of modifier keys. When special keys (like arrow, function) are detected, the processor pulls up a list of candidate escapes to send, and picks the first one based on terminal mode and active modifier keys. The input::Processor is generic over the thing receiving the escape sequences, the input::Notify type. Included is a wrapper for `&mut io::Write` which implements input::Notify and is currently used to connect the processor to the PTY stream. This added handling of the APP_CURSOR mode which changes affects input processing. --- src/input.rs | 330 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 49 ++++---- src/term.rs | 2 + 3 files changed, 357 insertions(+), 24 deletions(-) create mode 100644 src/input.rs diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 00000000..a175afa2 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,330 @@ +//! Handle input from glutin +//! +//! Certain key combinations should send some escape sequence back to the pty. +//! In order to figure that out, state about which modifier keys are pressed +//! needs to be tracked. Additionally, we need a bit of a state machine to +//! determine what to do when a non-modifier key is pressed. +//! +//! TODO would be nice to generalize this so it could work with other windowing +//! APIs +//! +//! TODO handling xmodmap would be good +use glutin::{ElementState, VirtualKeyCode}; + +use term::mode::{self, TermMode}; + +/// Modifier keys +/// +/// Contains a bitflags for modifier keys which are now namespaced thanks to +/// this module wrapper. +mod modifier { + use glutin::ElementState; + + bitflags! { + /// Flags indicating active modifier keys + pub flags Keys: u8 { + /// Left shift + const SHIFT_LEFT = 0b00000001, + /// Right shift + const SHIFT_RIGHT = 0b00000010, + /// Left meta + const META_LEFT = 0b00000100, + /// Right meta + const META_RIGHT = 0b00001000, + /// Left control + const CONTROL_LEFT = 0b00010000, + /// Right control + const CONTROL_RIGHT = 0b00100000, + /// Left alt + const ALT_LEFT = 0b01000000, + /// Right alt + const ALT_RIGHT = 0b10000000, + /// Any shift key + const SHIFT = SHIFT_LEFT.bits + | SHIFT_RIGHT.bits, + /// Any control key + const CONTROL = CONTROL_LEFT.bits + | CONTROL_RIGHT.bits, + /// Any alt key + const ALT = ALT_LEFT.bits + | ALT_RIGHT.bits, + /// Any meta key + const META = META_LEFT.bits + | META_RIGHT.bits, + /// Any mod + const ANY = 0b11111111, + /// No mod + const NONE = 0b00000000, + } + } + + impl Default for Keys { + fn default() -> Keys { + Keys::empty() + } + } + + impl Keys { + /// Take appropriate action given a modifier key and its state + #[inline] + pub fn update(&mut self, state: ElementState, key: Keys) { + match state { + ElementState::Pressed => self.insert(key), + ElementState::Released => self.remove(key), + } + } + } +} + +/// Processes input from glutin. +/// +/// An escape sequence may be emitted in case specific keys or key combinations +/// are activated. +/// +/// TODO also need terminal state when processing input +#[derive(Default)] +pub struct Processor { + /// Active modifier keys + mods: modifier::Keys, +} + +/// Types that are notified of escape sequences from the input::Processor. +pub trait Notify { + /// Notify that an escape sequence should be written to the pty + fn notify(&mut self, &str); +} + +/// Describes a key combination that should emit a control sequence +/// +/// The actual triggering key is omitted here since bindings are grouped by the trigger key. +#[derive(Debug)] +pub struct Binding { + /// Modifier keys required to activate binding + mods: modifier::Keys, + /// String to send to pty if mods and mode match + send: &'static str, + /// Terminal mode required to activate binding + mode: TermMode, + /// excluded terminal modes where the binding won't be activated + notmode: TermMode, +} + +/// Bindings for the LEFT key. +static LEFT_BINDINGS: &'static [Binding] = &[ + Binding { mods: modifier::SHIFT, send: "\x1b[1;2D", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::CONTROL, send: "\x1b[1;5D", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ALT, send: "\x1b[1;3D", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ANY, send: "\x1b[D", mode: mode::ANY, notmode: mode::APP_CURSOR }, + Binding { mods: modifier::ANY, send: "\x1bOD", mode: mode::APP_CURSOR, notmode: mode::NONE }, +]; + +/// Bindings for the RIGHT key +static RIGHT_BINDINGS: &'static [Binding] = &[ + Binding { mods: modifier::SHIFT, send: "\x1b[1;2C", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::CONTROL, send: "\x1b[1;5C", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ALT, send: "\x1b[1;3C", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ANY, send: "\x1b[C", mode: mode::ANY, notmode: mode::APP_CURSOR }, + Binding { mods: modifier::ANY, send: "\x1bOC", mode: mode::APP_CURSOR, notmode: mode::NONE }, +]; + +/// Bindings for the UP key +static UP_BINDINGS: &'static [Binding] = &[ + Binding { mods: modifier::SHIFT, send: "\x1b[1;2A", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::CONTROL, send: "\x1b[1;5A", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ALT, send: "\x1b[1;3A", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ANY, send: "\x1b[A", mode: mode::ANY, notmode: mode::APP_CURSOR }, + Binding { mods: modifier::ANY, send: "\x1bOA", mode: mode::APP_CURSOR, notmode: mode::NONE }, +]; + +/// Bindings for the DOWN key +static DOWN_BINDINGS: &'static [Binding] = &[ + Binding { mods: modifier::SHIFT, send: "\x1b[1;2B", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::CONTROL, send: "\x1b[1;5B", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ALT, send: "\x1b[1;3B", mode: mode::ANY, notmode: mode::NONE }, + Binding { mods: modifier::ANY, send: "\x1b[B", mode: mode::ANY, notmode: mode::APP_CURSOR }, + Binding { mods: modifier::ANY, send: "\x1bOB", mode: mode::APP_CURSOR, notmode: mode::NONE }, +]; + +// key mods escape appkey appcursor crlf +// +// notes: appkey = DECPAM (application keypad mode); not enabled is "normal keypad" +// appcursor = DECCKM (application cursor mode); +// crlf = LNM (Linefeed/new line); wtf is this + +impl Processor { + pub fn new() -> Processor { + Default::default() + } + + pub fn process(&mut self, + state: ElementState, + key: Option, + notifier: &mut N, + mode: TermMode) + where N: Notify + { + if let Some(key) = key { + + // Handle state updates + match key { + VirtualKeyCode::LAlt => self.mods.update(state, modifier::ALT_LEFT), + VirtualKeyCode::RAlt => self.mods.update(state, modifier::ALT_RIGHT), + VirtualKeyCode::LShift => self.mods.update(state, modifier::SHIFT_LEFT), + VirtualKeyCode::RShift => self.mods.update(state, modifier::SHIFT_RIGHT), + VirtualKeyCode::LControl => self.mods.update(state, modifier::CONTROL_LEFT), + VirtualKeyCode::RControl => self.mods.update(state, modifier::CONTROL_RIGHT), + VirtualKeyCode::LWin => self.mods.update(state, modifier::META_LEFT), + VirtualKeyCode::RWin => self.mods.update(state, modifier::META_RIGHT), + _ => () + } + + // Ignore release events + if state == ElementState::Released { + return; + } + + let bindings = match key { + VirtualKeyCode::Left => LEFT_BINDINGS, + VirtualKeyCode::Up => UP_BINDINGS, + VirtualKeyCode::Down => DOWN_BINDINGS, + VirtualKeyCode::Right => RIGHT_BINDINGS, + // Mode keys ignored now + VirtualKeyCode::LAlt | VirtualKeyCode::RAlt | VirtualKeyCode::LShift | + VirtualKeyCode::RShift | VirtualKeyCode::LControl | VirtualKeyCode::RControl | + VirtualKeyCode::LWin | VirtualKeyCode::RWin => return, + // All of the alphanumeric keys get passed through here as well, but there's no work + // to be done for them. + VirtualKeyCode::A | VirtualKeyCode::B | VirtualKeyCode::C | VirtualKeyCode::D | + VirtualKeyCode::E | VirtualKeyCode::F | VirtualKeyCode::G | VirtualKeyCode::H | + VirtualKeyCode::I | VirtualKeyCode::J | VirtualKeyCode::K | VirtualKeyCode::L | + VirtualKeyCode::M | VirtualKeyCode::N | VirtualKeyCode::O | VirtualKeyCode::P | + VirtualKeyCode::Q | VirtualKeyCode::R | VirtualKeyCode::S | VirtualKeyCode::T | + VirtualKeyCode::U | VirtualKeyCode::V | VirtualKeyCode::W | VirtualKeyCode::X | + VirtualKeyCode::Y | VirtualKeyCode::Z => return, + VirtualKeyCode::Key1 | VirtualKeyCode::Key2 | VirtualKeyCode::Key3 | + VirtualKeyCode::Key4 | VirtualKeyCode::Key5 | VirtualKeyCode::Key6 | + VirtualKeyCode::Key7 | VirtualKeyCode::Key8 | VirtualKeyCode::Key9 | + VirtualKeyCode::Key0 => return, + // Log something by default + _ => { + println!("Unhandled key: {:?}; state: {:?}; mods: {:?}", + key, state, self.mods); + return; + }, + }; + + self.process_bindings(bindings, mode, notifier); + } + } + + fn process_bindings(&self, bindings: &[Binding], mode: TermMode, notifier: &mut N) + where N: Notify + { + // Check each binding + for binding in bindings { + // TermMode positive + if binding.mode.is_all() || mode.intersects(binding.mode) { + // TermMode negative + if binding.notmode.is_empty() || !mode.intersects(binding.notmode) { + // Modifier keys + if binding.mods.is_all() || self.mods.intersects(binding.mods) { + // everything matches + notifier.notify(binding.send); + break; + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use term::mode::{self, TermMode}; + + use super::Processor; + use super::modifier; + use super::Binding; + + /// Receiver that keeps a copy of any strings it is notified with + #[derive(Default)] + struct Receiver { + pub got: Option + } + + impl super::Notify for Receiver { + fn notify(&mut self, s: &str) { + self.got = Some(String::from(s)); + } + } + + macro_rules! test_process_binding { + { + name: $name:ident, + binding: $binding:expr, + expect: $expect:expr, + mode: $mode:expr, + mods: $mods:expr + } => { + #[test] + fn $name() { + let bindings = &[$binding]; + + let mut processor = Processor::new(); + processor.mods.insert($mods); + let mut receiver = Receiver::default(); + + processor.process_bindings(bindings, $mode, &mut receiver); + assert_eq!(receiver.got, $expect); + } + } + } + + test_process_binding! { + name: process_binding_nomode_shiftmod_require_shift, + binding: Binding { mods: modifier::SHIFT, send: "\x1b[1;2D", mode: mode::ANY, notmode: mode::NONE }, + expect: Some(String::from("\x1b[1;2D")), + mode: mode::NONE, + mods: modifier::SHIFT + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_shift, + binding: Binding { mods: modifier::SHIFT, send: "\x1b[1;2D", mode: mode::ANY, notmode: mode::NONE }, + expect: None, + mode: mode::NONE, + mods: modifier::NONE + } + + test_process_binding! { + name: process_binding_nomode_controlmod, + binding: Binding { mods: modifier::CONTROL, send: "\x1b[1;5D", mode: mode::ANY, notmode: mode::NONE }, + expect: Some(String::from("\x1b[1;5D")), + mode: mode::NONE, + mods: modifier::CONTROL + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_not_appcursor, + binding: Binding { mods: modifier::ANY, send: "\x1b[D", mode: mode::ANY, notmode: mode::APP_CURSOR }, + expect: Some(String::from("\x1b[D")), + mode: mode::NONE, + mods: modifier::NONE + } + + test_process_binding! { + name: process_binding_appcursormode_nomod_require_appcursor, + binding: Binding { mods: modifier::ANY, send: "\x1bOD", mode: mode::APP_CURSOR, notmode: mode::NONE }, + expect: Some(String::from("\x1bOD")), + mode: mode::APP_CURSOR, + mods: modifier::NONE + } + + test_process_binding! { + name: process_binding_nomode_nomod_require_appcursor, + binding: Binding { mods: modifier::ANY, send: "\x1bOD", mode: mode::APP_CURSOR, notmode: mode::NONE }, + expect: None, + mode: mode::NONE, + mods: modifier::NONE + } +} diff --git a/src/main.rs b/src/main.rs index 33e5e8f9..fb062fc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ mod macros; mod renderer; pub mod grid; mod meter; +mod input; mod tty; pub mod ansi; mod term; @@ -30,11 +31,11 @@ use std::io::{Read, Write, BufWriter}; use std::sync::Arc; use std::sync::mpsc; +use font::FontDesc; use grid::Grid; use meter::Meter; use renderer::{QuadRenderer, GlyphCache}; use term::Term; -use font::FontDesc; use tty::process_should_exit; use util::thread; @@ -51,10 +52,19 @@ enum ShouldExit { No } +struct WriteNotifier<'a, W: Write + 'a>(&'a mut W); +impl<'a, W: Write> input::Notify for WriteNotifier<'a, W> { + fn notify(&mut self, message: &str) { + println!("writing: {:?} [{} bytes]", message.as_bytes(), message.as_bytes().len()); + self.0.write(message.as_bytes()).unwrap(); + } +} + fn handle_event(event: Event, writer: &mut W, terminal: &mut Term, - pty_parser: &mut ansi::Parser) -> ShouldExit + pty_parser: &mut ansi::Parser, + input_processor: &mut input::Processor) -> ShouldExit where W: Write { match event { @@ -68,26 +78,7 @@ fn handle_event(event: Event, writer.write(encoded.as_slice()).unwrap(); }, glutin::Event::KeyboardInput(state, _code, key) => { - match state { - glutin::ElementState::Pressed => { - match key { - Some(glutin::VirtualKeyCode::Up) => { - writer.write("\x1b[A".as_bytes()).unwrap(); - }, - Some(glutin::VirtualKeyCode::Down) => { - writer.write("\x1b[B".as_bytes()).unwrap(); - }, - Some(glutin::VirtualKeyCode::Left) => { - writer.write("\x1b[D".as_bytes()).unwrap(); - }, - Some(glutin::VirtualKeyCode::Right) => { - writer.write("\x1b[C".as_bytes()).unwrap(); - }, - _ => (), - } - }, - _ => (), - } + input_processor.process(state, key, &mut WriteNotifier(writer), *terminal.mode()) }, _ => () } @@ -204,6 +195,8 @@ fn main() { glyph_cache.init(&mut api); }); + let mut input_processor = input::Processor::new(); + 'main_loop: loop { { let mut writer = BufWriter::new(&writer); @@ -211,7 +204,11 @@ fn main() { // Block waiting for next event match rx.recv() { Ok(e) => { - let res = handle_event(e, &mut writer, &mut terminal, &mut pty_parser); + let res = handle_event(e, + &mut writer, + &mut terminal, + &mut pty_parser, + &mut input_processor); if res == ShouldExit::Yes { break; } @@ -223,7 +220,11 @@ fn main() { loop { match rx.try_recv() { Ok(e) => { - let res = handle_event(e, &mut writer, &mut terminal, &mut pty_parser); + let res = handle_event(e, + &mut writer, + &mut terminal, + &mut pty_parser, + &mut input_processor); if res == ShouldExit::Yes { break; diff --git a/src/term.rs b/src/term.rs index 56c34f6a..1d238cb3 100644 --- a/src/term.rs +++ b/src/term.rs @@ -439,6 +439,7 @@ impl ansi::Handler for Term { match mode { ansi::Mode::SwapScreenAndSetRestoreCursor => self.swap_alt(), ansi::Mode::ShowCursor => self.mode.insert(mode::SHOW_CURSOR), + ansi::Mode::CursorKeys => self.mode.insert(mode::APP_CURSOR), _ => { println!(".. ignoring set_mode"); } @@ -450,6 +451,7 @@ impl ansi::Handler for Term { match mode { ansi::Mode::SwapScreenAndSetRestoreCursor => self.swap_alt(), ansi::Mode::ShowCursor => self.mode.remove(mode::SHOW_CURSOR), + ansi::Mode::CursorKeys => self.mode.remove(mode::APP_CURSOR), _ => { println!(".. ignoring unset_mode"); }