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:
parent
09600a3d40
commit
00223b32c9
3 changed files with 357 additions and 24 deletions
330
src/input.rs
Normal file
330
src/input.rs
Normal 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
|
||||
}
|
||||
}
|
49
src/main.rs
49
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<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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue