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

Added key maps in config #104

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 5 additions & 11 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
144 changes: 144 additions & 0 deletions src/key.rs
Original file line number Diff line number Diff line change
@@ -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<Key, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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 {
Copy link
Owner

Choose a reason for hiding this comment

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

It would probably be good to support non-abbreviated modifiers for each of these also. For example, "Ctrl", "Alt", etc. Some people also call the Meta key "Super" or "Win".

Copy link
Author

Choose a reason for hiding this comment

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

I did not quite understand this?
Should I allow "Ctrl-Backspace" as an option for config?
And Meta and Super are seperate modifiers, so i dont quite get what you mean.

"C" => KeyModifiers::CONTROL,
"A" => KeyModifiers::ALT,
"W" => KeyModifiers::SUPER,
"H" => KeyModifiers::HYPER,
"M" => KeyModifiers::META,
_ => KeyModifiers::NONE,
Copy link
Owner

Choose a reason for hiding this comment

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

An unknown modifier should probably be an error, not a silent fallback to no modifier.

}
}

fn get_key_from_string(string: &str) -> Option<Key> {
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)
}
8 changes: 6 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod config;
mod key;
mod test;
mod ui;

Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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 {
Expand All @@ -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'),
Expand Down
66 changes: 65 additions & 1 deletion src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<String>, backtracking_enabled: bool) -> Self {
pub fn new(words: Vec<String>, backtracking_enabled: bool, config: Config) -> Self {
Self {
words: words.into_iter().map(TestWord::from).collect(),
current_word: 0,
complete: false,
backtracking_enabled,
config,
}
}

Expand All @@ -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(' ') {
Expand Down
Loading