Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds configurable keymaps for navigation movements #77

Merged
merged 19 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
46105ca
Adds configurable keymaps for navigation movements
LukeHalasy Sep 29, 2023
9e209fe
Changes navigation keymaps to a hashmap instead of a struct. Custom d…
LukeHalasy Oct 2, 2023
736bc67
Changes configurable keymaps to take a list of chars instead of a sin…
LukeHalasy Oct 2, 2023
34d68ee
allows specifying both chars and KeyCode enum strings within the keycode
LukeHalasy Oct 4, 2023
d35b6a8
don't panic (todo!) when no match occurs with a specified navigation
LukeHalasy Oct 4, 2023
f0ad446
Correct defaults for KeyMaps
LukeHalasy Oct 5, 2023
8e467b2
adds context comment surrounding why we must detect characters before
LukeHalasy Oct 5, 2023
19e9967
fix lint issues
LukeHalasy Oct 5, 2023
69183f2
add moveup and movedown as configurable keycommand options for the
LukeHalasy Oct 5, 2023
24c4524
updates README documentation config test
LukeHalasy Oct 5, 2023
16a04c0
merge in recent changes
LukeHalasy Oct 5, 2023
7af7fad
fix linting issues
LukeHalasy Oct 5, 2023
73abe8a
delete commented out block
LukeHalasy Oct 5, 2023
4ee9411
Adds expecting block to the KeyMaps deserializer
LukeHalasy Oct 5, 2023
0dcdef3
add strum crate
LukeHalasy Oct 8, 2023
a8f279c
Match configurable GotoTop and GotoBottom actions in the branch list
LukeHalasy Oct 8, 2023
ad6e296
Use default keymappings for an action if a user didn't add a mapping for
LukeHalasy Oct 8, 2023
9f74a7f
Adds a unit-test to ensure that every Action has a default keybinding
LukeHalasy Oct 8, 2023
c2663d2
fix clippy warnings
LukeHalasy Oct 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ nom = "7.1.3"
paste = "1.0.14"
serde = { version = "1.0.168", features = [ "derive" ] }
serde_ignored = "0.1.9"
strum = { version = "0.25", features = ["derive"] }
toml = "0.8.1"
vte = "0.12.0"

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ addition = "#b8bb26"
deletion = "#fb4934"
key = "#d79921"
error = "#cc241d"

