Skip to content

Commit

Permalink
Merge branch 'custom-mappings'
Browse files Browse the repository at this point in the history
* custom-mappings:
  Minor renaming and documentation tweaks
  Implement Default to enable skipping fields in mapping defs
  Add some debug logging for mapping definitions
  Tests for defining mappings
  Minor rewording
  Apply minor clippy fix
  Some renaming, restructuring, and documentation
  Implement setting mappings from config
  Fix logging bug
  Fix "upper" key handling
  README updates
  Extract all keybindings to the `Keymaps` struct
  Create a dedicated "Keymaps" structure
  Gitignore cargo config
  Extract browser and file picker into separate UI modules
  • Loading branch information
AndrewRadev committed Feb 27, 2022
2 parents 42ca6ce + 6f3dd1e commit e8e6070
Show file tree
Hide file tree
Showing 14 changed files with 575 additions and 210 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
/.cargo/config.toml

notes

# Ignored since it contains system-specific linker config
/.cargo/config.toml
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ Running the app is as simple as:
quickmd <markdown-file>
```

Pressing escape will close the window. Running it with `--help` should provide more info on the available options. Here's how the output looks for me:
Pressing CTRL+Q will close the window. Running it with `--help` should provide more info on the available options. Here's how the output looks for me:

```
quickmd 0.4.2
quickmd 0.5.0
A simple self-contained markdown previewer.
Code highlighting via highlight.js version 9.18.1
Expand Down
15 changes: 15 additions & 0 deletions res/default_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "+". You can also use it for special keys like "Escape", "F1", etc.
# - Modifiers that are supported: "control", "shift", "alt"
#
mappings: []
# mappings:
# - { key_char: "q", action: "Quit" }
# - { key_name: "minus", mods: ["control"], action: "ZoomOut" }
2 changes: 1 addition & 1 deletion res/help_popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<b>Settings</b>: {yaml_path}
<b>CSS</b>: {css_path}

<big>Keybindings</big>
<big>Default Keybindings</big>

<b>j/k</b>: Scroll up and down.
<b>J/K</b>: Scroll up and down by a larger amount.
Expand Down
2 changes: 1 addition & 1 deletion src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const ICON_PNG: &[u8] = include_bytes!("../res/icon.png");

/// The version of highlight.js the app uses for code highlighting.
///
/// More details about the tool at https://highlightjs.org/
/// More details about the tool at <https://highlightjs.org/>
///
pub const HIGHLIGHT_JS_VERSION: &str = "9.18.1";

Expand Down
2 changes: 1 addition & 1 deletion src/background.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Background monitoring for file-changes.
//! Background monitoring for file changes.
//!
//! Whenever a file changes, we want to regenerate the HTML and send it to the UI for rendering to
//! the user. This is done with the `init_update_loop` function.
Expand Down
36 changes: 30 additions & 6 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Input handling.
//! Input and config handling.
//!
//! Currently, this only includes handling command-line options. Potentially the place to handle
//! any other type of configuration and input to the application.
//! This includes command-line options and settings from the YAML config. Potentially the place to
//! handle any other type of configuration and input to the application.
use std::fs::File;
use std::io;
Expand All @@ -16,8 +16,9 @@ use structopt::StructOpt;
use tempfile::NamedTempFile;

use crate::assets::HIGHLIGHT_JS_VERSION;
use crate::ui::action::Action;

