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

Fix duplicate key issue #585

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ target/**
# End of https://www.toptal.com/developers/gitignore/api/rust

tests/data

.vscode/
1 change: 1 addition & 0 deletions docs/configuration/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ uda.taskwarrior-tui.keyconfig.start-stop=s
uda.taskwarrior-tui.keyconfig.quick-tag=t
uda.taskwarrior-tui.keyconfig.undo=u
uda.taskwarrior-tui.keyconfig.edit=e
uda.taskwarrior-tui.keyconfig.duplicate=y
uda.taskwarrior-tui.keyconfig.modify=m
uda.taskwarrior-tui.keyconfig.shell=!
uda.taskwarrior-tui.keyconfig.log=l
Expand Down
261 changes: 163 additions & 98 deletions src/keyconfig.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::{collections::HashSet, error::Error, hash::Hash};

use anyhow::{anyhow, Result};
use log::{error, info, warn};
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};

use crate::event::KeyCode;

static KEYCONFIG_PREFIX: &str = "uda.taskwarrior-tui.keyconfig";

#[derive(Serialize, Deserialize, Debug)]
pub struct KeyConfig {
pub quit: KeyCode,
Expand Down Expand Up @@ -108,88 +110,62 @@ impl KeyConfig {
Ok(kc)
}

// Set key to value in config file, if config file contains it
fn update_key_code(key: &mut KeyCode, key_name: &str, config_file: &str) {
let config_name = format!("{KEYCONFIG_PREFIX}.{key_name}");
let key_from_config = Self::get_config(&config_name, config_file);

if let Some(new_key) = key_from_config {
trace!("Updated action {} to new key {:?}", key_name, new_key);
*key = new_key;
}
}

pub fn update(&mut self, data: &str) -> Result<()> {
let quit = Self::get_config("uda.taskwarrior-tui.keyconfig.quit", data);
let refresh = Self::get_config("uda.taskwarrior-tui.keyconfig.refresh", data);
let go_to_bottom = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-bottom", data);
let go_to_top = Self::get_config("uda.taskwarrior-tui.keyconfig.go-to-top", data);
let down = Self::get_config("uda.taskwarrior-tui.keyconfig.down", data);
let up = Self::get_config("uda.taskwarrior-tui.keyconfig.up", data);
let page_down = Self::get_config("uda.taskwarrior-tui.keyconfig.page-down", data);
let page_up = Self::get_config("uda.taskwarrior-tui.keyconfig.page-up", data);
let delete = Self::get_config("uda.taskwarrior-tui.keyconfig.delete", data);
let done = Self::get_config("uda.taskwarrior-tui.keyconfig.done", data);
let start_stop = Self::get_config("uda.taskwarrior-tui.keyconfig.start-stop", data);
let quick_tag = Self::get_config("uda.taskwarrior-tui.keyconfig.quick-tag", data);
let select = Self::get_config("uda.taskwarrior-tui.keyconfig.select", data);
let select_all = Self::get_config("uda.taskwarrior-tui.keyconfig.select-all", data);
let undo = Self::get_config("uda.taskwarrior-tui.keyconfig.undo", data);
let edit = Self::get_config("uda.taskwarrior-tui.keyconfig.edit", data);
let duplicate = Self::get_config("uda.taskwarrior-tui.keyconfig.duplicate", data);
let modify = Self::get_config("uda.taskwarrior-tui.keyconfig.modify", data);
let shell = Self::get_config("uda.taskwarrior-tui.keyconfig.shell", data);
let log = Self::get_config("uda.taskwarrior-tui.keyconfig.log", data);
let add = Self::get_config("uda.taskwarrior-tui.keyconfig.add", data);
let annotate = Self::get_config("uda.taskwarrior-tui.keyconfig.annotate", data);
let filter = Self::get_config("uda.taskwarrior-tui.keyconfig.filter", data);
let zoom = Self::get_config("uda.taskwarrior-tui.keyconfig.zoom", data);
let context_menu = Self::get_config("uda.taskwarrior-tui.keyconfig.context-menu", data);
let next_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.next-tab", data);
let previous_tab = Self::get_config("uda.taskwarrior-tui.keyconfig.previous-tab", data);
let shortcut0 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut0", data);
let shortcut1 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut1", data);
let shortcut2 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut2", data);
let shortcut3 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut3", data);
let shortcut4 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut4", data);
let shortcut5 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut5", data);
let shortcut6 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut6", data);
let shortcut7 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut7", data);
let shortcut8 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut8", data);
let shortcut9 = Self::get_config("uda.taskwarrior-tui.keyconfig.shortcut9", data);

self.quit = quit.unwrap_or(self.quit);
self.refresh = refresh.unwrap_or(self.refresh);
self.go_to_bottom = go_to_bottom.unwrap_or(self.go_to_bottom);
self.go_to_top = go_to_top.unwrap_or(self.go_to_top);
self.down = down.unwrap_or(self.down);
self.up = up.unwrap_or(self.up);
self.page_down = page_down.unwrap_or(self.page_down);
self.page_up = page_up.unwrap_or(self.page_up);
self.delete = delete.unwrap_or(self.delete);
self.done = done.unwrap_or(self.done);
self.start_stop = start_stop.unwrap_or(self.start_stop);
self.quick_tag = quick_tag.unwrap_or(self.quick_tag);
self.select = select.unwrap_or(self.select);
self.select_all = select_all.unwrap_or(self.select_all);
self.undo = undo.unwrap_or(self.undo);
self.edit = edit.unwrap_or(self.edit);
self.duplicate = edit.unwrap_or(self.duplicate);
self.modify = modify.unwrap_or(self.modify);
self.shell = shell.unwrap_or(self.shell);
self.log = log.unwrap_or(self.log);
self.add = add.unwrap_or(self.add);
self.annotate = annotate.unwrap_or(self.annotate);
self.filter = filter.unwrap_or(self.filter);
self.zoom = zoom.unwrap_or(self.zoom);
self.context_menu = context_menu.unwrap_or(self.context_menu);
self.next_tab = next_tab.unwrap_or(self.next_tab);
self.previous_tab = previous_tab.unwrap_or(self.previous_tab);
self.shortcut0 = shortcut0.unwrap_or(self.shortcut0);
self.shortcut1 = shortcut1.unwrap_or(self.shortcut1);
self.shortcut2 = shortcut2.unwrap_or(self.shortcut2);
self.shortcut3 = shortcut3.unwrap_or(self.shortcut3);
self.shortcut4 = shortcut4.unwrap_or(self.shortcut4);
self.shortcut5 = shortcut5.unwrap_or(self.shortcut5);
self.shortcut6 = shortcut6.unwrap_or(self.shortcut6);
self.shortcut7 = shortcut7.unwrap_or(self.shortcut7);
self.shortcut8 = shortcut8.unwrap_or(self.shortcut8);
self.shortcut9 = shortcut9.unwrap_or(self.shortcut9);

self.check()
Self::update_key_code(&mut self.quit, "quit", data);
Self::update_key_code(&mut self.refresh, "refresh", data);
Self::update_key_code(&mut self.go_to_bottom, "go-to-bottom", data);
Self::update_key_code(&mut self.go_to_top, "go-to-top", data);
Self::update_key_code(&mut self.down, "down", data);
Self::update_key_code(&mut self.up, "up", data);
Self::update_key_code(&mut self.page_down, "page-down", data);
Self::update_key_code(&mut self.page_up, "page-up", data);
Self::update_key_code(&mut self.delete, "delete", data);
Self::update_key_code(&mut self.done, "done", data);
Self::update_key_code(&mut self.start_stop, "start-stop", data);
Self::update_key_code(&mut self.quick_tag, "quick-tag", data);
Self::update_key_code(&mut self.select, "select", data);
Self::update_key_code(&mut self.select_all, "select-all", data);
Self::update_key_code(&mut self.undo, "undo", data);
Self::update_key_code(&mut self.edit, "edit", data);
Self::update_key_code(&mut self.duplicate, "duplicate", data);
Self::update_key_code(&mut self.modify, "modify", data);
Self::update_key_code(&mut self.shell, "shell", data);
Self::update_key_code(&mut self.log, "log", data);
Self::update_key_code(&mut self.add, "add", data);
Self::update_key_code(&mut self.annotate, "annotate", data);
Self::update_key_code(&mut self.filter, "filter", data);
Self::update_key_code(&mut self.zoom, "zoom", data);
Self::update_key_code(&mut self.context_menu, "context-menu", data);
Self::update_key_code(&mut self.next_tab, "next-tab", data);
Self::update_key_code(&mut self.previous_tab, "previous-tab", data);
Self::update_key_code(&mut self.shortcut0, "shortcut0", data);
Self::update_key_code(&mut self.shortcut1, "shortcut1", data);
Self::update_key_code(&mut self.shortcut2, "shortcut2", data);
Self::update_key_code(&mut self.shortcut3, "shortcut3", data);
Self::update_key_code(&mut self.shortcut4, "shortcut4", data);
Self::update_key_code(&mut self.shortcut5, "shortcut5", data);
Self::update_key_code(&mut self.shortcut6, "shortcut6", data);
Self::update_key_code(&mut self.shortcut7, "shortcut7", data);
Self::update_key_code(&mut self.shortcut8, "shortcut8", data);
Self::update_key_code(&mut self.shortcut9, "shortcut9", data);

let keys_to_check = self.keycodes_for_duplicate_check();
self.check_duplicates(keys_to_check)
}

pub fn check(&self) -> Result<()> {
let mut elements = vec![
fn keycodes_for_duplicate_check(&self) -> Vec<&KeyCode> {
vec![
&self.quit,
&self.refresh,
&self.go_to_bottom,
Expand Down Expand Up @@ -218,9 +194,22 @@ impl KeyConfig {
&self.context_menu,
&self.next_tab,
&self.previous_tab,
];
]
}

pub fn check_duplicates(&self, mut elements: Vec<&KeyCode>) -> Result<()> {
let l = elements.len();

// Vecs need to be sorted for dedup to work correctly.
// TODO: Replace once Key-Handling has been rewritten from ground up and Non-Char KeyCodes are supported.
elements.sort_by(|a, b| match (a, b) {
(KeyCode::Char(a_char), KeyCode::Char(b_char)) => a_char.cmp(b_char),
_ => {
panic!("Non-Char Keycodes are not yet supported. Found KeyCodes {:#?} and {:#?} to compare", a, b);
}
});
elements.dedup();

if l == elements.len() {
Ok(())
} else {
Expand All @@ -230,32 +219,108 @@ impl KeyConfig {

fn get_config(config: &str, data: &str) -> Option<KeyCode> {
for line in data.split('\n') {
if line.starts_with(config) {
let line = line.trim_start_matches(config).trim_start().trim_end().to_string();
if has_just_one_char(&line) {
return Some(KeyCode::Char(line.chars().next().unwrap()));
} else {
error!("Found multiple characters in {} for {}", line, config);
// Provide leeway for swapped - and _ in keyconfigs
let config_variants = vec![config.to_owned(), config.replace('-', "_")];

for config in &config_variants {
if !line.starts_with(config) {
continue;
}
} else if line.starts_with(&config.replace('-', "_")) {
let line = line.trim_start_matches(&config.replace('-', "_")).trim_start().trim_end().to_string();
if has_just_one_char(&line) {
return Some(KeyCode::Char(line.chars().next().unwrap()));
} else {
error!("Found multiple characters in {} for {}", line, config);

let trimmed_line = line
.trim_start_matches(config)
.trim_start()
.trim_start_matches('=')
.trim_end()
.to_string();

let chars: Vec<char> = trimmed_line.chars().collect();

match chars.len() {
0 => error!("Found no override key for action {} in line {}, only the config prefix", config, line),
1 => {
let key_char = chars.first();
match key_char {
Some(key_char) => return Some(KeyCode::Char(*key_char)),
None => error!("Encountered impossible error. Could not fetch first character in Vector of length 1"),
}
}
_ => error!(
"Found multiple characters({}) in {} for action {}, instead of the expected 1",
chars.len(),
line,
config
),
}
}
}

trace!("Could not find a key override for action {}", config);
None
}
}

fn has_just_one_char(s: &str) -> bool {
let mut chars = s.chars();
chars.next().is_some() && chars.next().is_none()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
#[should_panic]
fn test_invalid_key_variant_panic() {
let kc = KeyConfig::default();
let mut keys_to_check = kc.keycodes_for_duplicate_check();

// We need at least 2 keys for the compare logic to run
assert!(keys_to_check.len() >= 2);
*keys_to_check.first_mut().unwrap() = &KeyCode::Backspace;

// This line will panic
let _ = kc.check_duplicates(keys_to_check);
}

#[test]
fn test_duplicate_key_error() {
let kc = KeyConfig::default();
let mut keys_to_check = kc.keycodes_for_duplicate_check();

// Replace first and last with colliding key
// This way the duplicate check for non-consecutive keys is assured and correct sorting is tested
assert!(keys_to_check.len() >= 3);
*keys_to_check.first_mut().unwrap() = &KeyCode::Char('E');
*keys_to_check.last_mut().unwrap() = &KeyCode::Char('E');

let res = kc.check_duplicates(keys_to_check);
assert!(res.is_err())
}

#[test]
fn test_read_key_config() {
let config_prefix = "uda.taskwarrior-tui.keyconfig.quit";
let config_name = format!("{config_prefix}");

let valid_line = "uda.taskwarrior-tui.keyconfig.quit=q";
assert!(KeyConfig::get_config(&config_name, valid_line).is_some());

let invalid_line = "uda.taskwarrior-tui.keyconfig.quit=";
assert!(KeyConfig::get_config(&config_name, invalid_line).is_none());

let invalid_line = "uda.taskwarrior-tui.keyconfig.quit=qt";
assert!(KeyConfig::get_config(&config_name, invalid_line).is_none());
}

#[test]
fn test_update_key() {
let config = "uda.taskwarrior-tui.keyconfig.quit=M";
let target_keycode = KeyCode::Char('M');

// Check in case defaults changed
let default_keyconfig = KeyConfig::default();
let default_keycodes = KeyConfig::keycodes_for_duplicate_check(&default_keyconfig);
for keycode in default_keycodes {
assert_ne!(*keycode, target_keycode);
}

let kc = KeyConfig::new(&config).expect("Changing KeyConfig failed");
assert_eq!(kc.quit, target_keycode);
}
}
Loading