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

feat: extra keybindings override base #224

Merged
128 changes: 128 additions & 0 deletions src/keybindings/keybindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use serde::Deserialize;

use crate::opts::KeybindingsSection;

use super::{action::Action, KeyCombo};

/// A list of [`keybindings`](KeyCombo) each associated with an [`Action`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct Keybindings(Vec<(Action, KeyCombo)>);

impl Keybindings {
/// Returns an iterator over the [`Action`]s and [`KeyCombo`]s
pub fn iter(&self) -> std::slice::Iter<'_, (Action, KeyCombo)> {
self.0.iter()
}
}

impl Extend<(Action, KeyCombo)> for Keybindings {
fn extend<I: IntoIterator<Item = (Action, KeyCombo)>>(&mut self, iter: I) {
self.0.extend(iter)
}
}

impl IntoIterator for Keybindings {
type Item = (Action, KeyCombo);
type IntoIter = <Vec<(Action, KeyCombo)> as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl Default for Keybindings {
fn default() -> Self {
Self(super::defaults::defaults())
}
}

impl From<KeybindingsSection> for Keybindings {
/// Converts from [`KeybindingsSection`] to [`Keybindings`].
///
/// If an `extra` keybinding collides with a `base` one, then the `base` one is dropped in
/// favor of the `extra` keybinding
fn from(value: KeybindingsSection) -> Self {
let mut base = value.base;

if let Some(extra) = value.extra {
for (_, extra_combo) in extra.iter() {
base.0 = base
.clone()
.into_iter()
.filter(|(_, combo)| {
if combo.starts_with(extra_combo) {
tracing::debug!("Base keybinding {combo} ignored in favor of extra keybinding {extra_combo}");
false
} else {
true
}
})
.collect();
}

base.extend(extra)
}

base
}
}

#[cfg(test)]
mod tests {
use winit::event::ModifiersState;

use crate::keybindings::{action::VertDirection, Key, ModifiedKey};

use super::*;

#[test]
fn from_keybinding_section_base() {
assert_eq!(
Keybindings::from(KeybindingsSection {
base: Keybindings::default(),
extra: None
}),
Keybindings::default()
);
}

#[test]
fn from_keybinding_section_extra() {
let combo = KeyCombo(vec![ModifiedKey(
Key::Resolved(winit::event::VirtualKeyCode::A),
ModifiersState::empty(),
)]);

let mut expected = Keybindings::default();
expected.0.push((Action::Quit, combo.clone()));

assert_eq!(
Keybindings::from(KeybindingsSection {
base: Keybindings::default(),
extra: Some(Keybindings(vec![(Action::Quit, combo)]))
}),
expected
);
}

#[test]
fn from_keybinding_section_extra_override_base() {
let j_combo = KeyCombo(vec![ModifiedKey(
Key::Resolved(winit::event::VirtualKeyCode::J),
ModifiersState::empty(),
)]);

let base = Keybindings(vec![(Action::Scroll(VertDirection::Down), j_combo.clone())]);
let extra = Keybindings(vec![(Action::Page(VertDirection::Down), j_combo.clone())]);

let expected = Keybindings(vec![(Action::Page(VertDirection::Down), j_combo.clone())]);

assert_eq!(
Keybindings::from(KeybindingsSection {
base,
extra: Some(extra)
}),
expected
);
}
}
53 changes: 10 additions & 43 deletions src/keybindings/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod action;
mod defaults;
#[allow(clippy::module_inception)]
mod keybindings;
mod mappings;
mod serialization;
#[cfg(test)]
Expand All @@ -11,52 +13,19 @@ use std::slice::Iter;
use std::str::FromStr;
use std::vec;

use action::Action;

use serde::Deserialize;
use winit::event::{ModifiersState, ScanCode, VirtualKeyCode as VirtKey};

#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct Keybindings(Vec<(Action, KeyCombo)>);
use action::Action;
pub use keybindings::Keybindings;

use crate::opts::KeybindingsSection;

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Key {
Resolved(VirtKey),
ScanCode(ScanCode),
}

impl Keybindings {
pub fn new(bindings: Vec<(Action, KeyCombo)>) -> Self {
Self(bindings)
}

#[cfg(test)]
pub fn empty() -> Self {
Self(Vec::new())
}
}

impl Extend<(Action, KeyCombo)> for Keybindings {
fn extend<I: IntoIterator<Item = (Action, KeyCombo)>>(&mut self, iter: I) {
self.0.extend(iter)
}
}

impl IntoIterator for Keybindings {
type Item = (Action, KeyCombo);
type IntoIter = <Vec<(Action, KeyCombo)> as IntoIterator>::IntoIter;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

impl Default for Keybindings {
fn default() -> Self {
Self(defaults::defaults())
}
}

impl Key {
pub fn new(resolved: Option<VirtKey>, scan_code: ScanCode) -> Self {
match resolved {
Expand Down Expand Up @@ -231,16 +200,14 @@ pub struct KeyCombos {
}

impl KeyCombos {
pub fn new(keybinds: Keybindings) -> anyhow::Result<Self> {
let keybinds = keybinds.0;
pub fn new(keybinds: KeybindingsSection) -> anyhow::Result<Self> {
let keybinds: Keybindings = keybinds.into();
let position = ROOT_INDEX;

// A keycombo that starts with another keycombo will never be reachable since the prefixing
// combo will always be activated first
for i in 0..keybinds.len() {
for j in (i + 1)..keybinds.len() {
let combo1 = &keybinds[i].1;
let combo2 = &keybinds[j].1;
for (i, (_, combo1)) in keybinds.iter().enumerate() {
for (_, combo2) in keybinds.iter().skip(i + 1) {
if combo1.starts_with(combo2) {
anyhow::bail!(
"A keycombo starts with another keycombo making it unreachable\n\tCombo: \
Expand Down
5 changes: 1 addition & 4 deletions src/keybindings/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::action::{Action, VertDirection};
use super::{KeyCombos, ModifiedKey};
use crate::keybindings::Keybindings;
use crate::opts::Config;
use crate::test_utils::init_test_log;

Expand All @@ -22,9 +21,7 @@ base = [

// TODO: move this to a helper somewhere
let Config { keybindings, .. } = Config::load_from_str(config).unwrap();
let mut bindings = keybindings.base.unwrap_or_else(Keybindings::empty);
bindings.extend(keybindings.extra.unwrap_or_else(Keybindings::empty));
let mut key_combos = KeyCombos::new(bindings).unwrap();
let mut key_combos = KeyCombos::new(keybindings.into()).unwrap();

let g: ModifiedKey = VirtKey::G.into();
let l_shift = VirtKey::LShift.into();
Expand Down
5 changes: 3 additions & 2 deletions src/opts/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ impl Default for LinesToScroll {
}
}

#[derive(Deserialize, Debug, Default, PartialEq)]
#[derive(Deserialize, Clone, Debug, Default, PartialEq)]
pub struct KeybindingsSection {
pub base: Option<Keybindings>,
#[serde(default)]
pub base: Keybindings,
CosmicHorrorDev marked this conversation as resolved.
Show resolved Hide resolved
pub extra: Option<Keybindings>,
}

Expand Down
13 changes: 4 additions & 9 deletions src/opts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ mod tests;

use std::path::{Path, PathBuf};

pub use self::cli::{Args, ThemeType};
pub use self::config::{Config, FontOptions};
use crate::color;
use crate::keybindings::Keybindings;
pub use cli::{Args, ThemeType};
pub use config::{Config, FontOptions, KeybindingsSection};

use anyhow::Result;
use serde::Deserialize;
Expand Down Expand Up @@ -48,7 +47,7 @@ pub struct Opts {
pub page_width: Option<f32>,
pub lines_to_scroll: f32,
pub font_opts: FontOptions,
pub keybindings: Keybindings,
pub keybindings: KeybindingsSection,
}

impl Opts {
Expand Down Expand Up @@ -85,7 +84,7 @@ impl Opts {
light_theme,
dark_theme,
font_options,
keybindings: config_keybindings,
keybindings,
} = config;

let Args {
Expand Down Expand Up @@ -116,10 +115,6 @@ impl Opts {
let font_opts = font_options.unwrap_or_default();
let page_width = args_page_width.or(config_page_width);
let lines_to_scroll = lines_to_scroll.into();
let mut keybindings = config_keybindings.base.unwrap_or_default();
if let Some(extra) = config_keybindings.extra {
keybindings.extend(extra);
}

Ok(Self {
file_path,
Expand Down
6 changes: 2 additions & 4 deletions src/opts/tests/error_msg.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::keybindings::{KeyCombos, Keybindings};
use crate::keybindings::KeyCombos;
use crate::opts::Config;

macro_rules! snapshot_config_parse_error {
Expand Down Expand Up @@ -40,9 +40,7 @@ snapshot_config_parse_error!(

fn keycombo_conflict_from_config(s: &str) -> anyhow::Result<anyhow::Error> {
let Config { keybindings, .. } = Config::load_from_str(s)?;
let mut combined_keybindings = keybindings.base.unwrap_or_else(Keybindings::empty);
combined_keybindings.extend(keybindings.extra.unwrap_or_else(Keybindings::empty));
let err = KeyCombos::new(combined_keybindings).unwrap_err();
let err = KeyCombos::new(keybindings.into()).unwrap_err();
Ok(err)
}

Expand Down
3 changes: 1 addition & 2 deletions src/opts/tests/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use std::ffi::OsString;
use std::path::PathBuf;

use crate::color::{SyntaxTheme, Theme, ThemeDefaults};
use crate::keybindings::Keybindings;
use crate::opts::config::{self, FontOptions, LinesToScroll};
use crate::opts::{cli, Args, Opts, ResolvedTheme, ThemeType};
use crate::test_utils::init_test_log;
Expand All @@ -25,7 +24,7 @@ impl Opts {
page_width: None,
font_opts: FontOptions::default(),
lines_to_scroll: LinesToScroll::default().0,
keybindings: Keybindings::default(),
keybindings: Default::default(),
}
}
}
Expand Down