/// Command-line options. Managed by StructOpt.
/// Command-line options. Managed by [`structopt`].
#[derive(Debug, StructOpt)]
pub struct Options {
/// Activates debug logging
Expand All @@ -44,7 +45,7 @@ pub struct Options {
}

impl Options {
/// Creates a new instance by parsing input args. Apart from just running StructOpt's
/// Creates a new instance by parsing input args. Apart from just running [`structopt`]'s
/// initialization, it also adds some additional information to the description that depends on
/// the current environment.
///
Expand Down Expand Up @@ -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.
Expand All @@ -104,11 +106,33 @@ pub struct Config {
/// which will produce a command-line warning when it's attempted.
///
pub editor_command: Vec<String>,

/// Custom mappings. See documentation of [`MappingDefinition`] for details.
pub mappings: Vec<MappingDefinition>,
}

/// A single description of a mapping from a keybinding to a UI action. The fields `key_char` and
/// `key_name` are exclusive, which is validated in [`crate::ui::action::Keymaps`].
///
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct MappingDefinition {
/// A descriptor, passed along to [`gdk::keys::Key::from_unicode`]
pub key_char: Option<char>,

/// A descriptor, passed along to [`gdk::keys::Key::from_name`]
pub key_name: Option<String>,

/// A list of key modifiers, either "control", "shift", or "alt"
pub mods: Vec<String>,

/// The action mapped to this key combination
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() }
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ use quickmd::markdown::Renderer;
use quickmd::ui;

fn main() {
let config = Config::load().
unwrap_or_default();

let options = Options::build();
options.init_logging();

let config = Config::load().
unwrap_or_default();

debug!("Loaded config: {:?}", config);
debug!(" > path: {}", Config::yaml_path().display());
debug!("Using input options: {:?}", options);
Expand All @@ -44,7 +44,7 @@ fn run(config: &Config, options: &Options) -> anyhow::Result<()> {
}

fn launch_file_picker() -> anyhow::Result<PathBuf> {
ui::FilePicker::new().run().ok_or_else(|| {
ui::dialogs::FilePicker::new().run().ok_or_else(|| {
anyhow!("Please provide a markdown file to render or call the program with - to read from STDIN")
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Markdown rendering.
//!
//! Uses the `pulldown_cmark` crate with Github-flavored markdown options enabled. Extracts
//! Uses the [`pulldown_cmark`] crate with Github-flavored markdown options enabled. Extracts
//! languages used in code blocks for highlighting purposes.
use std::fs;
Expand Down
168 changes: 168 additions & 0 deletions src/ui/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Actions on the UI triggered by keybindings.
use std::collections::HashMap;

use anyhow::anyhow;
use gdk::ModifierType;
use gdk::keys::{self, Key};
use log::debug;
use serde::{Serialize, Deserialize};

use crate::input::MappingDefinition;

/// Mappable actions
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Action {
/// Placeholder action to allow unmapping keys
Noop,

/// Scroll up by a small step. Default: `k`
SmallScrollUp,
/// Scroll down by a small step. Default: `j`
SmallScrollDown,

/// Scroll up by a large step. Default: `K`
BigScrollUp,
/// Scroll down by a large step. Default: `J`
BigScrollDown,

/// Scroll to the top of the document. Default: `g`
ScrollToTop,
/// Scroll to the bottom of the document. Default: `G`
ScrollToBottom,

/// Quit the entire application. Default: `ctrl+q`
Quit,

/// Launch an editor instance if it's configured. Default: `e`
LaunchEditor,
/// Exec the current process into an editor instance if it's configured (and it's possible on
/// the OS). Default: `E`
ExecEditor,

/// Zoom the browser in by 10%. Default: `+`
ZoomIn,
/// Zoom the browser out by 10%. Default: `-`
ZoomOut,
/// Reset the zoom level to the configured starting point. Default: `=`
ZoomReset,

/// Show a help popup. Default: `F1`
ShowHelp,
}

impl Default for Action {
fn default() -> Self {
Action::Noop
}
}

/// 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>,
}

impl Default for Keymaps {
fn default() -> Self {
let mut keymaps = Self::new();

// Scroll with j/k, J/K:
keymaps.set_action(ModifierType::empty(), keys::constants::j, Action::SmallScrollDown);
keymaps.set_action(ModifierType::empty(), keys::constants::k, Action::SmallScrollUp);
keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::j, Action::BigScrollDown);
keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::k, Action::BigScrollUp);
// Jump to the top/bottom with g/G
keymaps.set_action(ModifierType::empty(), keys::constants::g, Action::ScrollToTop);
keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::g, Action::ScrollToBottom);
// Ctrl+Q to quit
keymaps.set_action(ModifierType::CONTROL_MASK, keys::constants::q, Action::Quit);
// e, E for editor integration
keymaps.set_action(ModifierType::empty(), keys::constants::e, Action::LaunchEditor);
keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::e, Action::ExecEditor);
// +/-/= for zoom control
keymaps.set_action(ModifierType::empty(), keys::constants::plus, Action::ZoomIn);
keymaps.set_action(ModifierType::empty(), keys::constants::minus, Action::ZoomOut);
keymaps.set_action(ModifierType::empty(), keys::constants::equal, Action::ZoomReset);
// F1 to show help popup
keymaps.set_action(ModifierType::empty(), keys::constants::F1, Action::ShowHelp);

keymaps
}
}

impl Keymaps {
/// Create an empty set of keymaps.
///
pub fn new() -> Self {
Self { mappings: HashMap::new() }
}

/// Parse the given mappings as described in [`crate::input::Config`]
///
pub fn add_config_mappings(&mut self, mappings: &[MappingDefinition]) -> anyhow::Result<()> {
for mapping in mappings {
let mut modifiers = ModifierType::empty();
for m in &mapping.mods {
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)); }
},
}
}

if mapping.key_char.is_some() && mapping.key_name.is_some() {
return Err(
anyhow!("Both `key_char` or `key_name` given, please pick just one: {:?}", mapping)
);
}

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());
debug!("Defined custom mapping: {:?}", mapping);
}

Ok(())
}

/// Get the action corresponding to the given modifiers and key. Uppercase unicode letters like
/// are normalized to a lowercase letter + shift.
///
pub fn get_action(&self, modifiers: ModifierType, key: Key) -> Action {
let (key, modifiers) = Self::normalize_input(key, modifiers);
self.mappings.get(&(modifiers, key)).cloned().unwrap_or(Action::Noop)
}

/// Set the action corresponding to the given modifiers and key. Could override existing
/// actions. Setting `Action::Noop` is the way to "unmap" a keybinding. Uppercase unicode
/// letters like are normalized to a lowercase letter + shift.
///
pub fn set_action(&mut self, modifiers: ModifierType, key: Key, action: Action) {
let (key, modifiers) = Self::normalize_input(key, modifiers);
self.mappings.insert((modifiers, key), action);
}

fn normalize_input(mut key: Key, mut modifiers: ModifierType) -> (Key, ModifierType) {
// If we get something considered an "upper" key, that means shift is being held. This is
// not just for A -> S-a, but also for + -> = (though the + is not transformed).
if key.is_upper() {
key = key.to_lower();
modifiers.insert(ModifierType::SHIFT_MASK);
}

(key, modifiers)
}
}
Loading

0 comments on commit e8e6070

Please sign in to comment.