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
14 changes: 4 additions & 10 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@ use serde::{
Deserialize,
};

#[derive(Debug, Deserialize)]
use crate::key::KeyMap;

#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct Config {
pub default_language: String,
pub theme: Theme,
}

impl Default for Config {
fn default() -> Self {
Self {
default_language: "english200".into(),
theme: Theme::default(),
}
}
pub key_map: KeyMap,
}

#[derive(Debug, Deserialize)]
Expand Down
137 changes: 137 additions & 0 deletions src/key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use crossterm::event::{KeyCode, KeyModifiers};
use serde::{de, Deserialize};

#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct KeyMap {
#[serde(deserialize_with = "deseralize_key")]
pub remove_previous_word: Option<Key>,
Copy link
Owner

Choose a reason for hiding this comment

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

I think these should be named delete_word and delete_char. Ctrl-Backspace usually removes the current word.

Copy link
Author

@Praaa181203 Praaa181203 Jan 5, 2024

Choose a reason for hiding this comment

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

Usually Ctrl+Backspace, removes the word (part of the word if the cursor is between the word) before the cursor, that's why i chose it to name it remove_previous_word. In my opinion delete_word would somewhat be misleading, because delete_word indicates the its deleting the whole word, but instead it removes the word before the cursor.

Praaa181203 marked this conversation as resolved.
Show resolved Hide resolved
#[serde(deserialize_with = "deseralize_key")]
pub remove_previous_char: Option<Key>,
#[serde(deserialize_with = "deseralize_key")]
pub next_word: Option<Key>,
}

fn deseralize_key<'de, D>(deserializer: D) -> Result<Option<Key>, D::Error>
where
D: de::Deserializer<'de>,
{
struct KeyVisitor;
impl<'de> de::Visitor<'de> for KeyVisitor {
type Value = Option<Key>;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("")
Praaa181203 marked this conversation as resolved.
Show resolved Hide resolved
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(get_key_from_string(v))
}
}

return deserializer.deserialize_str(KeyVisitor);
}

#[derive(Debug)]
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.len() == 1 {
Praaa181203 marked this conversation as resolved.
Show resolved Hide resolved
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.len() == 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.len() == 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)
}
3 changes: 2 additions & 1 deletion 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 @@ -259,7 +260,7 @@ fn main() -> crossterm::Result<()> {
match state {
State::Test(ref mut test) => {
if let Event::Key(key) = event {
test.handle_key(key);
test.handle_key(&config, key);
Praaa181203 marked this conversation as resolved.
Show resolved Hide resolved
if test.complete {
state = State::Results(Results::from(&*test));
}
Expand Down
67 changes: 66 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 @@ -60,12 +62,75 @@ impl Test {
}
}

pub fn handle_key(&mut self, key: KeyEvent) {
pub fn handle_key(&mut self, config: &Config, key: KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}

let word = &mut self.words[self.current_word];

match &config.key_map.next_word {
Some(config_key) => {
if key.code == config_key.code && key.modifiers.contains(config_key.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;
}
}
None => {}
}

match &config.key_map.remove_previous_char {
Some(config_key) => {
if key.code == config_key.code && key.modifiers.contains(config_key.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;
}
}
None => {}
}

match &config.key_map.remove_previous_word {
Some(config_key) => {
if key.code == config_key.code && key.modifiers.contains(config_key.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;
}
}
None => {}
}

match key.code {
KeyCode::Char(' ') | KeyCode::Enter => {
if word.text.chars().nth(word.progress.len()) == Some(' ') {
Expand Down
Loading