From 652c269b1d5e717905a8fcd2c805ab89909c2cf6 Mon Sep 17 00:00:00 2001 From: Massimo Redaelli Date: Sat, 20 Mar 2021 22:13:19 +0100 Subject: [PATCH] init bindings --- src/conf.rs | 6 ++++ src/conf/bindings.rs | 84 +++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 85 +++++++++++++++++++++++++++++++++++++++----- src/terminal/keys.rs | 33 ++++++++++++++--- src/types.rs | 1 + 5 files changed, 195 insertions(+), 14 deletions(-) create mode 100644 src/conf/bindings.rs diff --git a/src/conf.rs b/src/conf.rs index 09bf7bb73..45f4f57cb 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -39,6 +39,7 @@ pub mod pgp; pub mod tags; #[macro_use] pub mod shortcuts; +pub mod bindings; mod listing; pub mod terminal; mod themes; @@ -46,6 +47,7 @@ pub use themes::*; pub mod accounts; pub use self::accounts::Account; +pub use self::bindings::*; pub use self::composing::*; pub use self::pgp::*; pub use self::shortcuts::*; @@ -212,6 +214,7 @@ pub struct FileSettings { pub terminal: TerminalSettings, #[serde(default)] pub log: LogSettings, + pub bindings: Bindings, } #[derive(Debug, Clone, Default, Serialize)] @@ -490,6 +493,7 @@ pub struct Settings { pub pgp: PGPSettings, pub terminal: TerminalSettings, pub log: LogSettings, + pub bindings: Bindings, } impl Settings { @@ -522,6 +526,7 @@ impl Settings { pgp: fs.pgp, terminal: fs.terminal, log: fs.log, + bindings: fs.bindings, }) } @@ -545,6 +550,7 @@ impl Settings { pgp: fs.pgp, terminal: fs.terminal, log: fs.log, + bindings: fs.bindings, }) } } diff --git a/src/conf/bindings.rs b/src/conf/bindings.rs new file mode 100644 index 000000000..4462bd050 --- /dev/null +++ b/src/conf/bindings.rs @@ -0,0 +1,84 @@ +/* + * meli - configuration module. + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use crate::terminal::Key; +use melib::{MeliError, Result}; +use serde::de::{Deserialize, Deserializer}; +use std::collections::HashMap; +use toml::Value; + +type BindingType = HashMap, String>; +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Bindings { + #[serde(deserialize_with = "deserialize_bindings")] + pub normal: BindingType, +} + +pub fn filter(bs: &BindingType, key: &[(Key, Vec)]) -> BindingType { + let mut res: BindingType = bs.clone(); + for (idx, (k, _)) in key.iter().enumerate() { + res = res.into_iter() + .filter(|(ks, _)| ks.get(idx).map(|c| c == k).unwrap_or(false)) + .collect() + } + res +} + +fn string_to_keys(s: &str) -> Result> { + s.split_whitespace() + .map(|s| { + Key::deserialize(Value::String(s.to_owned())) + .map_err(|e| MeliError::new(format!("{}", e))) + }) + .collect() +} + +fn deserialize_bindings<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let map: HashMap = HashMap::deserialize(deserializer)?; + let (ok, errors): (Vec<_>, Vec<_>) = map + .into_iter() + .map(|(k, v)| (string_to_keys(&k), v)) + .partition(|(k, _)| k.is_ok()); + + if !errors.is_empty() { + Err(serde::de::Error::custom( + errors + .into_iter() + .map(|(k, _)| format!("{}", k.err().unwrap())) + .collect::>() + .join(","), + )) + } else { + Ok(ok.into_iter().map(|(k, v)| (k.unwrap(), v)).collect()) + } +} + +impl Default for Bindings { + fn default() -> Self { + Self { + normal: HashMap::new(), + } + } +} diff --git a/src/state.rs b/src/state.rs index b58d3c1f7..f845913d1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -34,7 +34,7 @@ use melib::backends::{AccountHash, BackendEventConsumer}; use crate::jobs::JobExecutor; use crate::terminal::screen::Screen; -use crossbeam::channel::{unbounded, Receiver, Sender}; +use crossbeam::channel::{after, unbounded, Receiver, Sender}; use indexmap::IndexMap; use smallvec::SmallVec; use std::env; @@ -48,6 +48,74 @@ struct InputHandler { tx: Sender, state_tx: Sender, control: std::sync::Weak<()>, + bindings: Bindings, + chord_timeout_ms: u32, +} + +use std::time::{Duration, Instant}; +#[derive(Debug, Clone)] +struct BindingHandler { + last_ts: Instant, + timeoutlen: Duration, + tx: Sender, + bindings: Bindings, + prefix: Vec<(Key, Vec)>, +} +impl BindingHandler { + pub fn new(tx: Sender, bindings: Bindings, chord_timeout_ms: u32) -> Self { + BindingHandler { + tx, + last_ts: Instant::now(), + timeoutlen: Duration::new(0, chord_timeout_ms * 1000 * 1000), + bindings: bindings.clone(), + prefix: vec![], + } + } + pub fn handle_input(&mut self, key: Key, x: Vec) -> bool { + if Instant::now() - self.last_ts > self.timeoutlen && !self.prefix.is_empty() { + // No input for a while, but some keys left over: just send what we have + for key in &self.prefix { + self.tx.send(ThreadEvent::Input(key.clone())).unwrap(); + } + self.prefix = vec![]; + // and proceed normally + } + + self.prefix.push((key.clone(), x.clone())); + let filtered_bindings = filter(&self.bindings.normal, &self.prefix); + let need_to_wait = match filtered_bindings.len() { + 0 => { + // No matching macro: send the keys + for key in &self.prefix { + self.tx.send(ThreadEvent::Input(key.clone())).unwrap(); + } + self.prefix = vec![]; + false + } + 1 => { + let (keys, cmd) = filtered_bindings.into_iter().next().unwrap(); + if self.prefix.len() == keys.len() { + // Exact match: send the command + // TODO: if we want them recursive... not sure + // TODO: decoding special characters + for key in cmd.chars() { + self.tx + .send(ThreadEvent::Input((Key::Char(key), vec![]))) + .unwrap(); + } + self.prefix = vec![]; + false + } else { + true + } + } + _ => true, + }; + if need_to_wait { + self.last_ts = Instant::now(); + } + need_to_wait + } } impl InputHandler { @@ -62,17 +130,13 @@ impl InputHandler { let rx = self.rx.clone(); let pipe = self.pipe.0; let tx = self.state_tx.clone(); + let bindings = self.bindings.clone(); + let timeout = self.chord_timeout_ms.clone(); thread::Builder::new() .name("input-thread".to_string()) .spawn(move || { - get_events( - |i| { - tx.send(ThreadEvent::Input(i)).unwrap(); - }, - &rx, - pipe, - working, - ) + let mut h = BindingHandler::new(tx.clone(), bindings, timeout); + get_events(|(k, x)| h.handle_input(k, x), &rx, pipe, working, timeout) }) .unwrap(); self.control = control; @@ -302,6 +366,7 @@ impl State { let working = Arc::new(()); let control = Arc::downgrade(&working); + let bindings = settings.bindings.clone(); let mut s = State { screen: Screen { cols, @@ -344,6 +409,8 @@ impl State { tx: input_thread.0, control, state_tx: sender.clone(), + bindings, + chord_timeout_ms: 500u32, }, sender, receiver, diff --git a/src/terminal/keys.rs b/src/terminal/keys.rs index bdb4f456f..3382aab13 100644 --- a/src/terminal/keys.rs +++ b/src/terminal/keys.rs @@ -25,7 +25,7 @@ use serde::{Serialize, Serializer}; use termion::event::Event as TermionEvent; use termion::event::Key as TermionKey; -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Hash)] pub enum Key { /// Backspace. Backspace, @@ -67,6 +67,7 @@ pub enum Key { Esc, Mouse(termion::event::MouseEvent), Paste(String), + ChordTimeout, } pub use termion::event::MouseButton; @@ -98,6 +99,7 @@ impl fmt::Display for Key { Delete => write!(f, "Delete"), Insert => write!(f, "Insert"), Mouse(_) => write!(f, "Mouse"), + ChordTimeout => write!(f, "Chord timeout"), } } } @@ -151,6 +153,8 @@ enum InputMode { pub enum InputCommand { /// Exit thread Kill, + /// Stop waiting for the next key in a chord + ChordTimeout, } use nix::poll::{poll, PollFd, PollFlags}; @@ -166,10 +170,11 @@ use termion::input::TermReadEventsAndRaw; */ /// The thread function that listens for user input and forwards it to the main event loop. pub fn get_events( - mut closure: impl FnMut((Key, Vec)), + mut closure: impl FnMut((Key, Vec)) -> bool, rx: &Receiver, new_command_fd: RawFd, working: std::sync::Arc<()>, + chord_timeout_ms: u32, ) { let stdin = std::io::stdin(); let stdin_fd = PollFd::new(std::io::stdin().as_raw_fd(), PollFlags::POLLIN); @@ -177,15 +182,28 @@ pub fn get_events( let mut input_mode = InputMode::Normal; let mut paste_buf = String::with_capacity(256); let mut stdin_iter = stdin.events_and_raw(); - 'poll_while: while let Ok(_n_raw) = poll(&mut [new_command_pollfd, stdin_fd], -1) { - //debug!(_n_raw); + let mut waiting_for_chord = false; + 'poll_while: while let Ok(_n_raw) = poll( + &mut [new_command_pollfd, stdin_fd], + if waiting_for_chord { + chord_timeout_ms as i32 + } else { + -1 + }, + ) { select! { default => { + if _n_raw == 0 { + if waiting_for_chord { + waiting_for_chord = closure((Key::ChordTimeout, vec![])); + } + continue 'poll_while; + } if stdin_fd.revents().is_some() { 'stdin_while: while let Some(c) = stdin_iter.next(){ match (c, &mut input_mode) { (Ok((TermionEvent::Key(k), bytes)), InputMode::Normal) => { - closure((Key::from(k), bytes)); + waiting_for_chord = closure((Key::from(k), bytes)); continue 'poll_while; } ( @@ -236,6 +254,10 @@ pub fn get_events( let _ = nix::unistd::read(new_command_fd, buf.as_mut()); match cmd.unwrap() { InputCommand::Kill => return, + InputCommand::ChordTimeout => { + closure((Key::ChordTimeout, vec![])); + continue 'poll_while; + } } } }; @@ -353,6 +375,7 @@ impl Serialize for Key { Key::Null => serializer.serialize_str("Null"), Key::Mouse(_) => unreachable!(), Key::Paste(s) => serializer.serialize_str(s), + Key::ChordTimeout => unreachable!(), } } } diff --git a/src/types.rs b/src/types.rs index fb3711e8f..766948d4d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -93,6 +93,7 @@ pub enum ForkType { NewDraft(File, std::process::Child), } + #[derive(Debug, PartialEq, Copy, Clone)] pub enum NotificationType { Info,