diff --git a/README.md b/README.md index 20d39a1..f71c898 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ USAGE: ttyper [FLAGS] [OPTIONS] [contents] FLAGS: - -d, --debug + -d, --debug -h, --help Prints help information --list-languages List installed languages --no-backtrack Disable backtracking to completed words @@ -160,6 +160,41 @@ results_chart_y = "gray;italic" # restart/quit prompt in results ui results_restart_prompt = "gray;italic" + +[key_map] + +# key map for removing previous word +remove_previous_word = "C-Backspace" + +# key map for removing previous character +remove_previous_char = "Backspace" + +# key map for space/next word +next_word = "Space" +``` + +### Key Maps + +In this config file, you can define key maps to customize your experience. Key maps allow you to associate specific actions with keyboard inputs. + +Key Map Structure: + +- Single characters are allowed only when accompanied by a modifier. +- Certain special keys, like `Backspace`, are allowed both by themselves and with a modifier. + +Some examples: +```toml +[key_map] + +# This reperesnts `Ctrl + Backspace` +remove_previous_word = "C-Backspace" + +# This reperesnts `Ctrl + h` +remove_previous_char = "C-h" + +# This reperesnts `Space` +next_word = "Space" + ``` ### style format diff --git a/src/config.rs b/src/config.rs index f399184..0b470a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,23 +4,17 @@ use serde::{ Deserialize, }; -#[derive(Debug, Deserialize)] +use crate::key::KeyMap; + +#[derive(Debug, Deserialize, Default, Clone)] #[serde(default)] pub struct Config { pub default_language: String, pub theme: Theme, + pub key_map: KeyMap, } -impl Default for Config { - fn default() -> Self { - Self { - default_language: "english200".into(), - theme: Theme::default(), - } - } -} - -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] #[serde(default)] pub struct Theme { #[serde(deserialize_with = "deserialize_style")] diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000..968c1e5 --- /dev/null +++ b/src/key.rs @@ -0,0 +1,144 @@ +use core::panic; + +use crossterm::event::{KeyCode, KeyModifiers}; +use serde::{de, Deserialize}; + +#[derive(Debug, Deserialize, Default, Clone)] +#[serde(default)] +pub struct KeyMap { + #[serde(deserialize_with = "deseralize_key")] + pub remove_previous_word: Key, + #[serde(deserialize_with = "deseralize_key")] + pub remove_previous_char: Key, + #[serde(deserialize_with = "deseralize_key")] + pub next_word: Key, +} + +fn deseralize_key<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + struct KeyVisitor; + impl<'de> de::Visitor<'de> for KeyVisitor { + type Value = Key; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("key specification") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match get_key_from_string(v) { + Some(key) => Ok(key), + None => { + panic!("Key map `{}` is invalid", v) + } + } + } + } + + return deserializer.deserialize_str(KeyVisitor); +} + +#[derive(Debug, Clone)] +pub struct Key { + pub code: KeyCode, + pub modifier: KeyModifiers, +} + +impl Default for Key { + fn default() -> Self { + Self { + code: KeyCode::Null, + modifier: KeyModifiers::NONE, + } + } +} + +fn get_key_code_from_string(string: &str) -> KeyCode { + if string.chars().count() == 1 { + let key_code_char = string.chars().next(); + if let Some(key_code_char) = key_code_char { + if key_code_char.is_lowercase() { + return KeyCode::Char(key_code_char); + } + } + } + match string { + "Backspace" => KeyCode::Backspace, + "Enter" => KeyCode::Enter, + "Left" => KeyCode::Left, + "Right" => KeyCode::Right, + "Up" => KeyCode::Up, + "Down" => KeyCode::Down, + "Home" => KeyCode::Home, + "End" => KeyCode::End, + "PageUp" => KeyCode::PageUp, + "PageDown" => KeyCode::PageDown, + "Tab" => KeyCode::Tab, + "BackTab" => KeyCode::BackTab, + "Delete" => KeyCode::Delete, + "Insert" => KeyCode::Insert, + "Esc" => KeyCode::Esc, + "CapsLock" => KeyCode::CapsLock, + "ScrollLock" => KeyCode::ScrollLock, + "NumLock" => KeyCode::NumLock, + "PrintScreen" => KeyCode::PrintScreen, + "Pause" => KeyCode::Pause, + "Menu" => KeyCode::Menu, + "KeypadBegin" => KeyCode::KeypadBegin, + _ => KeyCode::Null, + } +} + +fn get_key_modifier_from_string(string: &str) -> KeyModifiers { + match string { + "C" => KeyModifiers::CONTROL, + "A" => KeyModifiers::ALT, + "W" => KeyModifiers::SUPER, + "H" => KeyModifiers::HYPER, + "M" => KeyModifiers::META, + _ => KeyModifiers::NONE, + } +} + +fn get_key_from_string(string: &str) -> Option { + let mut key = Key { + code: KeyCode::Null, + modifier: KeyModifiers::NONE, + }; + match string.split('-').count() { + 1 => { + if string.chars().count() == 1 { + key.code = KeyCode::Null; + } else { + key.code = get_key_code_from_string(string); + } + } + 2 => { + let mut split = string.split('-'); + let key_code = split.next(); + if let Some(key_code) = key_code { + if key_code.chars().count() == 1 { + key.modifier = get_key_modifier_from_string(key_code); + } + } + if key.modifier != KeyModifiers::NONE { + let key_code = split.next(); + if let Some(key_code) = key_code { + key.code = get_key_code_from_string(key_code); + if key.code == KeyCode::Null { + key.modifier = KeyModifiers::NONE; + } + } + } + } + _ => {} + } + if key.modifier == KeyModifiers::NONE && key.code == KeyCode::Null { + return None; + } + Some(key) +} diff --git a/src/main.rs b/src/main.rs index bc5a3e5..a89384e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod key; mod test; mod ui; @@ -228,6 +229,7 @@ fn main() -> crossterm::Result<()> { "Couldn't get test contents. Make sure the specified language actually exists.", ), !opt.no_backtrack, + config.clone(), )); state.render_into(&mut terminal, &config)?; @@ -276,7 +278,8 @@ fn main() -> crossterm::Result<()> { opt.gen_contents().expect( "Couldn't get test contents. Make sure the specified language actually exists.", ), - !opt.no_backtrack + !opt.no_backtrack, + config.clone(), )); } Event::Key(KeyEvent { @@ -294,7 +297,8 @@ fn main() -> crossterm::Result<()> { .flat_map(|w| vec![w.clone(); 5]) .collect(); practice_words.shuffle(&mut thread_rng()); - state = State::Test(Test::new(practice_words, !opt.no_backtrack)); + state = + State::Test(Test::new(practice_words, !opt.no_backtrack, config.clone())); } Event::Key(KeyEvent { code: KeyCode::Char('q'), diff --git a/src/test/mod.rs b/src/test/mod.rs index 01295d9..6097968 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -4,6 +4,8 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use std::fmt; use std::time::Instant; +use crate::config::Config; + pub struct TestEvent { pub time: Instant, pub key: KeyEvent, @@ -48,15 +50,17 @@ pub struct Test { pub current_word: usize, pub complete: bool, pub backtracking_enabled: bool, + pub config: Config, } impl Test { - pub fn new(words: Vec, backtracking_enabled: bool) -> Self { + pub fn new(words: Vec, backtracking_enabled: bool, config: Config) -> Self { Self { words: words.into_iter().map(TestWord::from).collect(), current_word: 0, complete: false, backtracking_enabled, + config, } } @@ -66,6 +70,66 @@ impl Test { } let word = &mut self.words[self.current_word]; + + if key.code == self.config.key_map.next_word.code + && key + .modifiers + .contains(self.config.key_map.next_word.modifier) + { + if word.text.chars().nth(word.progress.len()) == Some(' ') { + word.progress.push(' '); + word.events.push(TestEvent { + time: Instant::now(), + correct: Some(true), + key, + }) + } else if !word.progress.is_empty() || word.text.is_empty() { + word.events.push(TestEvent { + time: Instant::now(), + correct: Some(word.text == word.progress), + key, + }); + self.next_word(); + } + return; + } + + if key.code == self.config.key_map.remove_previous_char.code + && key + .modifiers + .contains(self.config.key_map.remove_previous_char.modifier) + { + if word.progress.is_empty() && self.backtracking_enabled { + self.last_word(); + } else { + word.events.push(TestEvent { + time: Instant::now(), + correct: Some(!word.text.starts_with(&word.progress[..])), + key, + }); + word.progress.pop(); + } + return; + } + + if key.code == self.config.key_map.remove_previous_word.code + && key + .modifiers + .contains(self.config.key_map.remove_previous_word.modifier) + { + if self.words[self.current_word].progress.is_empty() { + self.last_word(); + } + let word = &mut self.words[self.current_word]; + word.events.push(TestEvent { + time: Instant::now(), + correct: None, + key, + }); + word.progress.clear(); + return; + } + match key.code { KeyCode::Char(' ') | KeyCode::Enter => { if word.text.chars().nth(word.progress.len()) == Some(' ') {