[keymap.navigation]
move_down = ['j', "Down"]
move_up = ['k', "Up"]
next_file = ['J']
previous_file = ['K']
toggle_expand = [" ", "Tab"]
goto_top = ['g']
goto_bottom = ['G']
```

## Versioning
Expand Down
154 changes: 150 additions & 4 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
//! Gex configuration.
#![allow(clippy::derivable_impls)]
use std::{fs, path::PathBuf, str::FromStr, sync::OnceLock};
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::OnceLock};

use anyhow::{Context, Result};
use clap::Parser;
use crossterm::style::Color;
use serde::Deserialize;
use clap::{command, Parser};
LukeHalasy marked this conversation as resolved.
Show resolved Hide resolved
use crossterm::{event::KeyCode, style::Color};
use serde::{
de::{self, Visitor},
Deserialize,
};

#[allow(unused_imports)]
use strum::{EnumIter, IntoEnumIterator};
Comment on lines +13 to +14
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're only using strum in the unit test, you can move it to dev-dependencies, and move this use statement into the test module


pub static CONFIG: OnceLock<Config> = OnceLock::new();
#[macro_export]
Expand Down Expand Up @@ -36,6 +42,7 @@ pub struct Clargs {
pub struct Config {
pub options: Options,
pub colors: Colors,
pub keymap: Keymaps,
}

#[derive(Deserialize, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -127,6 +134,108 @@ impl Default for Colors {
}
}

#[derive(Debug, PartialEq, Eq)]
pub struct Keymaps {
pub navigation: HashMap<KeyCode, Action>,
}

struct KeymapsVisitor;

impl<'de> Deserialize<'de> for Keymaps {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
impl<'de> Visitor<'de> for KeymapsVisitor {
type Value = Keymaps;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"
[keymap.SECTION]
action_under_section = ['<CHARACTER_VALUE>', \"<KeyCode enum value name>\"],
...
",
)
}

fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut navigation = Self::Value::default().navigation;

while let Some((section, section_values)) =
map.next_entry::<String, HashMap<String, Vec<String>>>()?
{
if section == "navigation" {
for (action, keys) in section_values {
let ac: Action = Deserialize::deserialize(
de::value::StringDeserializer::new(action),
)?;

// over-write default key-map to action
navigation.retain(|_, value| value != &ac);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a less jank way to do this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine to me, only happens once anyway!


for key in keys {
// cross-term can't, with Serde, directly deserialize '<CHARACTER_VALUE>' into a KeyCode
if key.len() == 1 {
if let Some(c) = key.chars().next() {
let key = KeyCode::Char(c);
navigation.insert(key, ac.clone());
continue;
}
}

let key: KeyCode = Deserialize::deserialize(
de::value::StringDeserializer::new(key),
)?;

navigation.insert(key, ac.clone());
}
}
}
}

Ok(Keymaps { navigation })
LukeHalasy marked this conversation as resolved.
Show resolved Hide resolved
}
}

deserializer.deserialize_map(KeymapsVisitor)
}
}

#[derive(Deserialize, Clone, Debug, PartialEq, Eq, EnumIter)]
#[serde(rename_all(deserialize = "snake_case"))]
pub enum Action {
MoveDown,
MoveUp,
NextFile,
PreviousFile,
ToggleExpand,
GotoTop,
GotoBottom,
}

impl Default for Keymaps {
fn default() -> Self {
Self {
navigation: HashMap::from([
(KeyCode::Char('j'), Action::MoveDown),
(KeyCode::Down, Action::MoveDown),
(KeyCode::Char('k'), Action::MoveUp),
(KeyCode::Up, Action::MoveUp),
(KeyCode::Char('J'), Action::NextFile),
(KeyCode::Char('K'), Action::PreviousFile),
(KeyCode::Char(' '), Action::ToggleExpand),
(KeyCode::Tab, Action::ToggleExpand),
(KeyCode::Char('g'), Action::GotoTop),
(KeyCode::Char('G'), Action::GotoBottom),
]),
LukeHalasy marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

impl Config {
/// Reads the config from the config file (usually `~/.config/gex/config.toml` on Linux) and
/// returns it along with a Vec of unrecognised keys.
Expand Down Expand Up @@ -236,6 +345,20 @@ mod tests {
use super::*;
use crossterm::style::Color;

#[test]
fn every_action_has_a_default_key() {
let mut action_list: Vec<Action> = Action::iter().collect();
for (_, action) in Keymaps::default().navigation {
action_list.retain(|x| x != &action);
}

assert!(
action_list.is_empty(),
"The following Actions do not have a default keybinding: {:?}",
action_list
)
}

// Should be up to date with the example config in the README.
#[test]
fn parse_readme_example() {
Expand All @@ -262,6 +385,15 @@ addition = \"#b8bb26\"
deletion = \"#fb4934\"
key = \"#d79921\"
error = \"#cc241d\"

[keymap.navigation]
move_down = [\'j\', \"Down\"]
move_up = [\'k\', \"Up\"]
next_file = [\'J\']
previous_file = [\'K\']
toggle_expand = [\" \", \"Tab\"]
goto_top = [\'g\']
goto_bottom = [\'G\']
";
assert_eq!(
toml::from_str(INPUT),
Expand All @@ -288,6 +420,20 @@ error = \"#cc241d\"
deletion: Color::from((251, 73, 52)),
key: Color::from((215, 153, 33)),
error: Color::from((204, 36, 29))
},
keymap: Keymaps {
navigation: HashMap::from([
(KeyCode::Char('j'), Action::MoveDown),
(KeyCode::Down, Action::MoveDown),
(KeyCode::Char('k'), Action::MoveUp),
(KeyCode::Up, Action::MoveUp),
(KeyCode::Char('J'), Action::NextFile),
(KeyCode::Char('K'), Action::PreviousFile),
(KeyCode::Char(' '), Action::ToggleExpand),
(KeyCode::Tab, Action::ToggleExpand),
(KeyCode::Char('g'), Action::GotoTop),
(KeyCode::Char('G'), Action::GotoBottom),
]),
}
})
)
Expand Down
Loading
Loading