From dff9052e2b00f8267a14b3bab732f17c84a128e0 Mon Sep 17 00:00:00 2001 From: Andrew Radev Date: Thu, 10 Feb 2022 21:12:08 +0200 Subject: [PATCH] Implement setting mappings from config --- res/default_config.yaml | 15 +++++++++++++++ src/input.rs | 21 ++++++++++++++++++++- src/ui/action.rs | 38 +++++++++++++++++++++++++++++++++++++- src/ui/mod.rs | 15 ++++++++++----- 4 files changed, 82 insertions(+), 7 deletions(-) diff --git a/res/default_config.yaml b/res/default_config.yaml index b3c0df4..17bfb2f 100644 --- a/res/default_config.yaml +++ b/res/default_config.yaml @@ -16,3 +16,18 @@ zoom: 1.0 # - ["alacritty", "-e", "vim", "{path}"] # editor_command: ["gvim", "{path}"] + +# You can set your own keybindings, or unset the defaults by setting them to +# "Noop". See the API documentaiton for a full list of actions, under +# `ui::action::Action`. +# +# The keybindings are passed along to GDK: +# - if given `key_char`, it's passed along to `Key::from_unicode` +# - if given `key_name`, `Key::from_name` is called with it. So, "plus" +# instead of "+". +# - Modifiers that are supported: "control", "shift", "alt" +# +mappings: [] +# mappings: +# - { key_char: "q", modifiers: [], action: "Quit" } +# - { key_name: "minus", modifiers: ["control"], action: "ZoomOut" } diff --git a/src/input.rs b/src/input.rs index 17f3144..b5a9f22 100644 --- a/src/input.rs +++ b/src/input.rs @@ -16,6 +16,7 @@ use structopt::StructOpt; use tempfile::NamedTempFile; use crate::assets::HIGHLIGHT_JS_VERSION; +use crate::ui::action::Action; /// Command-line options. Managed by StructOpt. #[derive(Debug, StructOpt)] @@ -94,6 +95,7 @@ impl Options { /// config directory named "config.yaml". /// #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct Config { /// The zoom level of the page. Defaults to 1.0, but on a HiDPI screen should be set to a /// higher value. @@ -104,11 +106,28 @@ pub struct Config { /// which will produce a command-line warning when it's attempted. /// pub editor_command: Vec, + + /// Custom mappings. Each entry can contain three keys: + /// + /// - `key_char` or `key_name`: A descriptor, passed along to [`gdk::keys::Key::from_unicode`] + /// or [`gdk::keys::Key::from_name`] respectively. + /// - `modifiers`: A list of modifiers, either "control", "shift", or "alt". + /// - `action`: See [`crate::ui::action::Action`]. + /// + pub mappings: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MappingDefinition { + pub key_char: Option, + pub key_name: Option, + pub modifiers: Vec, + pub action: Action, } impl Default for Config { fn default() -> Self { - Self { zoom: 1.0, editor_command: Vec::new() } + Self { zoom: 1.0, editor_command: Vec::new(), mappings: Vec::new() } } } diff --git a/src/ui/action.rs b/src/ui/action.rs index b46fd15..41cc6b8 100644 --- a/src/ui/action.rs +++ b/src/ui/action.rs @@ -2,11 +2,15 @@ use std::collections::HashMap; +use anyhow::anyhow; use gdk::ModifierType; use gdk::keys::{self, Key}; +use serde::{Serialize, Deserialize}; + +use crate::input::MappingDefinition; /// Mappable actions -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Action { /// Placeholder action to allow unmapping keys Noop, @@ -49,6 +53,7 @@ pub enum Action { /// A mapping from key bindings to all the different UI actions. Initialized with a full set of /// defaults, which can be overridden by configuration. /// +#[derive(Clone)] pub struct Keymaps { mappings: HashMap<(ModifierType, Key), Action>, } @@ -86,6 +91,37 @@ impl Keymaps { Self { mappings: HashMap::new() } } + /// Parse the given mappings as described in [`crate::input::Config`] + /// + pub fn add_config_mappings(&mut self, mappings: &Vec) -> anyhow::Result<()> { + for mapping in mappings { + let mut modifiers = ModifierType::empty(); + for m in &mapping.modifiers { + match m.as_str() { + "control" => { modifiers |= ModifierType::CONTROL_MASK; } + "shift" => { modifiers |= ModifierType::SHIFT_MASK; } + "alt" => { modifiers |= ModifierType::MOD1_MASK; } + _ => { + { return Err(anyhow!("Unknown modifier: {}", m)); } + }, + } + } + + let key = + if let Some(c) = mapping.key_char { + Key::from_unicode(c) + } else if let Some(name) = &mapping.key_name { + Key::from_name(name) + } else { + return Err(anyhow!("No `key_char` or `key_name` given: {:?}", mapping)); + }; + + self.set_action(modifiers, key, mapping.action.clone()); + } + + Ok(()) + } + /// Get the action corresponding to the given modifiers and key. Uppercase unicode letters like /// are normalized to a lowercase letter + shift. /// diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 640231f..3d4f82b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -8,7 +8,7 @@ use std::path::{PathBuf, Path}; use std::process::Command; use gtk::prelude::*; -use log::{debug, warn}; +use log::{debug, warn, error}; use pathbuftools::PathBufTools; use crate::assets::Assets; @@ -107,14 +107,19 @@ impl App { let filename = self.filename.clone(); let editor_command = self.config.editor_command.clone(); + let mut keymaps = Keymaps::default(); + keymaps.add_config_mappings(&self.config.mappings).unwrap_or_else(|e| { + error!("Mapping parsing error: {}", e); + }); + // Key presses mapped to repeatable events: let browser = self.browser.clone(); - let keymaps = Keymaps::default(); + let keymaps_clone = keymaps.clone(); self.window.connect_key_press_event(move |_window, event| { let keyval = event.keyval(); let keystate = event.state(); - match keymaps.get_action(keystate, keyval) { + match keymaps_clone.get_action(keystate, keyval) { Action::SmallScrollDown => browser.execute_js("window.scrollBy(0, 70)"), Action::BigScrollDown => browser.execute_js("window.scrollBy(0, 250)"), Action::SmallScrollUp => browser.execute_js("window.scrollBy(0, -70)"), @@ -130,12 +135,12 @@ impl App { // Key releases mapped to one-time events: let browser = self.browser.clone(); - let keymaps = Keymaps::default(); + let keymaps_clone = keymaps.clone(); self.window.connect_key_release_event(move |window, event| { let keyval = event.keyval(); let keystate = event.state(); - match keymaps.get_action(keystate, keyval) { + match keymaps_clone.get_action(keystate, keyval) { Action::LaunchEditor => { debug!("Launching an editor"); launch_editor(&editor_command, &filename);