Add more themes and rustfmt
This commit is contained in:
23
assets/themes/farout.toml
Normal file
23
assets/themes/farout.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "farout"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#0f0908"
|
||||||
|
fg = "#E0CCAE"
|
||||||
|
text_correct = "#a4896f"
|
||||||
|
text_incorrect = "#bf472c"
|
||||||
|
text_incorrect_bg = "#392D2B"
|
||||||
|
text_pending = "#A67458"
|
||||||
|
text_cursor_bg = "#0f0908"
|
||||||
|
text_cursor_fg = "#f2a766"
|
||||||
|
focused_key = "#f2a766"
|
||||||
|
accent = "#d47d49"
|
||||||
|
accent_dim = "#392D2B"
|
||||||
|
border = "#392D2B"
|
||||||
|
border_focused = "#d47d49"
|
||||||
|
header_bg = "#392D2B"
|
||||||
|
header_fg = "#E0CCAE"
|
||||||
|
bar_filled = "#a67458"
|
||||||
|
bar_empty = "#392D2B"
|
||||||
|
error = "#bf472c"
|
||||||
|
warning = "#f2a766"
|
||||||
|
success = "#a4896f"
|
||||||
23
assets/themes/gruvbox-darkest.toml
Normal file
23
assets/themes/gruvbox-darkest.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "gruvbox-darkest"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#121212"
|
||||||
|
fg = "#ebdbb2"
|
||||||
|
text_correct = "#b8bb26"
|
||||||
|
text_incorrect = "#fb4934"
|
||||||
|
text_incorrect_bg = "#462726"
|
||||||
|
text_pending = "#a89984"
|
||||||
|
text_cursor_bg = "#fabd2f"
|
||||||
|
text_cursor_fg = "#121212"
|
||||||
|
focused_key = "#fabd2f"
|
||||||
|
accent = "#83a598"
|
||||||
|
accent_dim = "#3c3836"
|
||||||
|
border = "#504945"
|
||||||
|
border_focused = "#83a598"
|
||||||
|
header_bg = "#3c3836"
|
||||||
|
header_fg = "#ebdbb2"
|
||||||
|
bar_filled = "#83a598"
|
||||||
|
bar_empty = "#3c3836"
|
||||||
|
error = "#fb4934"
|
||||||
|
warning = "#fabd2f"
|
||||||
|
success = "#b8bb26"
|
||||||
23
assets/themes/kanagawa-dragon.toml
Normal file
23
assets/themes/kanagawa-dragon.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-dragon"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#181616"
|
||||||
|
fg = "#c5c9c5"
|
||||||
|
text_correct = "#8a9a7b"
|
||||||
|
text_incorrect = "#c4746e"
|
||||||
|
text_incorrect_bg = "#43242B"
|
||||||
|
text_pending = "#a6a69c"
|
||||||
|
text_cursor_bg = "#2d4f67"
|
||||||
|
text_cursor_fg = "#c8c093"
|
||||||
|
focused_key = "#c4b28a"
|
||||||
|
accent = "#8ba4b0"
|
||||||
|
accent_dim = "#282727"
|
||||||
|
border = "#625e5a"
|
||||||
|
border_focused = "#8ba4b0"
|
||||||
|
header_bg = "#282727"
|
||||||
|
header_fg = "#c5c9c5"
|
||||||
|
bar_filled = "#8ea4a2"
|
||||||
|
bar_empty = "#282727"
|
||||||
|
error = "#c4746e"
|
||||||
|
warning = "#c4b28a"
|
||||||
|
success = "#8a9a7b"
|
||||||
23
assets/themes/kanagawa-lotus.toml
Normal file
23
assets/themes/kanagawa-lotus.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-lotus"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#f2ecbc"
|
||||||
|
fg = "#545464"
|
||||||
|
text_correct = "#6f894e"
|
||||||
|
text_incorrect = "#c84053"
|
||||||
|
text_incorrect_bg = "#d9a594"
|
||||||
|
text_pending = "#8a8980"
|
||||||
|
text_cursor_bg = "#5d57a3"
|
||||||
|
text_cursor_fg = "#f2ecbc"
|
||||||
|
focused_key = "#77713f"
|
||||||
|
accent = "#4d699b"
|
||||||
|
accent_dim = "#e7dba0"
|
||||||
|
border = "#a5a37d"
|
||||||
|
border_focused = "#4d699b"
|
||||||
|
header_bg = "#e7dba0"
|
||||||
|
header_fg = "#545464"
|
||||||
|
bar_filled = "#597b75"
|
||||||
|
bar_empty = "#d9d0a3"
|
||||||
|
error = "#c84053"
|
||||||
|
warning = "#77713f"
|
||||||
|
success = "#6f894e"
|
||||||
23
assets/themes/kanagawa-wave.toml
Normal file
23
assets/themes/kanagawa-wave.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "kanagawa-wave"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "#1f1f28"
|
||||||
|
fg = "#dcd7ba"
|
||||||
|
text_correct = "#76946a"
|
||||||
|
text_incorrect = "#c34043"
|
||||||
|
text_incorrect_bg = "#43242B"
|
||||||
|
text_pending = "#727169"
|
||||||
|
text_cursor_bg = "#2d4f67"
|
||||||
|
text_cursor_fg = "#c8c093"
|
||||||
|
focused_key = "#c0a36e"
|
||||||
|
accent = "#7e9cd8"
|
||||||
|
accent_dim = "#2A2A37"
|
||||||
|
border = "#54546D"
|
||||||
|
border_focused = "#7e9cd8"
|
||||||
|
header_bg = "#2A2A37"
|
||||||
|
header_fg = "#dcd7ba"
|
||||||
|
bar_filled = "#7e9cd8"
|
||||||
|
bar_empty = "#2A2A37"
|
||||||
|
error = "#c34043"
|
||||||
|
warning = "#c0a36e"
|
||||||
|
success = "#76946a"
|
||||||
23
assets/themes/terminal-default.toml
Normal file
23
assets/themes/terminal-default.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name = "terminal-default"
|
||||||
|
|
||||||
|
[colors]
|
||||||
|
bg = "reset"
|
||||||
|
fg = "reset"
|
||||||
|
text_correct = "green"
|
||||||
|
text_incorrect = "red"
|
||||||
|
text_incorrect_bg = "reset"
|
||||||
|
text_pending = "darkgray"
|
||||||
|
text_cursor_bg = "reset"
|
||||||
|
text_cursor_fg = "reset"
|
||||||
|
focused_key = "yellow"
|
||||||
|
accent = "blue"
|
||||||
|
accent_dim = "darkgray"
|
||||||
|
border = "darkgray"
|
||||||
|
border_focused = "blue"
|
||||||
|
header_bg = "reset"
|
||||||
|
header_fg = "reset"
|
||||||
|
bar_filled = "blue"
|
||||||
|
bar_empty = "darkgray"
|
||||||
|
error = "red"
|
||||||
|
warning = "yellow"
|
||||||
|
success = "green"
|
||||||
80
src/app.rs
80
src/app.rs
@@ -1,14 +1,15 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::engine::filter::CharFilter;
|
use crate::engine::filter::CharFilter;
|
||||||
use crate::engine::key_stats::KeyStatsStore;
|
use crate::engine::key_stats::KeyStatsStore;
|
||||||
use crate::engine::scoring;
|
use crate::engine::scoring;
|
||||||
use crate::engine::skill_tree::{BranchId, BranchStatus, DrillScope, SkillTree};
|
use crate::engine::skill_tree::{BranchId, BranchStatus, DrillScope, SkillTree};
|
||||||
|
use crate::generator::TextGenerator;
|
||||||
use crate::generator::capitalize;
|
use crate::generator::capitalize;
|
||||||
use crate::generator::code_patterns;
|
use crate::generator::code_patterns;
|
||||||
use crate::generator::code_syntax::CodeSyntaxGenerator;
|
use crate::generator::code_syntax::CodeSyntaxGenerator;
|
||||||
@@ -17,14 +18,13 @@ use crate::generator::numbers;
|
|||||||
use crate::generator::passage::PassageGenerator;
|
use crate::generator::passage::PassageGenerator;
|
||||||
use crate::generator::phonetic::PhoneticGenerator;
|
use crate::generator::phonetic::PhoneticGenerator;
|
||||||
use crate::generator::punctuate;
|
use crate::generator::punctuate;
|
||||||
use crate::generator::TextGenerator;
|
|
||||||
use crate::generator::transition_table::TransitionTable;
|
use crate::generator::transition_table::TransitionTable;
|
||||||
|
|
||||||
use crate::session::input::{self, KeystrokeEvent};
|
|
||||||
use crate::session::drill::DrillState;
|
use crate::session::drill::DrillState;
|
||||||
|
use crate::session::input::{self, KeystrokeEvent};
|
||||||
use crate::session::result::DrillResult;
|
use crate::session::result::DrillResult;
|
||||||
use crate::store::json_store::JsonStore;
|
use crate::store::json_store::JsonStore;
|
||||||
use crate::store::schema::{KeyStatsData, DrillHistoryData, ProfileData};
|
use crate::store::schema::{DrillHistoryData, KeyStatsData, ProfileData};
|
||||||
use crate::ui::components::menu::Menu;
|
use crate::ui::components::menu::Menu;
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
|
||||||
@@ -182,7 +182,8 @@ impl App {
|
|||||||
let focused = self.skill_tree.focused_key(scope, &self.key_stats);
|
let focused = self.skill_tree.focused_key(scope, &self.key_stats);
|
||||||
|
|
||||||
// Generate base lowercase text using only lowercase keys from scope
|
// Generate base lowercase text using only lowercase keys from scope
|
||||||
let lowercase_keys: Vec<char> = all_keys.iter()
|
let lowercase_keys: Vec<char> = all_keys
|
||||||
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|ch| ch.is_ascii_lowercase() || *ch == ' ')
|
.filter(|ch| ch.is_ascii_lowercase() || *ch == ' ')
|
||||||
.collect();
|
.collect();
|
||||||
@@ -196,7 +197,8 @@ impl App {
|
|||||||
let mut text = generator.generate(&filter, lowercase_focused, word_count);
|
let mut text = generator.generate(&filter, lowercase_focused, word_count);
|
||||||
|
|
||||||
// Apply capitalization if uppercase keys are in scope
|
// Apply capitalization if uppercase keys are in scope
|
||||||
let cap_keys: Vec<char> = all_keys.iter()
|
let cap_keys: Vec<char> = all_keys
|
||||||
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|ch| ch.is_ascii_uppercase())
|
.filter(|ch| ch.is_ascii_uppercase())
|
||||||
.collect();
|
.collect();
|
||||||
@@ -206,9 +208,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply punctuation if punctuation keys are in scope
|
// Apply punctuation if punctuation keys are in scope
|
||||||
let punct_keys: Vec<char> = all_keys.iter()
|
let punct_keys: Vec<char> = all_keys
|
||||||
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|ch| matches!(ch, '.' | ',' | '\'' | ';' | ':' | '"' | '-' | '?' | '!' | '(' | ')'))
|
.filter(|ch| {
|
||||||
|
matches!(
|
||||||
|
ch,
|
||||||
|
'.' | ',' | '\'' | ';' | ':' | '"' | '-' | '?' | '!' | '(' | ')'
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if !punct_keys.is_empty() {
|
if !punct_keys.is_empty() {
|
||||||
let mut rng = SmallRng::from_rng(&mut self.rng).unwrap();
|
let mut rng = SmallRng::from_rng(&mut self.rng).unwrap();
|
||||||
@@ -216,7 +224,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply numbers if digit keys are in scope
|
// Apply numbers if digit keys are in scope
|
||||||
let digit_keys: Vec<char> = all_keys.iter()
|
let digit_keys: Vec<char> = all_keys
|
||||||
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|ch| ch.is_ascii_digit())
|
.filter(|ch| ch.is_ascii_digit())
|
||||||
.collect();
|
.collect();
|
||||||
@@ -236,16 +245,44 @@ impl App {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
if code_active {
|
if code_active {
|
||||||
let symbol_keys: Vec<char> = all_keys.iter()
|
let symbol_keys: Vec<char> = all_keys
|
||||||
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|ch| matches!(ch,
|
.filter(|ch| {
|
||||||
'=' | '+' | '*' | '/' | '-' | '{' | '}' | '[' | ']' | '<' | '>' |
|
matches!(
|
||||||
'&' | '|' | '^' | '~' | '@' | '#' | '$' | '%' | '_' | '\\' | '`'
|
ch,
|
||||||
))
|
'=' | '+'
|
||||||
|
| '*'
|
||||||
|
| '/'
|
||||||
|
| '-'
|
||||||
|
| '{'
|
||||||
|
| '}'
|
||||||
|
| '['
|
||||||
|
| ']'
|
||||||
|
| '<'
|
||||||
|
| '>'
|
||||||
|
| '&'
|
||||||
|
| '|'
|
||||||
|
| '^'
|
||||||
|
| '~'
|
||||||
|
| '@'
|
||||||
|
| '#'
|
||||||
|
| '$'
|
||||||
|
| '%'
|
||||||
|
| '_'
|
||||||
|
| '\\'
|
||||||
|
| '`'
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if !symbol_keys.is_empty() {
|
if !symbol_keys.is_empty() {
|
||||||
let mut rng = SmallRng::from_rng(&mut self.rng).unwrap();
|
let mut rng = SmallRng::from_rng(&mut self.rng).unwrap();
|
||||||
text = code_patterns::apply_code_symbols(&text, &symbol_keys, focused, &mut rng);
|
text = code_patterns::apply_code_symbols(
|
||||||
|
&text,
|
||||||
|
&symbol_keys,
|
||||||
|
focused,
|
||||||
|
&mut rng,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +335,12 @@ impl App {
|
|||||||
fn finish_drill(&mut self) {
|
fn finish_drill(&mut self) {
|
||||||
if let Some(ref drill) = self.drill {
|
if let Some(ref drill) = self.drill {
|
||||||
let ranked = self.drill_mode.is_ranked();
|
let ranked = self.drill_mode.is_ranked();
|
||||||
let result = DrillResult::from_drill(drill, &self.drill_events, self.drill_mode.as_str(), ranked);
|
let result = DrillResult::from_drill(
|
||||||
|
drill,
|
||||||
|
&self.drill_events,
|
||||||
|
self.drill_mode.as_str(),
|
||||||
|
ranked,
|
||||||
|
);
|
||||||
|
|
||||||
if ranked {
|
if ranked {
|
||||||
for kt in &result.per_key_times {
|
for kt in &result.per_key_times {
|
||||||
@@ -329,8 +371,7 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
self.profile.streak_days = 1;
|
self.profile.streak_days = 1;
|
||||||
}
|
}
|
||||||
self.profile.best_streak =
|
self.profile.best_streak = self.profile.best_streak.max(self.profile.streak_days);
|
||||||
self.profile.best_streak.max(self.profile.streak_days);
|
|
||||||
self.profile.last_practice_date = Some(today);
|
self.profile.last_practice_date = Some(today);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,8 +488,7 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
self.profile.streak_days = 1;
|
self.profile.streak_days = 1;
|
||||||
}
|
}
|
||||||
self.profile.best_streak =
|
self.profile.best_streak = self.profile.best_streak.max(self.profile.streak_days);
|
||||||
self.profile.best_streak.max(self.profile.streak_days);
|
|
||||||
self.profile.last_practice_date = Some(day);
|
self.profile.last_practice_date = Some(day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ fn default_target_wpm() -> u32 {
|
|||||||
35
|
35
|
||||||
}
|
}
|
||||||
fn default_theme() -> String {
|
fn default_theme() -> String {
|
||||||
"catppuccin-mocha".to_string()
|
"terminal-default".to_string()
|
||||||
}
|
}
|
||||||
fn default_keyboard_layout() -> String {
|
fn default_keyboard_layout() -> String {
|
||||||
"qwerty".to_string()
|
"qwerty".to_string()
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ impl CharFilter {
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn filter_text(&self, text: &str) -> String {
|
pub fn filter_text(&self, text: &str) -> String {
|
||||||
text.chars()
|
text.chars().filter(|&ch| self.is_allowed(ch)).collect()
|
||||||
.filter(|&ch| self.is_allowed(ch))
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ impl KeyStatsStore {
|
|||||||
if stat.sample_count == 1 {
|
if stat.sample_count == 1 {
|
||||||
stat.filtered_time_ms = time_ms;
|
stat.filtered_time_ms = time_ms;
|
||||||
} else {
|
} else {
|
||||||
stat.filtered_time_ms =
|
stat.filtered_time_ms = EMA_ALPHA * time_ms + (1.0 - EMA_ALPHA) * stat.filtered_time_ms;
|
||||||
EMA_ALPHA * time_ms + (1.0 - EMA_ALPHA) * stat.filtered_time_ms;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stat.best_time_ms = stat.best_time_ms.min(stat.filtered_time_ms);
|
stat.best_time_ms = stat.best_time_ms.min(stat.filtered_time_ms);
|
||||||
@@ -64,10 +63,7 @@ impl KeyStatsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_confidence(&self, key: char) -> f64 {
|
pub fn get_confidence(&self, key: char) -> f64 {
|
||||||
self.stats
|
self.stats.get(&key).map(|s| s.confidence).unwrap_or(0.0)
|
||||||
.get(&key)
|
|
||||||
.map(|s| s.confidence)
|
|
||||||
.unwrap_or(0.0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -104,7 +100,10 @@ mod tests {
|
|||||||
let conf = store.get_confidence('t');
|
let conf = store.get_confidence('t');
|
||||||
// At 175 CPM target, target_time = 60000/175 = 342.8ms
|
// At 175 CPM target, target_time = 60000/175 = 342.8ms
|
||||||
// With 200ms typing time, confidence = 342.8/200 = 1.71
|
// With 200ms typing time, confidence = 342.8/200 = 1.71
|
||||||
assert!(conf > 1.0, "confidence should be > 1.0 for fast typing, got {conf}");
|
assert!(
|
||||||
|
conf > 1.0,
|
||||||
|
"confidence should be > 1.0 for fast typing, got {conf}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -115,6 +114,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let conf = store.get_confidence('a');
|
let conf = store.get_confidence('a');
|
||||||
// target_time = 342.8ms, typing at 1000ms -> conf = 342.8/1000 = 0.34
|
// target_time = 342.8ms, typing at 1000ms -> conf = 342.8/1000 = 0.34
|
||||||
assert!(conf < 1.0, "confidence should be < 1.0 for slow typing, got {conf}");
|
assert!(
|
||||||
|
conf < 1.0,
|
||||||
|
"confidence should be < 1.0 for slow typing, got {conf}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ pub struct BranchDefinition {
|
|||||||
const LOWERCASE_LEVELS: &[LevelDefinition] = &[LevelDefinition {
|
const LOWERCASE_LEVELS: &[LevelDefinition] = &[LevelDefinition {
|
||||||
name: "Frequency Order",
|
name: "Frequency Order",
|
||||||
keys: &[
|
keys: &[
|
||||||
'e', 't', 'a', 'o', 'i', 'n', 's', 'h', 'r', 'd', 'l', 'c', 'u', 'm', 'w', 'f', 'g',
|
'e', 't', 'a', 'o', 'i', 'n', 's', 'h', 'r', 'd', 'l', 'c', 'u', 'm', 'w', 'f', 'g', 'y',
|
||||||
'y', 'p', 'b', 'v', 'k', 'j', 'x', 'q', 'z',
|
'p', 'b', 'v', 'k', 'j', 'x', 'q', 'z',
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@@ -293,10 +293,7 @@ impl SkillTree {
|
|||||||
status: BranchStatus::Locked,
|
status: BranchStatus::Locked,
|
||||||
current_level: 0,
|
current_level: 0,
|
||||||
};
|
};
|
||||||
self.progress
|
self.progress.branches.get(id.to_key()).unwrap_or(&DEFAULT)
|
||||||
.branches
|
|
||||||
.get(id.to_key())
|
|
||||||
.unwrap_or(&DEFAULT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn branch_progress_mut(&mut self, id: BranchId) -> &mut BranchProgress {
|
pub fn branch_progress_mut(&mut self, id: BranchId) -> &mut BranchProgress {
|
||||||
@@ -663,8 +660,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_initial_state() {
|
fn test_initial_state() {
|
||||||
let tree = SkillTree::default();
|
let tree = SkillTree::default();
|
||||||
assert_eq!(*tree.branch_status(BranchId::Lowercase), BranchStatus::InProgress);
|
assert_eq!(
|
||||||
assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::Locked);
|
*tree.branch_status(BranchId::Lowercase),
|
||||||
|
BranchStatus::InProgress
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Capitals),
|
||||||
|
BranchStatus::Locked
|
||||||
|
);
|
||||||
assert_eq!(*tree.branch_status(BranchId::Numbers), BranchStatus::Locked);
|
assert_eq!(*tree.branch_status(BranchId::Numbers), BranchStatus::Locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,12 +714,30 @@ mod tests {
|
|||||||
tree.update(&stats);
|
tree.update(&stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(*tree.branch_status(BranchId::Lowercase), BranchStatus::Complete);
|
assert_eq!(
|
||||||
assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::Available);
|
*tree.branch_status(BranchId::Lowercase),
|
||||||
assert_eq!(*tree.branch_status(BranchId::Numbers), BranchStatus::Available);
|
BranchStatus::Complete
|
||||||
assert_eq!(*tree.branch_status(BranchId::ProsePunctuation), BranchStatus::Available);
|
);
|
||||||
assert_eq!(*tree.branch_status(BranchId::Whitespace), BranchStatus::Available);
|
assert_eq!(
|
||||||
assert_eq!(*tree.branch_status(BranchId::CodeSymbols), BranchStatus::Available);
|
*tree.branch_status(BranchId::Capitals),
|
||||||
|
BranchStatus::Available
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Numbers),
|
||||||
|
BranchStatus::Available
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::ProsePunctuation),
|
||||||
|
BranchStatus::Available
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Whitespace),
|
||||||
|
BranchStatus::Available
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::CodeSymbols),
|
||||||
|
BranchStatus::Available
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -726,7 +747,10 @@ mod tests {
|
|||||||
tree.branch_progress_mut(BranchId::Capitals).status = BranchStatus::Available;
|
tree.branch_progress_mut(BranchId::Capitals).status = BranchStatus::Available;
|
||||||
|
|
||||||
tree.start_branch(BranchId::Capitals);
|
tree.start_branch(BranchId::Capitals);
|
||||||
assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::InProgress);
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Capitals),
|
||||||
|
BranchStatus::InProgress
|
||||||
|
);
|
||||||
assert_eq!(tree.branch_progress(BranchId::Capitals).current_level, 0);
|
assert_eq!(tree.branch_progress(BranchId::Capitals).current_level, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +769,10 @@ mod tests {
|
|||||||
tree.update(&stats);
|
tree.update(&stats);
|
||||||
|
|
||||||
assert_eq!(tree.branch_progress(BranchId::Capitals).current_level, 1);
|
assert_eq!(tree.branch_progress(BranchId::Capitals).current_level, 1);
|
||||||
assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::InProgress);
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Capitals),
|
||||||
|
BranchStatus::InProgress
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -766,7 +793,10 @@ mod tests {
|
|||||||
tree.update(&stats);
|
tree.update(&stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::Complete);
|
assert_eq!(
|
||||||
|
*tree.branch_status(BranchId::Capitals),
|
||||||
|
BranchStatus::Complete
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ impl EventHandler {
|
|||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let _tx = tx.clone();
|
let _tx = tx.clone();
|
||||||
|
|
||||||
thread::spawn(move || loop {
|
thread::spawn(move || {
|
||||||
|
loop {
|
||||||
if event::poll(tick_rate).unwrap_or(false) {
|
if event::poll(tick_rate).unwrap_or(false) {
|
||||||
match event::read() {
|
match event::read() {
|
||||||
Ok(Event::Key(key)) => {
|
Ok(Event::Key(key)) => {
|
||||||
@@ -38,6 +39,7 @@ impl EventHandler {
|
|||||||
} else if tx.send(AppEvent::Tick).is_err() {
|
} else if tx.send(AppEvent::Tick).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { rx, _tx }
|
Self { rx, _tx }
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ impl DiskCache {
|
|||||||
|
|
||||||
fn sanitize_key(key: &str) -> String {
|
fn sanitize_key(key: &str) -> String {
|
||||||
key.chars()
|
key.chars()
|
||||||
.map(|c| if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' { c } else { '_' })
|
.map(|c| {
|
||||||
|
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
/// Post-processing pass that capitalizes words in generated text.
|
/// Post-processing pass that capitalizes words in generated text.
|
||||||
/// Only capitalizes using letters from `unlocked_capitals`.
|
/// Only capitalizes using letters from `unlocked_capitals`.
|
||||||
@@ -39,11 +39,16 @@ pub fn apply_capitalization(
|
|||||||
|
|
||||||
// Capitalize word starts: boosted for focused key, ~12% for others
|
// Capitalize word starts: boosted for focused key, ~12% for others
|
||||||
if ch.is_ascii_lowercase() && !at_sentence_start {
|
if ch.is_ascii_lowercase() && !at_sentence_start {
|
||||||
let is_word_start = i == 0 || text.as_bytes().get(i - 1).map(|&b| b as char) == Some(' ');
|
let is_word_start =
|
||||||
|
i == 0 || text.as_bytes().get(i - 1).map(|&b| b as char) == Some(' ');
|
||||||
if is_word_start {
|
if is_word_start {
|
||||||
let upper = ch.to_ascii_uppercase();
|
let upper = ch.to_ascii_uppercase();
|
||||||
if unlocked_capitals.contains(&upper) {
|
if unlocked_capitals.contains(&upper) {
|
||||||
let prob = if focused_upper == Some(upper) { 0.40 } else { 0.12 };
|
let prob = if focused_upper == Some(upper) {
|
||||||
|
0.40
|
||||||
|
} else {
|
||||||
|
0.12
|
||||||
|
};
|
||||||
if rng.gen_bool(prob) {
|
if rng.gen_bool(prob) {
|
||||||
result.push(upper);
|
result.push(upper);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
/// Post-processing pass that inserts code-like expressions into text.
|
/// Post-processing pass that inserts code-like expressions into text.
|
||||||
/// Only uses symbols from `unlocked_symbols`.
|
/// Only uses symbols from `unlocked_symbols`.
|
||||||
@@ -51,35 +51,47 @@ fn generate_code_expr(
|
|||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} = val")));
|
patterns.push(Box::new(move |_| format!("{w} = val")));
|
||||||
if focused_symbol == Some('=') { focused_patterns.push(idx); }
|
if focused_symbol == Some('=') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('+') {
|
if has('+') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} + num")));
|
patterns.push(Box::new(move |_| format!("{w} + num")));
|
||||||
if focused_symbol == Some('+') { focused_patterns.push(idx); }
|
if focused_symbol == Some('+') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('*') {
|
if has('*') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} * cnt")));
|
patterns.push(Box::new(move |_| format!("{w} * cnt")));
|
||||||
if focused_symbol == Some('*') { focused_patterns.push(idx); }
|
if focused_symbol == Some('*') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('/') {
|
if has('/') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} / max")));
|
patterns.push(Box::new(move |_| format!("{w} / max")));
|
||||||
if focused_symbol == Some('/') { focused_patterns.push(idx); }
|
if focused_symbol == Some('/') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('-') {
|
if has('-') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} - one")));
|
patterns.push(Box::new(move |_| format!("{w} - one")));
|
||||||
if focused_symbol == Some('-') { focused_patterns.push(idx); }
|
if focused_symbol == Some('-') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("-{w}")));
|
patterns.push(Box::new(move |_| format!("-{w}")));
|
||||||
if focused_symbol == Some('-') { focused_patterns.push(idx); }
|
if focused_symbol == Some('-') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('=') && has('+') {
|
if has('=') && has('+') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
@@ -89,7 +101,9 @@ fn generate_code_expr(
|
|||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} -= one")));
|
patterns.push(Box::new(move |_| format!("{w} -= one")));
|
||||||
if focused_symbol == Some('-') { focused_patterns.push(idx); }
|
if focused_symbol == Some('-') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('=') && has('=') {
|
if has('=') && has('=') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
@@ -101,19 +115,25 @@ fn generate_code_expr(
|
|||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{{ {w} }}")));
|
patterns.push(Box::new(move |_| format!("{{ {w} }}")));
|
||||||
if matches!(focused_symbol, Some('{') | Some('}')) { focused_patterns.push(idx); }
|
if matches!(focused_symbol, Some('{') | Some('}')) {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('[') && has(']') {
|
if has('[') && has(']') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w}[idx]")));
|
patterns.push(Box::new(move |_| format!("{w}[idx]")));
|
||||||
if matches!(focused_symbol, Some('[') | Some(']')) { focused_patterns.push(idx); }
|
if matches!(focused_symbol, Some('[') | Some(']')) {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('<') && has('>') {
|
if has('<') && has('>') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("Vec<{w}>")));
|
patterns.push(Box::new(move |_| format!("Vec<{w}>")));
|
||||||
if matches!(focused_symbol, Some('<') | Some('>')) { focused_patterns.push(idx); }
|
if matches!(focused_symbol, Some('<') | Some('>')) {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logic patterns
|
// Logic patterns
|
||||||
@@ -121,19 +141,25 @@ fn generate_code_expr(
|
|||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("&{w}")));
|
patterns.push(Box::new(move |_| format!("&{w}")));
|
||||||
if focused_symbol == Some('&') { focused_patterns.push(idx); }
|
if focused_symbol == Some('&') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('|') {
|
if has('|') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w} | nil")));
|
patterns.push(Box::new(move |_| format!("{w} | nil")));
|
||||||
if focused_symbol == Some('|') { focused_patterns.push(idx); }
|
if focused_symbol == Some('|') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('!') {
|
if has('!') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("!{w}")));
|
patterns.push(Box::new(move |_| format!("!{w}")));
|
||||||
if focused_symbol == Some('!') { focused_patterns.push(idx); }
|
if focused_symbol == Some('!') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special patterns
|
// Special patterns
|
||||||
@@ -141,31 +167,41 @@ fn generate_code_expr(
|
|||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("@{w}")));
|
patterns.push(Box::new(move |_| format!("@{w}")));
|
||||||
if focused_symbol == Some('@') { focused_patterns.push(idx); }
|
if focused_symbol == Some('@') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('#') {
|
if has('#') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("#{w}")));
|
patterns.push(Box::new(move |_| format!("#{w}")));
|
||||||
if focused_symbol == Some('#') { focused_patterns.push(idx); }
|
if focused_symbol == Some('#') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('_') {
|
if has('_') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("{w}_val")));
|
patterns.push(Box::new(move |_| format!("{w}_val")));
|
||||||
if focused_symbol == Some('_') { focused_patterns.push(idx); }
|
if focused_symbol == Some('_') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('$') {
|
if has('$') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("${w}")));
|
patterns.push(Box::new(move |_| format!("${w}")));
|
||||||
if focused_symbol == Some('$') { focused_patterns.push(idx); }
|
if focused_symbol == Some('$') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if has('\\') {
|
if has('\\') {
|
||||||
let w = word.to_string();
|
let w = word.to_string();
|
||||||
let idx = patterns.len();
|
let idx = patterns.len();
|
||||||
patterns.push(Box::new(move |_| format!("\\{w}")));
|
patterns.push(Box::new(move |_| format!("\\{w}")));
|
||||||
if focused_symbol == Some('\\') { focused_patterns.push(idx); }
|
if focused_symbol == Some('\\') {
|
||||||
|
focused_patterns.push(idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if patterns.is_empty() {
|
if patterns.is_empty() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
use crate::engine::filter::CharFilter;
|
use crate::engine::filter::CharFilter;
|
||||||
use crate::generator::cache::{DiskCache, fetch_url};
|
|
||||||
use crate::generator::TextGenerator;
|
use crate::generator::TextGenerator;
|
||||||
|
use crate::generator::cache::{DiskCache, fetch_url};
|
||||||
|
|
||||||
pub struct CodeSyntaxGenerator {
|
pub struct CodeSyntaxGenerator {
|
||||||
rng: SmallRng,
|
rng: SmallRng,
|
||||||
@@ -49,9 +49,7 @@ impl CodeSyntaxGenerator {
|
|||||||
"https://raw.githubusercontent.com/lodash/lodash/main/src/chunk.ts",
|
"https://raw.githubusercontent.com/lodash/lodash/main/src/chunk.ts",
|
||||||
"https://raw.githubusercontent.com/expressjs/express/master/lib/router/index.js",
|
"https://raw.githubusercontent.com/expressjs/express/master/lib/router/index.js",
|
||||||
],
|
],
|
||||||
"go" => vec![
|
"go" => vec!["https://raw.githubusercontent.com/golang/go/master/src/fmt/print.go"],
|
||||||
"https://raw.githubusercontent.com/golang/go/master/src/fmt/print.go",
|
|
||||||
],
|
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ impl Dictionary {
|
|||||||
self.words.clone()
|
self.words.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_matching(
|
pub fn find_matching(&self, filter: &CharFilter, focused: Option<char>) -> Vec<&str> {
|
||||||
&self,
|
|
||||||
filter: &CharFilter,
|
|
||||||
focused: Option<char>,
|
|
||||||
) -> Vec<&str> {
|
|
||||||
let mut matching: Vec<&str> = self
|
let mut matching: Vec<&str> = self
|
||||||
.words
|
.words
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
/// Post-processing pass that inserts number expressions into text.
|
/// Post-processing pass that inserts number expressions into text.
|
||||||
/// Only uses digits from `unlocked_digits`.
|
/// Only uses digits from `unlocked_digits`.
|
||||||
@@ -127,6 +127,9 @@ mod tests {
|
|||||||
let digits = ['1', '2', '3', '4', '5'];
|
let digits = ['1', '2', '3', '4', '5'];
|
||||||
let text = "a b c d e f g h i j k l m n o p q r s t";
|
let text = "a b c d e f g h i j k l m n o p q r s t";
|
||||||
let result = apply_numbers(text, &digits, false, None, &mut rng);
|
let result = apply_numbers(text, &digits, false, None, &mut rng);
|
||||||
assert!(!result.contains('.'), "Should not contain dot when has_dot=false: {result}");
|
assert!(
|
||||||
|
!result.contains('.'),
|
||||||
|
"Should not contain dot when has_dot=false: {result}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
use crate::engine::filter::CharFilter;
|
use crate::engine::filter::CharFilter;
|
||||||
use crate::generator::cache::{DiskCache, fetch_url};
|
|
||||||
use crate::generator::TextGenerator;
|
use crate::generator::TextGenerator;
|
||||||
|
use crate::generator::cache::{DiskCache, fetch_url};
|
||||||
|
|
||||||
const PASSAGES: &[&str] = &[
|
const PASSAGES: &[&str] = &[
|
||||||
// Classic literature & speeches
|
// Classic literature & speeches
|
||||||
@@ -217,7 +217,9 @@ fn extract_paragraphs(text: &str) -> Vec<String> {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.chars()
|
.chars()
|
||||||
.filter(|c| c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation())
|
.filter(|c| {
|
||||||
|
c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation()
|
||||||
|
})
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
use crate::engine::filter::CharFilter;
|
use crate::engine::filter::CharFilter;
|
||||||
|
use crate::generator::TextGenerator;
|
||||||
use crate::generator::dictionary::Dictionary;
|
use crate::generator::dictionary::Dictionary;
|
||||||
use crate::generator::transition_table::TransitionTable;
|
use crate::generator::transition_table::TransitionTable;
|
||||||
use crate::generator::TextGenerator;
|
|
||||||
|
|
||||||
const MIN_WORD_LEN: usize = 3;
|
const MIN_WORD_LEN: usize = 3;
|
||||||
const MAX_WORD_LEN: usize = 10;
|
const MAX_WORD_LEN: usize = 10;
|
||||||
@@ -149,7 +149,8 @@ impl PhoneticGenerator {
|
|||||||
if space_weight > 0.0 {
|
if space_weight > 0.0 {
|
||||||
let boost = 1.3f64.powi(word.len() as i32 - MIN_WORD_LEN as i32);
|
let boost = 1.3f64.powi(word.len() as i32 - MIN_WORD_LEN as i32);
|
||||||
let total: f64 = probs.iter().map(|(_, w)| w).sum();
|
let total: f64 = probs.iter().map(|(_, w)| w).sum();
|
||||||
let space_prob = (space_weight * boost) / (total + space_weight * (boost - 1.0));
|
let space_prob =
|
||||||
|
(space_weight * boost) / (total + space_weight * (boost - 1.0));
|
||||||
if self.rng.gen_bool(space_prob.min(0.85)) {
|
if self.rng.gen_bool(space_prob.min(0.85)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -164,11 +165,8 @@ impl PhoneticGenerator {
|
|||||||
|
|
||||||
// Get next character from transition table
|
// Get next character from transition table
|
||||||
if let Some(probs) = self.table.segment(&prefix) {
|
if let Some(probs) = self.table.segment(&prefix) {
|
||||||
let non_space: Vec<(char, f64)> = probs
|
let non_space: Vec<(char, f64)> =
|
||||||
.iter()
|
probs.iter().filter(|(ch, _)| *ch != ' ').copied().collect();
|
||||||
.filter(|(ch, _)| *ch != ' ')
|
|
||||||
.copied()
|
|
||||||
.collect();
|
|
||||||
if let Some(next) = Self::pick_weighted_from(&mut self.rng, &non_space, filter) {
|
if let Some(next) = Self::pick_weighted_from(&mut self.rng, &non_space, filter) {
|
||||||
word.push(next);
|
word.push(next);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
|
||||||
/// Post-processing pass that inserts punctuation into generated text.
|
/// Post-processing pass that inserts punctuation into generated text.
|
||||||
/// Only uses punctuation chars from `unlocked_punct`.
|
/// Only uses punctuation chars from `unlocked_punct`.
|
||||||
@@ -41,25 +41,41 @@ pub fn apply_punctuation(
|
|||||||
let mut w = word.to_string();
|
let mut w = word.to_string();
|
||||||
|
|
||||||
// Contractions (~8% of words, boosted if apostrophe is focused)
|
// Contractions (~8% of words, boosted if apostrophe is focused)
|
||||||
let apostrophe_prob = if focused_punct == Some('\'') { 0.30 } else { 0.08 };
|
let apostrophe_prob = if focused_punct == Some('\'') {
|
||||||
|
0.30
|
||||||
|
} else {
|
||||||
|
0.08
|
||||||
|
};
|
||||||
if has_apostrophe && w.len() >= 3 && rng.gen_bool(apostrophe_prob) {
|
if has_apostrophe && w.len() >= 3 && rng.gen_bool(apostrophe_prob) {
|
||||||
w = make_contraction(&w, rng);
|
w = make_contraction(&w, rng);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compound words with dash (~5% of words, boosted if dash is focused)
|
// Compound words with dash (~5% of words, boosted if dash is focused)
|
||||||
let dash_prob = if focused_punct == Some('-') { 0.25 } else { 0.05 };
|
let dash_prob = if focused_punct == Some('-') {
|
||||||
|
0.25
|
||||||
|
} else {
|
||||||
|
0.05
|
||||||
|
};
|
||||||
if has_dash && i + 1 < words.len() && rng.gen_bool(dash_prob) {
|
if has_dash && i + 1 < words.len() && rng.gen_bool(dash_prob) {
|
||||||
w.push('-');
|
w.push('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sentence ending punctuation
|
// Sentence ending punctuation
|
||||||
words_since_period += 1;
|
words_since_period += 1;
|
||||||
let end_sentence = words_since_period >= 8 && rng.gen_bool(0.15)
|
let end_sentence =
|
||||||
|| words_since_period >= 12;
|
words_since_period >= 8 && rng.gen_bool(0.15) || words_since_period >= 12;
|
||||||
|
|
||||||
if end_sentence && i < words.len() - 1 {
|
if end_sentence && i < words.len() - 1 {
|
||||||
let q_prob = if focused_punct == Some('?') { 0.40 } else { 0.15 };
|
let q_prob = if focused_punct == Some('?') {
|
||||||
let excl_prob = if focused_punct == Some('!') { 0.40 } else { 0.10 };
|
0.40
|
||||||
|
} else {
|
||||||
|
0.15
|
||||||
|
};
|
||||||
|
let excl_prob = if focused_punct == Some('!') {
|
||||||
|
0.40
|
||||||
|
} else {
|
||||||
|
0.10
|
||||||
|
};
|
||||||
if has_question && rng.gen_bool(q_prob) {
|
if has_question && rng.gen_bool(q_prob) {
|
||||||
w.push('?');
|
w.push('?');
|
||||||
} else if has_exclaim && rng.gen_bool(excl_prob) {
|
} else if has_exclaim && rng.gen_bool(excl_prob) {
|
||||||
@@ -72,34 +88,62 @@ pub fn apply_punctuation(
|
|||||||
} else {
|
} else {
|
||||||
// Comma after clause (~every 4-6 words)
|
// Comma after clause (~every 4-6 words)
|
||||||
words_since_comma += 1;
|
words_since_comma += 1;
|
||||||
let comma_prob = if focused_punct == Some(',') { 0.40 } else { 0.20 };
|
let comma_prob = if focused_punct == Some(',') {
|
||||||
if has_comma && words_since_comma >= 4 && rng.gen_bool(comma_prob) && i < words.len() - 1 {
|
0.40
|
||||||
|
} else {
|
||||||
|
0.20
|
||||||
|
};
|
||||||
|
if has_comma
|
||||||
|
&& words_since_comma >= 4
|
||||||
|
&& rng.gen_bool(comma_prob)
|
||||||
|
&& i < words.len() - 1
|
||||||
|
{
|
||||||
w.push(',');
|
w.push(',');
|
||||||
words_since_comma = 0;
|
words_since_comma = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semicolon between clauses (rare, boosted if focused)
|
// Semicolon between clauses (rare, boosted if focused)
|
||||||
let semi_prob = if focused_punct == Some(';') { 0.25 } else { 0.05 };
|
let semi_prob = if focused_punct == Some(';') {
|
||||||
if has_semicolon && words_since_comma >= 5 && rng.gen_bool(semi_prob) && i < words.len() - 1 {
|
0.25
|
||||||
|
} else {
|
||||||
|
0.05
|
||||||
|
};
|
||||||
|
if has_semicolon
|
||||||
|
&& words_since_comma >= 5
|
||||||
|
&& rng.gen_bool(semi_prob)
|
||||||
|
&& i < words.len() - 1
|
||||||
|
{
|
||||||
w.push(';');
|
w.push(';');
|
||||||
words_since_comma = 0;
|
words_since_comma = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colon before list-like content (rare, boosted if focused)
|
// Colon before list-like content (rare, boosted if focused)
|
||||||
let colon_prob = if focused_punct == Some(':') { 0.20 } else { 0.03 };
|
let colon_prob = if focused_punct == Some(':') {
|
||||||
|
0.20
|
||||||
|
} else {
|
||||||
|
0.03
|
||||||
|
};
|
||||||
if has_colon && rng.gen_bool(colon_prob) && i < words.len() - 1 {
|
if has_colon && rng.gen_bool(colon_prob) && i < words.len() - 1 {
|
||||||
w.push(':');
|
w.push(':');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quoted phrases (~5% chance to start a quote, boosted if focused)
|
// Quoted phrases (~5% chance to start a quote, boosted if focused)
|
||||||
let quote_prob = if focused_punct == Some('"') { 0.20 } else { 0.04 };
|
let quote_prob = if focused_punct == Some('"') {
|
||||||
|
0.20
|
||||||
|
} else {
|
||||||
|
0.04
|
||||||
|
};
|
||||||
if has_quote && rng.gen_bool(quote_prob) && i + 2 < words.len() {
|
if has_quote && rng.gen_bool(quote_prob) && i + 2 < words.len() {
|
||||||
w = format!("\"{w}");
|
w = format!("\"{w}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parenthetical asides (rare, boosted if focused)
|
// Parenthetical asides (rare, boosted if focused)
|
||||||
let paren_prob = if matches!(focused_punct, Some('(' | ')')) { 0.15 } else { 0.03 };
|
let paren_prob = if matches!(focused_punct, Some('(' | ')')) {
|
||||||
|
0.15
|
||||||
|
} else {
|
||||||
|
0.03
|
||||||
|
};
|
||||||
if has_open_paren && has_close_paren && rng.gen_bool(paren_prob) && i + 2 < words.len() {
|
if has_open_paren && has_close_paren && rng.gen_bool(paren_prob) && i + 2 < words.len() {
|
||||||
w = format!("({w}");
|
w = format!("({w}");
|
||||||
}
|
}
|
||||||
@@ -122,9 +166,15 @@ pub fn apply_punctuation(
|
|||||||
let mut open_parens = 0i32;
|
let mut open_parens = 0i32;
|
||||||
for w in &result {
|
for w in &result {
|
||||||
for ch in w.chars() {
|
for ch in w.chars() {
|
||||||
if ch == '"' { open_quotes += 1; }
|
if ch == '"' {
|
||||||
if ch == '(' { open_parens += 1; }
|
open_quotes += 1;
|
||||||
if ch == ')' { open_parens -= 1; }
|
}
|
||||||
|
if ch == '(' {
|
||||||
|
open_parens += 1;
|
||||||
|
}
|
||||||
|
if ch == ')' {
|
||||||
|
open_parens -= 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(last) = result.last_mut() {
|
if let Some(last) = result.last_mut() {
|
||||||
|
|||||||
@@ -108,24 +108,94 @@ impl TransitionTable {
|
|||||||
let mut table = Self::new(4);
|
let mut table = Self::new(4);
|
||||||
|
|
||||||
let common_patterns: &[(&str, f64)] = &[
|
let common_patterns: &[(&str, f64)] = &[
|
||||||
("the", 10.0), ("and", 8.0), ("ing", 7.0), ("tion", 6.0), ("ent", 5.0),
|
("the", 10.0),
|
||||||
("ion", 5.0), ("her", 4.0), ("for", 4.0), ("are", 4.0), ("his", 4.0),
|
("and", 8.0),
|
||||||
("hat", 3.0), ("tha", 3.0), ("ere", 3.0), ("ate", 3.0), ("ith", 3.0),
|
("ing", 7.0),
|
||||||
("ver", 3.0), ("all", 3.0), ("not", 3.0), ("ess", 3.0), ("est", 3.0),
|
("tion", 6.0),
|
||||||
("rea", 3.0), ("sta", 3.0), ("ted", 3.0), ("com", 3.0), ("con", 3.0),
|
("ent", 5.0),
|
||||||
("oun", 2.5), ("pro", 2.5), ("oth", 2.5), ("igh", 2.5), ("ore", 2.5),
|
("ion", 5.0),
|
||||||
("our", 2.5), ("ine", 2.5), ("ove", 2.5), ("ome", 2.5), ("use", 2.5),
|
("her", 4.0),
|
||||||
("ble", 2.0), ("ful", 2.0), ("ous", 2.0), ("str", 2.0), ("tri", 2.0),
|
("for", 4.0),
|
||||||
("ght", 2.0), ("whi", 2.0), ("who", 2.0), ("hen", 2.0), ("ter", 2.0),
|
("are", 4.0),
|
||||||
("man", 2.0), ("men", 2.0), ("ner", 2.0), ("per", 2.0), ("pre", 2.0),
|
("his", 4.0),
|
||||||
("ran", 2.0), ("lin", 2.0), ("kin", 2.0), ("din", 2.0), ("sin", 2.0),
|
("hat", 3.0),
|
||||||
("out", 2.0), ("ind", 2.0), ("ber", 2.0), ("der", 2.0),
|
("tha", 3.0),
|
||||||
("end", 2.0), ("hin", 2.0), ("old", 2.0), ("ear", 2.0), ("ain", 2.0),
|
("ere", 3.0),
|
||||||
("ant", 2.0), ("urn", 2.0), ("ell", 2.0), ("ill", 2.0), ("ade", 2.0),
|
("ate", 3.0),
|
||||||
("ong", 2.0), ("ung", 2.0), ("ast", 2.0), ("ist", 2.0),
|
("ith", 3.0),
|
||||||
("ust", 2.0), ("ost", 2.0), ("ard", 2.0), ("ord", 2.0), ("art", 2.0),
|
("ver", 3.0),
|
||||||
("ort", 2.0), ("ect", 2.0), ("act", 2.0), ("ack", 2.0), ("ick", 2.0),
|
("all", 3.0),
|
||||||
("ock", 2.0), ("uck", 2.0), ("ash", 2.0), ("ish", 2.0), ("ush", 2.0),
|
("not", 3.0),
|
||||||
|
("ess", 3.0),
|
||||||
|
("est", 3.0),
|
||||||
|
("rea", 3.0),
|
||||||
|
("sta", 3.0),
|
||||||
|
("ted", 3.0),
|
||||||
|
("com", 3.0),
|
||||||
|
("con", 3.0),
|
||||||
|
("oun", 2.5),
|
||||||
|
("pro", 2.5),
|
||||||
|
("oth", 2.5),
|
||||||
|
("igh", 2.5),
|
||||||
|
("ore", 2.5),
|
||||||
|
("our", 2.5),
|
||||||
|
("ine", 2.5),
|
||||||
|
("ove", 2.5),
|
||||||
|
("ome", 2.5),
|
||||||
|
("use", 2.5),
|
||||||
|
("ble", 2.0),
|
||||||
|
("ful", 2.0),
|
||||||
|
("ous", 2.0),
|
||||||
|
("str", 2.0),
|
||||||
|
("tri", 2.0),
|
||||||
|
("ght", 2.0),
|
||||||
|
("whi", 2.0),
|
||||||
|
("who", 2.0),
|
||||||
|
("hen", 2.0),
|
||||||
|
("ter", 2.0),
|
||||||
|
("man", 2.0),
|
||||||
|
("men", 2.0),
|
||||||
|
("ner", 2.0),
|
||||||
|
("per", 2.0),
|
||||||
|
("pre", 2.0),
|
||||||
|
("ran", 2.0),
|
||||||
|
("lin", 2.0),
|
||||||
|
("kin", 2.0),
|
||||||
|
("din", 2.0),
|
||||||
|
("sin", 2.0),
|
||||||
|
("out", 2.0),
|
||||||
|
("ind", 2.0),
|
||||||
|
("ber", 2.0),
|
||||||
|
("der", 2.0),
|
||||||
|
("end", 2.0),
|
||||||
|
("hin", 2.0),
|
||||||
|
("old", 2.0),
|
||||||
|
("ear", 2.0),
|
||||||
|
("ain", 2.0),
|
||||||
|
("ant", 2.0),
|
||||||
|
("urn", 2.0),
|
||||||
|
("ell", 2.0),
|
||||||
|
("ill", 2.0),
|
||||||
|
("ade", 2.0),
|
||||||
|
("ong", 2.0),
|
||||||
|
("ung", 2.0),
|
||||||
|
("ast", 2.0),
|
||||||
|
("ist", 2.0),
|
||||||
|
("ust", 2.0),
|
||||||
|
("ost", 2.0),
|
||||||
|
("ard", 2.0),
|
||||||
|
("ord", 2.0),
|
||||||
|
("art", 2.0),
|
||||||
|
("ort", 2.0),
|
||||||
|
("ect", 2.0),
|
||||||
|
("act", 2.0),
|
||||||
|
("ack", 2.0),
|
||||||
|
("ick", 2.0),
|
||||||
|
("ock", 2.0),
|
||||||
|
("uck", 2.0),
|
||||||
|
("ash", 2.0),
|
||||||
|
("ish", 2.0),
|
||||||
|
("ush", 2.0),
|
||||||
];
|
];
|
||||||
|
|
||||||
for &(pattern, weight) in common_patterns {
|
for &(pattern, weight) in common_patterns {
|
||||||
@@ -142,8 +212,8 @@ impl TransitionTable {
|
|||||||
|
|
||||||
let vowels = ['a', 'e', 'i', 'o', 'u'];
|
let vowels = ['a', 'e', 'i', 'o', 'u'];
|
||||||
let consonants = [
|
let consonants = [
|
||||||
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v',
|
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w',
|
||||||
'w', 'x', 'y', 'z',
|
'x', 'y', 'z',
|
||||||
];
|
];
|
||||||
|
|
||||||
for &c in &consonants {
|
for &c in &consonants {
|
||||||
|
|||||||
81
src/main.rs
81
src/main.rs
@@ -21,28 +21,32 @@ use crossterm::execute;
|
|||||||
use crossterm::terminal::{
|
use crossterm::terminal::{
|
||||||
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||||
};
|
};
|
||||||
|
use ratatui::Terminal;
|
||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::layout::{Constraint, Direction, Layout};
|
use ratatui::layout::{Constraint, Direction, Layout};
|
||||||
use ratatui::style::{Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Paragraph, Widget};
|
use ratatui::widgets::{Block, Paragraph, Widget};
|
||||||
use ratatui::Terminal;
|
|
||||||
|
|
||||||
use app::{App, AppScreen, DrillMode};
|
use app::{App, AppScreen, DrillMode};
|
||||||
use engine::skill_tree::DrillScope;
|
use engine::skill_tree::DrillScope;
|
||||||
use session::result::DrillResult;
|
|
||||||
use ui::components::skill_tree::{SkillTreeWidget, selectable_branches};
|
|
||||||
use event::{AppEvent, EventHandler};
|
use event::{AppEvent, EventHandler};
|
||||||
|
use session::result::DrillResult;
|
||||||
use ui::components::dashboard::Dashboard;
|
use ui::components::dashboard::Dashboard;
|
||||||
use ui::components::keyboard_diagram::KeyboardDiagram;
|
use ui::components::keyboard_diagram::KeyboardDiagram;
|
||||||
use ui::components::progress_bar::ProgressBar;
|
use ui::components::progress_bar::ProgressBar;
|
||||||
|
use ui::components::skill_tree::{SkillTreeWidget, selectable_branches};
|
||||||
use ui::components::stats_dashboard::StatsDashboard;
|
use ui::components::stats_dashboard::StatsDashboard;
|
||||||
use ui::components::stats_sidebar::StatsSidebar;
|
use ui::components::stats_sidebar::StatsSidebar;
|
||||||
use ui::components::typing_area::TypingArea;
|
use ui::components::typing_area::TypingArea;
|
||||||
use ui::layout::AppLayout;
|
use ui::layout::AppLayout;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "keydr", version, about = "Terminal typing tutor with adaptive learning")]
|
#[command(
|
||||||
|
name = "keydr",
|
||||||
|
version,
|
||||||
|
about = "Terminal typing tutor with adaptive learning"
|
||||||
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(short, long, help = "Theme name")]
|
#[arg(short, long, help = "Theme name")]
|
||||||
theme: Option<String>,
|
theme: Option<String>,
|
||||||
@@ -237,7 +241,12 @@ fn handle_drill_key(app: &mut App, key: KeyEvent) {
|
|||||||
if has_progress && app.drill_mode != DrillMode::Adaptive {
|
if has_progress && app.drill_mode != DrillMode::Adaptive {
|
||||||
// Non-adaptive: show result screen for partial drill
|
// Non-adaptive: show result screen for partial drill
|
||||||
if let Some(ref drill) = app.drill {
|
if let Some(ref drill) = app.drill {
|
||||||
let result = DrillResult::from_drill(drill, &app.drill_events, app.drill_mode.as_str(), app.drill_mode.is_ranked());
|
let result = DrillResult::from_drill(
|
||||||
|
drill,
|
||||||
|
&app.drill_events,
|
||||||
|
app.drill_mode.as_str(),
|
||||||
|
app.drill_mode.is_ranked(),
|
||||||
|
);
|
||||||
app.last_result = Some(result);
|
app.last_result = Some(result);
|
||||||
}
|
}
|
||||||
app.screen = AppScreen::DrillResult;
|
app.screen = AppScreen::DrillResult;
|
||||||
@@ -283,8 +292,7 @@ fn handle_stats_key(app: &mut App, key: KeyEvent) {
|
|||||||
KeyCode::Char('j') | KeyCode::Down => {
|
KeyCode::Char('j') | KeyCode::Down => {
|
||||||
if !app.drill_history.is_empty() {
|
if !app.drill_history.is_empty() {
|
||||||
let max_visible = app.drill_history.len().min(20) - 1;
|
let max_visible = app.drill_history.len().min(20) - 1;
|
||||||
app.history_selected =
|
app.history_selected = (app.history_selected + 1).min(max_visible);
|
||||||
(app.history_selected + 1).min(max_visible);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
@@ -300,7 +308,11 @@ fn handle_stats_key(app: &mut App, key: KeyEvent) {
|
|||||||
KeyCode::Char('3') => app.stats_tab = 2,
|
KeyCode::Char('3') => app.stats_tab = 2,
|
||||||
KeyCode::Tab => app.stats_tab = (app.stats_tab + 1) % 3,
|
KeyCode::Tab => app.stats_tab = (app.stats_tab + 1) % 3,
|
||||||
KeyCode::BackTab => {
|
KeyCode::BackTab => {
|
||||||
app.stats_tab = if app.stats_tab == 0 { 2 } else { app.stats_tab - 1 }
|
app.stats_tab = if app.stats_tab == 0 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
app.stats_tab - 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -313,7 +325,13 @@ fn handle_stats_key(app: &mut App, key: KeyEvent) {
|
|||||||
KeyCode::Char('2') => app.stats_tab = 1,
|
KeyCode::Char('2') => app.stats_tab = 1,
|
||||||
KeyCode::Char('3') => app.stats_tab = 2,
|
KeyCode::Char('3') => app.stats_tab = 2,
|
||||||
KeyCode::Tab => app.stats_tab = (app.stats_tab + 1) % 3,
|
KeyCode::Tab => app.stats_tab = (app.stats_tab + 1) % 3,
|
||||||
KeyCode::BackTab => app.stats_tab = if app.stats_tab == 0 { 2 } else { app.stats_tab - 1 },
|
KeyCode::BackTab => {
|
||||||
|
app.stats_tab = if app.stats_tab == 0 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
app.stats_tab - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,9 +479,8 @@ fn render_drill(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
let wpm = drill.wpm();
|
let wpm = drill.wpm();
|
||||||
let accuracy = drill.accuracy();
|
let accuracy = drill.accuracy();
|
||||||
let errors = drill.typo_count();
|
let errors = drill.typo_count();
|
||||||
let header_text = format!(
|
let header_text =
|
||||||
" {mode_name} | WPM: {wpm:.0} | Acc: {accuracy:.1}% | Errors: {errors}"
|
format!(" {mode_name} | WPM: {wpm:.0} | Acc: {accuracy:.1}% | Errors: {errors}");
|
||||||
);
|
|
||||||
let header = Paragraph::new(Line::from(Span::styled(
|
let header = Paragraph::new(Line::from(Span::styled(
|
||||||
&*header_text,
|
&*header_text,
|
||||||
Style::default()
|
Style::default()
|
||||||
@@ -525,11 +542,7 @@ fn render_drill(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
let unlocked = app.skill_tree.total_unlocked_count() as f64;
|
let unlocked = app.skill_tree.total_unlocked_count() as f64;
|
||||||
let total = app.skill_tree.total_unique_keys as f64;
|
let total = app.skill_tree.total_unique_keys as f64;
|
||||||
let progress_val = (unlocked / total).min(1.0);
|
let progress_val = (unlocked / total).min(1.0);
|
||||||
let progress = ProgressBar::new(
|
let progress = ProgressBar::new("Key Progress", progress_val, app.theme);
|
||||||
"Key Progress",
|
|
||||||
progress_val,
|
|
||||||
app.theme,
|
|
||||||
);
|
|
||||||
frame.render_widget(progress, main_layout[idx]);
|
frame.render_widget(progress, main_layout[idx]);
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
@@ -550,7 +563,12 @@ fn render_drill(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(sidebar_area) = app_layout.sidebar {
|
if let Some(sidebar_area) = app_layout.sidebar {
|
||||||
let sidebar = StatsSidebar::new(drill, app.last_result.as_ref(), &app.drill_history, app.theme);
|
let sidebar = StatsSidebar::new(
|
||||||
|
drill,
|
||||||
|
app.last_result.as_ref(),
|
||||||
|
&app.drill_history,
|
||||||
|
app.theme,
|
||||||
|
);
|
||||||
frame.render_widget(sidebar, sidebar_area);
|
frame.render_widget(sidebar, sidebar_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,9 +627,15 @@ fn render_settings(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
.unwrap_or("rust");
|
.unwrap_or("rust");
|
||||||
|
|
||||||
let fields: Vec<(String, String)> = vec![
|
let fields: Vec<(String, String)> = vec![
|
||||||
("Target WPM".to_string(), format!("{}", app.config.target_wpm)),
|
(
|
||||||
|
"Target WPM".to_string(),
|
||||||
|
format!("{}", app.config.target_wpm),
|
||||||
|
),
|
||||||
("Theme".to_string(), app.config.theme.clone()),
|
("Theme".to_string(), app.config.theme.clone()),
|
||||||
("Word Count".to_string(), format!("{}", app.config.word_count)),
|
(
|
||||||
|
"Word Count".to_string(),
|
||||||
|
format!("{}", app.config.word_count),
|
||||||
|
),
|
||||||
("Code Language".to_string(), current_lang.to_string()),
|
("Code Language".to_string(), current_lang.to_string()),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -633,7 +657,12 @@ fn render_settings(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
|
|
||||||
let field_layout = Layout::default()
|
let field_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(fields.iter().map(|_| Constraint::Length(3)).collect::<Vec<_>>())
|
.constraints(
|
||||||
|
fields
|
||||||
|
.iter()
|
||||||
|
.map(|_| Constraint::Length(3))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
.split(layout[1]);
|
.split(layout[1]);
|
||||||
|
|
||||||
for (i, (label, value)) in fields.iter().enumerate() {
|
for (i, (label, value)) in fields.iter().enumerate() {
|
||||||
@@ -643,11 +672,17 @@ fn render_settings(frame: &mut ratatui::Frame, app: &App) {
|
|||||||
let label_text = format!("{indicator}{label}:");
|
let label_text = format!("{indicator}{label}:");
|
||||||
let value_text = format!(" < {value} >");
|
let value_text = format!(" < {value} >");
|
||||||
|
|
||||||
let label_style = Style::default().fg(if is_selected {
|
let label_style = Style::default()
|
||||||
|
.fg(if is_selected {
|
||||||
colors.accent()
|
colors.accent()
|
||||||
} else {
|
} else {
|
||||||
colors.fg()
|
colors.fg()
|
||||||
}).add_modifier(if is_selected { Modifier::BOLD } else { Modifier::empty() });
|
})
|
||||||
|
.add_modifier(if is_selected {
|
||||||
|
Modifier::BOLD
|
||||||
|
} else {
|
||||||
|
Modifier::empty()
|
||||||
|
});
|
||||||
|
|
||||||
let value_style = Style::default().fg(if is_selected {
|
let value_style = Style::default().fg(if is_selected {
|
||||||
colors.focused_key()
|
colors.focused_key()
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod input;
|
|
||||||
pub mod drill;
|
pub mod drill;
|
||||||
|
pub mod input;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::session::input::KeystrokeEvent;
|
|
||||||
use crate::session::drill::DrillState;
|
use crate::session::drill::DrillState;
|
||||||
|
use crate::session::input::KeystrokeEvent;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DrillResult {
|
pub struct DrillResult {
|
||||||
@@ -37,7 +37,12 @@ pub struct KeyTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DrillResult {
|
impl DrillResult {
|
||||||
pub fn from_drill(drill: &DrillState, events: &[KeystrokeEvent], drill_mode: &str, ranked: bool) -> Self {
|
pub fn from_drill(
|
||||||
|
drill: &DrillState,
|
||||||
|
events: &[KeystrokeEvent],
|
||||||
|
drill_mode: &str,
|
||||||
|
ranked: bool,
|
||||||
|
) -> Self {
|
||||||
let per_key_times: Vec<KeyTime> = events
|
let per_key_times: Vec<KeyTime> = events
|
||||||
.windows(2)
|
.windows(2)
|
||||||
.map(|pair| {
|
.map(|pair| {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use std::io::Write;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{Serialize, de::DeserializeOwned};
|
||||||
|
|
||||||
use crate::store::schema::{KeyStatsData, DrillHistoryData, ProfileData};
|
use crate::store::schema::{DrillHistoryData, KeyStatsData, ProfileData};
|
||||||
|
|
||||||
pub struct JsonStore {
|
pub struct JsonStore {
|
||||||
base_dir: PathBuf,
|
base_dir: PathBuf,
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ impl Widget for ActivityHeatmap<'_> {
|
|||||||
let weeks_to_show = weeks_to_show.min(26);
|
let weeks_to_show = weeks_to_show.min(26);
|
||||||
let start_date = today - chrono::Duration::weeks(weeks_to_show as i64);
|
let start_date = today - chrono::Duration::weeks(weeks_to_show as i64);
|
||||||
// Align to Monday
|
// Align to Monday
|
||||||
let start_date = start_date
|
let start_date =
|
||||||
- chrono::Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
start_date - chrono::Duration::days(start_date.weekday().num_days_from_monday() as i64);
|
||||||
|
|
||||||
// Day-of-week labels
|
// Day-of-week labels
|
||||||
let day_labels = ["M", " ", "W", " ", "F", " ", "S"];
|
let day_labels = ["M", " ", "W", " ", "F", " ", "S"];
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ impl Widget for Dashboard<'_> {
|
|||||||
Style::default().fg(colors.text_pending()),
|
Style::default().fg(colors.text_pending()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let title = Paragraph::new(Line::from(title_spans))
|
let title = Paragraph::new(Line::from(title_spans)).alignment(Alignment::Center);
|
||||||
.alignment(Alignment::Center);
|
|
||||||
title.render(layout[0], buf);
|
title.render(layout[0], buf);
|
||||||
|
|
||||||
let wpm_text = format!("{:.0} WPM", self.result.wpm);
|
let wpm_text = format!("{:.0} WPM", self.result.wpm);
|
||||||
|
|||||||
@@ -91,11 +91,7 @@ impl Widget for KeyboardDiagram<'_> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let offsets: &[u16] = if self.compact {
|
let offsets: &[u16] = if self.compact { &[0, 1, 3] } else { &[1, 3, 5] };
|
||||||
&[0, 1, 3]
|
|
||||||
} else {
|
|
||||||
&[1, 3, 5]
|
|
||||||
};
|
|
||||||
|
|
||||||
for (row_idx, row) in ROWS.iter().enumerate() {
|
for (row_idx, row) in ROWS.iter().enumerate() {
|
||||||
let y = inner.y + row_idx as u16;
|
let y = inner.y + row_idx as u16;
|
||||||
@@ -128,21 +124,13 @@ impl Widget for KeyboardDiagram<'_> {
|
|||||||
.bg(bg)
|
.bg(bg)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else if is_next {
|
} else if is_next {
|
||||||
Style::default()
|
Style::default().fg(colors.bg()).bg(colors.accent())
|
||||||
.fg(colors.bg())
|
|
||||||
.bg(colors.accent())
|
|
||||||
} else if is_focused {
|
} else if is_focused {
|
||||||
Style::default()
|
Style::default().fg(colors.bg()).bg(colors.focused_key())
|
||||||
.fg(colors.bg())
|
|
||||||
.bg(colors.focused_key())
|
|
||||||
} else if is_unlocked {
|
} else if is_unlocked {
|
||||||
Style::default()
|
Style::default().fg(colors.fg()).bg(finger_color(key))
|
||||||
.fg(colors.fg())
|
|
||||||
.bg(finger_color(key))
|
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default().fg(colors.text_pending()).bg(colors.bg())
|
||||||
.fg(colors.text_pending())
|
|
||||||
.bg(colors.bg())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let display = if self.compact {
|
let display = if self.compact {
|
||||||
|
|||||||
@@ -117,13 +117,29 @@ impl Widget for &Menu<'_> {
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
.split(layout[2]);
|
.split(layout[2]);
|
||||||
|
let key_width = self
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| item.key.len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(1);
|
||||||
|
|
||||||
for (i, item) in self.items.iter().enumerate() {
|
for (i, item) in self.items.iter().enumerate() {
|
||||||
let is_selected = i == self.selected;
|
let is_selected = i == self.selected;
|
||||||
let indicator = if is_selected { ">" } else { " " };
|
let indicator = if is_selected { ">" } else { " " };
|
||||||
|
|
||||||
let label_text = format!(" {indicator} [{key}] {label}", key = item.key, label = item.label);
|
let label_text = format!(
|
||||||
let desc_text = format!(" {}", item.description);
|
" {indicator} [{key:<key_width$}] {label}",
|
||||||
|
key = item.key,
|
||||||
|
key_width = key_width,
|
||||||
|
label = item.label
|
||||||
|
);
|
||||||
|
let desc_text = format!(
|
||||||
|
" {:indent$}{}",
|
||||||
|
"",
|
||||||
|
item.description,
|
||||||
|
indent = key_width + 4
|
||||||
|
);
|
||||||
|
|
||||||
let lines = vec![
|
let lines = vec![
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ use ratatui::widgets::{Block, Paragraph, Widget};
|
|||||||
|
|
||||||
use crate::engine::key_stats::KeyStatsStore;
|
use crate::engine::key_stats::KeyStatsStore;
|
||||||
use crate::engine::skill_tree::{
|
use crate::engine::skill_tree::{
|
||||||
BranchId, BranchStatus, DrillScope, SkillTree as SkillTreeEngine,
|
BranchId, BranchStatus, DrillScope, SkillTree as SkillTreeEngine, get_branch_definition,
|
||||||
get_branch_definition,
|
|
||||||
};
|
};
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
|
||||||
@@ -88,7 +87,8 @@ impl Widget for SkillTreeWidget<'_> {
|
|||||||
let bp = self.skill_tree.branch_progress(branches[self.selected]);
|
let bp = self.skill_tree.branch_progress(branches[self.selected]);
|
||||||
if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked {
|
if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked {
|
||||||
" Complete a-z to unlock branches "
|
" Complete a-z to unlock branches "
|
||||||
} else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress {
|
} else if bp.status == BranchStatus::Available || bp.status == BranchStatus::InProgress
|
||||||
|
{
|
||||||
" [Enter] Start Drill [\u{2191}\u{2193}/jk] Navigate [q] Back "
|
" [Enter] Start Drill [\u{2191}\u{2193}/jk] Navigate [q] Back "
|
||||||
} else {
|
} else {
|
||||||
" [\u{2191}\u{2193}/jk] Navigate [q] Back "
|
" [\u{2191}\u{2193}/jk] Navigate [q] Back "
|
||||||
@@ -113,22 +113,29 @@ impl SkillTreeWidget<'_> {
|
|||||||
// Root: Lowercase a-z
|
// Root: Lowercase a-z
|
||||||
let lowercase_bp = self.skill_tree.branch_progress(BranchId::Lowercase);
|
let lowercase_bp = self.skill_tree.branch_progress(BranchId::Lowercase);
|
||||||
let lowercase_def = get_branch_definition(BranchId::Lowercase);
|
let lowercase_def = get_branch_definition(BranchId::Lowercase);
|
||||||
let lowercase_total = lowercase_def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
|
let lowercase_total = lowercase_def
|
||||||
let lowercase_confident = self.skill_tree.branch_confident_keys(BranchId::Lowercase, self.key_stats);
|
.levels
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.keys.len())
|
||||||
|
.sum::<usize>();
|
||||||
|
let lowercase_confident = self
|
||||||
|
.skill_tree
|
||||||
|
.branch_confident_keys(BranchId::Lowercase, self.key_stats);
|
||||||
|
|
||||||
let (prefix, style) = match lowercase_bp.status {
|
let (prefix, style) = match lowercase_bp.status {
|
||||||
BranchStatus::Complete => (
|
BranchStatus::Complete => (
|
||||||
"\u{2605} ",
|
"\u{2605} ",
|
||||||
Style::default().fg(colors.text_correct()).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(colors.text_correct())
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
BranchStatus::InProgress => (
|
BranchStatus::InProgress => (
|
||||||
"\u{25b6} ",
|
"\u{25b6} ",
|
||||||
Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
),
|
.fg(colors.accent())
|
||||||
_ => (
|
.add_modifier(Modifier::BOLD),
|
||||||
" ",
|
|
||||||
Style::default().fg(colors.text_pending()),
|
|
||||||
),
|
),
|
||||||
|
_ => (" ", Style::default().fg(colors.text_pending())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_text = match lowercase_bp.status {
|
let status_text = match lowercase_bp.status {
|
||||||
@@ -173,43 +180,56 @@ impl SkillTreeWidget<'_> {
|
|||||||
let bp = self.skill_tree.branch_progress(branch_id);
|
let bp = self.skill_tree.branch_progress(branch_id);
|
||||||
let def = get_branch_definition(branch_id);
|
let def = get_branch_definition(branch_id);
|
||||||
let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
|
let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
|
||||||
let confident_keys = self.skill_tree.branch_confident_keys(branch_id, self.key_stats);
|
let confident_keys = self
|
||||||
|
.skill_tree
|
||||||
|
.branch_confident_keys(branch_id, self.key_stats);
|
||||||
let is_selected = i == self.selected;
|
let is_selected = i == self.selected;
|
||||||
|
|
||||||
let (prefix, style) = match bp.status {
|
let (prefix, style) = match bp.status {
|
||||||
BranchStatus::Complete => (
|
BranchStatus::Complete => (
|
||||||
"\u{2605} ",
|
"\u{2605} ",
|
||||||
if is_selected {
|
if is_selected {
|
||||||
Style::default().fg(colors.text_correct()).add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
Style::default()
|
||||||
|
.fg(colors.text_correct())
|
||||||
|
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.text_correct()).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(colors.text_correct())
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BranchStatus::InProgress => (
|
BranchStatus::InProgress => (
|
||||||
"\u{25b6} ",
|
"\u{25b6} ",
|
||||||
if is_selected {
|
if is_selected {
|
||||||
Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
Style::default()
|
||||||
|
.fg(colors.accent())
|
||||||
|
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD)
|
Style::default()
|
||||||
|
.fg(colors.accent())
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BranchStatus::Available => (
|
BranchStatus::Available => (
|
||||||
" ",
|
" ",
|
||||||
if is_selected {
|
if is_selected {
|
||||||
Style::default().fg(colors.fg()).add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
Style::default()
|
||||||
|
.fg(colors.fg())
|
||||||
|
.add_modifier(Modifier::BOLD | Modifier::REVERSED)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.fg())
|
Style::default().fg(colors.fg())
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
BranchStatus::Locked => (
|
BranchStatus::Locked => (" ", Style::default().fg(colors.text_pending())),
|
||||||
" ",
|
|
||||||
Style::default().fg(colors.text_pending()),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let status_text = match bp.status {
|
let status_text = match bp.status {
|
||||||
BranchStatus::Complete => format!("COMPLETE {confident_keys}/{total_keys} keys"),
|
BranchStatus::Complete => format!("COMPLETE {confident_keys}/{total_keys} keys"),
|
||||||
BranchStatus::InProgress => format!("Lvl {}/{} {confident_keys}/{total_keys} keys", bp.current_level + 1, def.levels.len()),
|
BranchStatus::InProgress => format!(
|
||||||
|
"Lvl {}/{} {confident_keys}/{total_keys} keys",
|
||||||
|
bp.current_level + 1,
|
||||||
|
def.levels.len()
|
||||||
|
),
|
||||||
BranchStatus::Available => format!("Available 0/{total_keys} keys"),
|
BranchStatus::Available => format!("Available 0/{total_keys} keys"),
|
||||||
BranchStatus::Locked => format!("Locked 0/{total_keys} keys"),
|
BranchStatus::Locked => format!("Locked 0/{total_keys} keys"),
|
||||||
};
|
};
|
||||||
@@ -218,7 +238,10 @@ impl SkillTreeWidget<'_> {
|
|||||||
|
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(format!("{sel_indicator}{prefix}{}", def.name), style),
|
Span::styled(format!("{sel_indicator}{prefix}{}", def.name), style),
|
||||||
Span::styled(format!(" {status_text}"), Style::default().fg(colors.text_pending())),
|
Span::styled(
|
||||||
|
format!(" {status_text}"),
|
||||||
|
Style::default().fg(colors.text_pending()),
|
||||||
|
),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
let pct = if total_keys > 0 {
|
let pct = if total_keys > 0 {
|
||||||
@@ -251,14 +274,18 @@ impl SkillTreeWidget<'_> {
|
|||||||
|
|
||||||
// Branch title with level info
|
// Branch title with level info
|
||||||
let level_text = match bp.status {
|
let level_text = match bp.status {
|
||||||
BranchStatus::InProgress => format!("Level {}/{}", bp.current_level + 1, def.levels.len()),
|
BranchStatus::InProgress => {
|
||||||
|
format!("Level {}/{}", bp.current_level + 1, def.levels.len())
|
||||||
|
}
|
||||||
BranchStatus::Complete => format!("Level {}/{}", def.levels.len(), def.levels.len()),
|
BranchStatus::Complete => format!("Level {}/{}", def.levels.len(), def.levels.len()),
|
||||||
_ => format!("Level 0/{}", def.levels.len()),
|
_ => format!("Level 0/{}", def.levels.len()),
|
||||||
};
|
};
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(" {}", def.name),
|
format!(" {}", def.name),
|
||||||
Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(colors.accent())
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!(" {level_text}"),
|
format!(" {level_text}"),
|
||||||
@@ -267,10 +294,13 @@ impl SkillTreeWidget<'_> {
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
// Per-level key breakdown
|
// Per-level key breakdown
|
||||||
let focused = self.skill_tree.focused_key(DrillScope::Branch(branch_id), self.key_stats);
|
let focused = self
|
||||||
|
.skill_tree
|
||||||
|
.focused_key(DrillScope::Branch(branch_id), self.key_stats);
|
||||||
|
|
||||||
for (level_idx, level) in def.levels.iter().enumerate() {
|
for (level_idx, level) in def.levels.iter().enumerate() {
|
||||||
let level_status = if bp.status == BranchStatus::Complete || level_idx < bp.current_level {
|
let level_status =
|
||||||
|
if bp.status == BranchStatus::Complete || level_idx < bp.current_level {
|
||||||
"complete"
|
"complete"
|
||||||
} else if bp.status == BranchStatus::InProgress && level_idx == bp.current_level {
|
} else if bp.status == BranchStatus::InProgress && level_idx == bp.current_level {
|
||||||
"in progress"
|
"in progress"
|
||||||
@@ -324,7 +354,9 @@ impl SkillTreeWidget<'_> {
|
|||||||
// Average confidence
|
// Average confidence
|
||||||
let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
|
let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::<usize>();
|
||||||
let avg_conf = if total_keys > 0 {
|
let avg_conf = if total_keys > 0 {
|
||||||
let sum: f64 = def.levels.iter()
|
let sum: f64 = def
|
||||||
|
.levels
|
||||||
|
.iter()
|
||||||
.flat_map(|l| l.keys.iter())
|
.flat_map(|l| l.keys.iter())
|
||||||
.map(|&ch| self.key_stats.get_confidence(ch).min(1.0))
|
.map(|&ch| self.key_stats.get_confidence(ch).min(1.0))
|
||||||
.sum();
|
.sum();
|
||||||
@@ -334,7 +366,11 @@ impl SkillTreeWidget<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!(" Avg Confidence: {} {:.0}%", progress_bar_str(avg_conf, 20), avg_conf * 100.0),
|
format!(
|
||||||
|
" Avg Confidence: {} {:.0}%",
|
||||||
|
progress_bar_str(avg_conf, 20),
|
||||||
|
avg_conf * 100.0
|
||||||
|
),
|
||||||
Style::default().fg(colors.text_pending()),
|
Style::default().fg(colors.text_pending()),
|
||||||
)));
|
)));
|
||||||
|
|
||||||
@@ -346,9 +382,5 @@ impl SkillTreeWidget<'_> {
|
|||||||
fn progress_bar_str(pct: f64, width: usize) -> String {
|
fn progress_bar_str(pct: f64, width: usize) -> String {
|
||||||
let filled = (pct * width as f64).round() as usize;
|
let filled = (pct * width as f64).round() as usize;
|
||||||
let empty = width.saturating_sub(filled);
|
let empty = width.saturating_sub(filled);
|
||||||
format!(
|
format!("{}{}", "\u{2588}".repeat(filled), "\u{2591}".repeat(empty),)
|
||||||
"{}{}",
|
|
||||||
"\u{2588}".repeat(filled),
|
|
||||||
"\u{2591}".repeat(empty),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,7 @@ impl Widget for StatsDashboard<'_> {
|
|||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.text_pending())
|
Style::default().fg(colors.text_pending())
|
||||||
};
|
};
|
||||||
vec![
|
vec![Span::styled(format!(" {label} "), style), Span::raw(" ")]
|
||||||
Span::styled(format!(" {label} "), style),
|
|
||||||
Span::raw(" "),
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Paragraph::new(Line::from(tab_spans)).render(layout[0], buf);
|
Paragraph::new(Line::from(tab_spans)).render(layout[0], buf);
|
||||||
@@ -173,13 +170,8 @@ impl StatsDashboard<'_> {
|
|||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Summary stats as bordered table
|
// Summary stats as bordered table
|
||||||
let avg_wpm =
|
let avg_wpm = self.history.iter().map(|r| r.wpm).sum::<f64>() / self.history.len() as f64;
|
||||||
self.history.iter().map(|r| r.wpm).sum::<f64>() / self.history.len() as f64;
|
let best_wpm = self.history.iter().map(|r| r.wpm).fold(0.0f64, f64::max);
|
||||||
let best_wpm = self
|
|
||||||
.history
|
|
||||||
.iter()
|
|
||||||
.map(|r| r.wpm)
|
|
||||||
.fold(0.0f64, f64::max);
|
|
||||||
let avg_accuracy =
|
let avg_accuracy =
|
||||||
self.history.iter().map(|r| r.accuracy).sum::<f64>() / self.history.len() as f64;
|
self.history.iter().map(|r| r.accuracy).sum::<f64>() / self.history.len() as f64;
|
||||||
let total_time: f64 = self.history.iter().map(|r| r.elapsed_secs).sum();
|
let total_time: f64 = self.history.iter().map(|r| r.elapsed_secs).sum();
|
||||||
@@ -297,12 +289,27 @@ impl StatsDashboard<'_> {
|
|||||||
// Y-axis labels (max, mid, 0)
|
// Y-axis labels (max, mid, 0)
|
||||||
let max_label = format!("{:.0}", max_wpm);
|
let max_label = format!("{:.0}", max_wpm);
|
||||||
let mid_label = format!("{:.0}", max_wpm / 2.0);
|
let mid_label = format!("{:.0}", max_wpm / 2.0);
|
||||||
buf.set_string(inner.x, inner.y, &max_label, Style::default().fg(colors.text_pending()));
|
buf.set_string(
|
||||||
|
inner.x,
|
||||||
|
inner.y,
|
||||||
|
&max_label,
|
||||||
|
Style::default().fg(colors.text_pending()),
|
||||||
|
);
|
||||||
if inner.height > 3 {
|
if inner.height > 3 {
|
||||||
let mid_y = inner.y + inner.height / 2;
|
let mid_y = inner.y + inner.height / 2;
|
||||||
buf.set_string(inner.x, mid_y, &mid_label, Style::default().fg(colors.text_pending()));
|
buf.set_string(
|
||||||
|
inner.x,
|
||||||
|
mid_y,
|
||||||
|
&mid_label,
|
||||||
|
Style::default().fg(colors.text_pending()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
buf.set_string(inner.x, inner.y + inner.height - 1, "0", Style::default().fg(colors.text_pending()));
|
buf.set_string(
|
||||||
|
inner.x,
|
||||||
|
inner.y + inner.height - 1,
|
||||||
|
"0",
|
||||||
|
Style::default().fg(colors.text_pending()),
|
||||||
|
);
|
||||||
|
|
||||||
let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||||
|
|
||||||
@@ -342,7 +349,12 @@ impl StatsDashboard<'_> {
|
|||||||
// WPM label on top row
|
// WPM label on top row
|
||||||
if bar_spacing >= 3 {
|
if bar_spacing >= 3 {
|
||||||
let label = format!("{wpm:.0}");
|
let label = format!("{wpm:.0}");
|
||||||
buf.set_string(x, inner.y, &label, Style::default().fg(colors.text_pending()));
|
buf.set_string(
|
||||||
|
x,
|
||||||
|
inner.y,
|
||||||
|
&label,
|
||||||
|
Style::default().fg(colors.text_pending()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,8 +424,7 @@ impl StatsDashboard<'_> {
|
|||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
let avg_wpm =
|
let avg_wpm = self.history.iter().map(|r| r.wpm).sum::<f64>() / self.history.len() as f64;
|
||||||
self.history.iter().map(|r| r.wpm).sum::<f64>() / self.history.len() as f64;
|
|
||||||
let avg_accuracy =
|
let avg_accuracy =
|
||||||
self.history.iter().map(|r| r.accuracy).sum::<f64>() / self.history.len() as f64;
|
self.history.iter().map(|r| r.accuracy).sum::<f64>() / self.history.len() as f64;
|
||||||
|
|
||||||
@@ -454,7 +465,11 @@ impl StatsDashboard<'_> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Level progress
|
// Level progress
|
||||||
let total_score: f64 = self.history.iter().map(|r| r.wpm * r.accuracy / 100.0).sum();
|
let total_score: f64 = self
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.wpm * r.accuracy / 100.0)
|
||||||
|
.sum();
|
||||||
let level = ((total_score / 100.0).sqrt() as u32).max(1);
|
let level = ((total_score / 100.0).sqrt() as u32).max(1);
|
||||||
let next_level_score = ((level + 1) as f64).powi(2) * 100.0;
|
let next_level_score = ((level + 1) as f64).powi(2) * 100.0;
|
||||||
let current_level_score = (level as f64).powi(2) * 100.0;
|
let current_level_score = (level as f64).powi(2) * 100.0;
|
||||||
@@ -523,11 +538,7 @@ impl StatsDashboard<'_> {
|
|||||||
" "
|
" "
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode_str = if result.ranked {
|
let mode_str = if result.ranked { "" } else { " (unranked)" };
|
||||||
""
|
|
||||||
} else {
|
|
||||||
" (unranked)"
|
|
||||||
};
|
|
||||||
let row = format!(
|
let row = format!(
|
||||||
" {wpm_indicator}{idx_str} {wpm_str} {raw_str} {acc_str} {time_str:>6} {date_str} {mode}{mode_str}",
|
" {wpm_indicator}{idx_str} {wpm_str} {raw_str} {acc_str} {time_str:>6} {date_str} {mode}{mode_str}",
|
||||||
mode = result.drill_mode,
|
mode = result.drill_mode,
|
||||||
@@ -738,12 +749,7 @@ impl StatsDashboard<'_> {
|
|||||||
} else {
|
} else {
|
||||||
format!("{key} ")
|
format!("{key} ")
|
||||||
};
|
};
|
||||||
buf.set_string(
|
buf.set_string(x, y, &display, Style::default().fg(fg_color).bg(bg_color));
|
||||||
x,
|
|
||||||
y,
|
|
||||||
&display,
|
|
||||||
Style::default().fg(fg_color).bg(bg_color),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,12 +799,7 @@ impl StatsDashboard<'_> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let text = format!(" '{ch}' {time:.0}ms");
|
let text = format!(" '{ch}' {time:.0}ms");
|
||||||
buf.set_string(
|
buf.set_string(inner.x, y, &text, Style::default().fg(colors.error()));
|
||||||
inner.x,
|
|
||||||
y,
|
|
||||||
&text,
|
|
||||||
Style::default().fg(colors.error()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,12 +827,7 @@ impl StatsDashboard<'_> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let text = format!(" '{ch}' {time:.0}ms");
|
let text = format!(" '{ch}' {time:.0}ms");
|
||||||
buf.set_string(
|
buf.set_string(inner.x, y, &text, Style::default().fg(colors.success()));
|
||||||
inner.x,
|
|
||||||
y,
|
|
||||||
&text,
|
|
||||||
Style::default().fg(colors.success()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,12 +949,7 @@ fn render_text_bar(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Label on first line
|
// Label on first line
|
||||||
buf.set_string(
|
buf.set_string(area.x, area.y, label, Style::default().fg(fill_color));
|
||||||
area.x,
|
|
||||||
area.y,
|
|
||||||
label,
|
|
||||||
Style::default().fg(fill_color),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bar on second line using ┃ filled / dim ┃ empty
|
// Bar on second line using ┃ filled / dim ┃ empty
|
||||||
let bar_width = (area.width as usize).saturating_sub(4);
|
let bar_width = (area.width as usize).saturating_sub(4);
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ impl<'a> StatsSidebar<'a> {
|
|||||||
history: &'a [DrillResult],
|
history: &'a [DrillResult],
|
||||||
theme: &'a Theme,
|
theme: &'a Theme,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { drill, last_result, history, theme }
|
Self {
|
||||||
|
drill,
|
||||||
|
last_result,
|
||||||
|
history,
|
||||||
|
theme,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +164,10 @@ impl Widget for StatsSidebar<'_> {
|
|||||||
colors.text_pending()
|
colors.text_pending()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut lines = vec![
|
let mut lines = vec![Line::from(vec![
|
||||||
Line::from(vec![
|
|
||||||
Span::styled("WPM: ", Style::default().fg(colors.fg())),
|
Span::styled("WPM: ", Style::default().fg(colors.fg())),
|
||||||
Span::styled(wpm_str, Style::default().fg(colors.accent())),
|
Span::styled(wpm_str, Style::default().fg(colors.accent())),
|
||||||
]),
|
])];
|
||||||
];
|
|
||||||
|
|
||||||
if prior_count > 0 {
|
if prior_count > 0 {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ use ratatui::style::{Modifier, Style};
|
|||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Paragraph, Widget, Wrap};
|
use ratatui::widgets::{Block, Paragraph, Widget, Wrap};
|
||||||
|
|
||||||
use crate::session::input::CharStatus;
|
|
||||||
use crate::session::drill::DrillState;
|
use crate::session::drill::DrillState;
|
||||||
|
use crate::session::input::CharStatus;
|
||||||
use crate::ui::theme::Theme;
|
use crate::ui::theme::Theme;
|
||||||
|
|
||||||
pub struct TypingArea<'a> {
|
pub struct TypingArea<'a> {
|
||||||
@@ -78,6 +78,7 @@ impl Widget for TypingArea<'_> {
|
|||||||
|
|
||||||
for token in &tokens {
|
for token in &tokens {
|
||||||
let idx = token.target_idx;
|
let idx = token.target_idx;
|
||||||
|
let target_ch = self.drill.target[idx];
|
||||||
|
|
||||||
let style = if idx < self.drill.cursor {
|
let style = if idx < self.drill.cursor {
|
||||||
match &self.drill.input[idx] {
|
match &self.drill.input[idx] {
|
||||||
@@ -91,6 +92,7 @@ impl Widget for TypingArea<'_> {
|
|||||||
Style::default()
|
Style::default()
|
||||||
.fg(colors.text_cursor_fg())
|
.fg(colors.text_cursor_fg())
|
||||||
.bg(colors.text_cursor_bg())
|
.bg(colors.text_cursor_bg())
|
||||||
|
.add_modifier(Modifier::REVERSED | Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(colors.text_pending())
|
Style::default().fg(colors.text_pending())
|
||||||
};
|
};
|
||||||
@@ -99,7 +101,6 @@ impl Widget for TypingArea<'_> {
|
|||||||
// but always show the token display for whitespace markers
|
// but always show the token display for whitespace markers
|
||||||
let display = if idx < self.drill.cursor {
|
let display = if idx < self.drill.cursor {
|
||||||
if let CharStatus::Incorrect(actual) = &self.drill.input[idx] {
|
if let CharStatus::Incorrect(actual) = &self.drill.input[idx] {
|
||||||
let target_ch = self.drill.target[idx];
|
|
||||||
if target_ch == '\n' || target_ch == '\t' {
|
if target_ch == '\n' || target_ch == '\t' {
|
||||||
// Show the whitespace marker even when incorrect
|
// Show the whitespace marker even when incorrect
|
||||||
token.display.clone()
|
token.display.clone()
|
||||||
@@ -109,6 +110,8 @@ impl Widget for TypingArea<'_> {
|
|||||||
} else {
|
} else {
|
||||||
token.display.clone()
|
token.display.clone()
|
||||||
}
|
}
|
||||||
|
} else if idx == self.drill.cursor && target_ch == ' ' {
|
||||||
|
"\u{00b7}".to_string()
|
||||||
} else {
|
} else {
|
||||||
token.display.clone()
|
token.display.clone()
|
||||||
};
|
};
|
||||||
@@ -120,6 +123,16 @@ impl Widget for TypingArea<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep cursor visible at end-of-input as an insertion marker.
|
||||||
|
if self.drill.cursor >= self.drill.target.len() {
|
||||||
|
lines.last_mut().unwrap().push(Span::styled(
|
||||||
|
"\u{258f}",
|
||||||
|
Style::default()
|
||||||
|
.fg(colors.accent())
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let ratatui_lines: Vec<Line> = lines.into_iter().map(Line::from).collect();
|
let ratatui_lines: Vec<Line> = lines.into_iter().map(Line::from).collect();
|
||||||
|
|
||||||
let block = Block::bordered()
|
let block = Block::bordered()
|
||||||
|
|||||||
131
src/ui/theme.rs
131
src/ui/theme.rs
@@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[folder = "assets/themes/"]
|
#[folder = "assets/themes/"]
|
||||||
struct ThemeAssets;
|
struct ThemeAssets;
|
||||||
|
|
||||||
|
const TERMINAL_DEFAULT_THEME: &str = "terminal-default";
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -42,7 +44,10 @@ impl Theme {
|
|||||||
pub fn load(name: &str) -> Option<Self> {
|
pub fn load(name: &str) -> Option<Self> {
|
||||||
// Try user themes dir
|
// Try user themes dir
|
||||||
if let Some(config_dir) = dirs::config_dir() {
|
if let Some(config_dir) = dirs::config_dir() {
|
||||||
let user_theme_path = config_dir.join("keydr").join("themes").join(format!("{name}.toml"));
|
let user_theme_path = config_dir
|
||||||
|
.join("keydr")
|
||||||
|
.join("themes")
|
||||||
|
.join(format!("{name}.toml"));
|
||||||
if let Ok(content) = fs::read_to_string(&user_theme_path) {
|
if let Ok(content) = fs::read_to_string(&user_theme_path) {
|
||||||
if let Ok(theme) = toml::from_str::<Theme>(&content) {
|
if let Ok(theme) = toml::from_str::<Theme>(&content) {
|
||||||
return Some(theme);
|
return Some(theme);
|
||||||
@@ -64,17 +69,20 @@ impl Theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn available_themes() -> Vec<String> {
|
pub fn available_themes() -> Vec<String> {
|
||||||
ThemeAssets::iter()
|
let mut themes: Vec<String> = ThemeAssets::iter()
|
||||||
.filter_map(|f| {
|
.filter_map(|f| f.strip_suffix(".toml").map(|n| n.to_string()))
|
||||||
f.strip_suffix(".toml").map(|n| n.to_string())
|
.collect();
|
||||||
})
|
themes.sort_unstable();
|
||||||
.collect()
|
if let Some(idx) = themes.iter().position(|t| t == TERMINAL_DEFAULT_THEME) {
|
||||||
|
themes.swap(0, idx);
|
||||||
|
}
|
||||||
|
themes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Theme {
|
impl Default for Theme {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::load("catppuccin-mocha").unwrap_or_else(|| Self {
|
Self::load(TERMINAL_DEFAULT_THEME).unwrap_or_else(|| Self {
|
||||||
name: "default".to_string(),
|
name: "default".to_string(),
|
||||||
colors: ThemeColors::default(),
|
colors: ThemeColors::default(),
|
||||||
})
|
})
|
||||||
@@ -109,9 +117,10 @@ impl Default for ThemeColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ThemeColors {
|
impl ThemeColors {
|
||||||
pub fn parse_color(hex: &str) -> Color {
|
pub fn parse_color(value: &str) -> Color {
|
||||||
let hex = hex.trim_start_matches('#');
|
let value = value.trim();
|
||||||
if hex.len() == 6 {
|
let hex = value.trim_start_matches('#');
|
||||||
|
if hex.len() == 6 && value.starts_with('#') {
|
||||||
if let (Ok(r), Ok(g), Ok(b)) = (
|
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||||
u8::from_str_radix(&hex[0..2], 16),
|
u8::from_str_radix(&hex[0..2], 16),
|
||||||
u8::from_str_radix(&hex[2..4], 16),
|
u8::from_str_radix(&hex[2..4], 16),
|
||||||
@@ -120,27 +129,87 @@ impl ThemeColors {
|
|||||||
return Color::Rgb(r, g, b);
|
return Color::Rgb(r, g, b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Color::White
|
|
||||||
|
match value.to_ascii_lowercase().as_str() {
|
||||||
|
"reset" | "default" | "none" => Color::Reset,
|
||||||
|
"black" => Color::Black,
|
||||||
|
"red" => Color::Red,
|
||||||
|
"green" => Color::Green,
|
||||||
|
"yellow" => Color::Yellow,
|
||||||
|
"blue" => Color::Blue,
|
||||||
|
"magenta" => Color::Magenta,
|
||||||
|
"cyan" => Color::Cyan,
|
||||||
|
"gray" | "grey" => Color::Gray,
|
||||||
|
"darkgray" | "darkgrey" => Color::DarkGray,
|
||||||
|
"lightred" | "brightred" => Color::LightRed,
|
||||||
|
"lightgreen" | "brightgreen" => Color::LightGreen,
|
||||||
|
"lightyellow" | "brightyellow" => Color::LightYellow,
|
||||||
|
"lightblue" | "brightblue" => Color::LightBlue,
|
||||||
|
"lightmagenta" | "brightmagenta" => Color::LightMagenta,
|
||||||
|
"lightcyan" | "brightcyan" => Color::LightCyan,
|
||||||
|
"white" | "lightwhite" | "brightwhite" => Color::White,
|
||||||
|
_ => Color::White,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bg(&self) -> Color { Self::parse_color(&self.bg) }
|
pub fn bg(&self) -> Color {
|
||||||
pub fn fg(&self) -> Color { Self::parse_color(&self.fg) }
|
Self::parse_color(&self.bg)
|
||||||
pub fn text_correct(&self) -> Color { Self::parse_color(&self.text_correct) }
|
}
|
||||||
pub fn text_incorrect(&self) -> Color { Self::parse_color(&self.text_incorrect) }
|
pub fn fg(&self) -> Color {
|
||||||
pub fn text_incorrect_bg(&self) -> Color { Self::parse_color(&self.text_incorrect_bg) }
|
Self::parse_color(&self.fg)
|
||||||
pub fn text_pending(&self) -> Color { Self::parse_color(&self.text_pending) }
|
}
|
||||||
pub fn text_cursor_bg(&self) -> Color { Self::parse_color(&self.text_cursor_bg) }
|
pub fn text_correct(&self) -> Color {
|
||||||
pub fn text_cursor_fg(&self) -> Color { Self::parse_color(&self.text_cursor_fg) }
|
Self::parse_color(&self.text_correct)
|
||||||
pub fn focused_key(&self) -> Color { Self::parse_color(&self.focused_key) }
|
}
|
||||||
pub fn accent(&self) -> Color { Self::parse_color(&self.accent) }
|
pub fn text_incorrect(&self) -> Color {
|
||||||
pub fn accent_dim(&self) -> Color { Self::parse_color(&self.accent_dim) }
|
Self::parse_color(&self.text_incorrect)
|
||||||
pub fn border(&self) -> Color { Self::parse_color(&self.border) }
|
}
|
||||||
pub fn border_focused(&self) -> Color { Self::parse_color(&self.border_focused) }
|
pub fn text_incorrect_bg(&self) -> Color {
|
||||||
pub fn header_bg(&self) -> Color { Self::parse_color(&self.header_bg) }
|
Self::parse_color(&self.text_incorrect_bg)
|
||||||
pub fn header_fg(&self) -> Color { Self::parse_color(&self.header_fg) }
|
}
|
||||||
pub fn bar_filled(&self) -> Color { Self::parse_color(&self.bar_filled) }
|
pub fn text_pending(&self) -> Color {
|
||||||
pub fn bar_empty(&self) -> Color { Self::parse_color(&self.bar_empty) }
|
Self::parse_color(&self.text_pending)
|
||||||
pub fn error(&self) -> Color { Self::parse_color(&self.error) }
|
}
|
||||||
pub fn warning(&self) -> Color { Self::parse_color(&self.warning) }
|
pub fn text_cursor_bg(&self) -> Color {
|
||||||
pub fn success(&self) -> Color { Self::parse_color(&self.success) }
|
Self::parse_color(&self.text_cursor_bg)
|
||||||
|
}
|
||||||
|
pub fn text_cursor_fg(&self) -> Color {
|
||||||
|
Self::parse_color(&self.text_cursor_fg)
|
||||||
|
}
|
||||||
|
pub fn focused_key(&self) -> Color {
|
||||||
|
Self::parse_color(&self.focused_key)
|
||||||
|
}
|
||||||
|
pub fn accent(&self) -> Color {
|
||||||
|
Self::parse_color(&self.accent)
|
||||||
|
}
|
||||||
|
pub fn accent_dim(&self) -> Color {
|
||||||
|
Self::parse_color(&self.accent_dim)
|
||||||
|
}
|
||||||
|
pub fn border(&self) -> Color {
|
||||||
|
Self::parse_color(&self.border)
|
||||||
|
}
|
||||||
|
pub fn border_focused(&self) -> Color {
|
||||||
|
Self::parse_color(&self.border_focused)
|
||||||
|
}
|
||||||
|
pub fn header_bg(&self) -> Color {
|
||||||
|
Self::parse_color(&self.header_bg)
|
||||||
|
}
|
||||||
|
pub fn header_fg(&self) -> Color {
|
||||||
|
Self::parse_color(&self.header_fg)
|
||||||
|
}
|
||||||
|
pub fn bar_filled(&self) -> Color {
|
||||||
|
Self::parse_color(&self.bar_filled)
|
||||||
|
}
|
||||||
|
pub fn bar_empty(&self) -> Color {
|
||||||
|
Self::parse_color(&self.bar_empty)
|
||||||
|
}
|
||||||
|
pub fn error(&self) -> Color {
|
||||||
|
Self::parse_color(&self.error)
|
||||||
|
}
|
||||||
|
pub fn warning(&self) -> Color {
|
||||||
|
Self::parse_color(&self.warning)
|
||||||
|
}
|
||||||
|
pub fn success(&self) -> Color {
|
||||||
|
Self::parse_color(&self.success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user