1
0
Fork 0
mirror of https://github.com/alacritty/alacritty.git synced 2024-11-25 14:05:41 -05:00

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.
This commit is contained in:
Joe Wilm 2016-06-23 09:48:31 -07:00
parent 09600a3d40
commit 00223b32c9
3 changed files with 357 additions and 24 deletions

330
src/input.rs Normal file
View file

@ -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<N>(&mut self,
state: ElementState,
key: Option<VirtualKeyCode>,
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<N>(&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<String>
}
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
}
}

View file

@ -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<W>(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<W>(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;

View file

@ -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");
}