diff --git a/assets/themes/farout.toml b/assets/themes/farout.toml new file mode 100644 index 0000000..2d97094 --- /dev/null +++ b/assets/themes/farout.toml @@ -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" diff --git a/assets/themes/gruvbox-darkest.toml b/assets/themes/gruvbox-darkest.toml new file mode 100644 index 0000000..ae14e3e --- /dev/null +++ b/assets/themes/gruvbox-darkest.toml @@ -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" diff --git a/assets/themes/kanagawa-dragon.toml b/assets/themes/kanagawa-dragon.toml new file mode 100644 index 0000000..87ef6f4 --- /dev/null +++ b/assets/themes/kanagawa-dragon.toml @@ -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" diff --git a/assets/themes/kanagawa-lotus.toml b/assets/themes/kanagawa-lotus.toml new file mode 100644 index 0000000..e36084c --- /dev/null +++ b/assets/themes/kanagawa-lotus.toml @@ -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" diff --git a/assets/themes/kanagawa-wave.toml b/assets/themes/kanagawa-wave.toml new file mode 100644 index 0000000..4527f12 --- /dev/null +++ b/assets/themes/kanagawa-wave.toml @@ -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" diff --git a/assets/themes/terminal-default.toml b/assets/themes/terminal-default.toml new file mode 100644 index 0000000..cc2d413 --- /dev/null +++ b/assets/themes/terminal-default.toml @@ -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" diff --git a/src/app.rs b/src/app.rs index 28610d5..9326fb6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,14 +1,15 @@ use std::collections::HashSet; use std::time::Instant; -use rand::rngs::SmallRng; use rand::SeedableRng; +use rand::rngs::SmallRng; use crate::config::Config; use crate::engine::filter::CharFilter; use crate::engine::key_stats::KeyStatsStore; use crate::engine::scoring; use crate::engine::skill_tree::{BranchId, BranchStatus, DrillScope, SkillTree}; +use crate::generator::TextGenerator; use crate::generator::capitalize; use crate::generator::code_patterns; use crate::generator::code_syntax::CodeSyntaxGenerator; @@ -17,14 +18,13 @@ use crate::generator::numbers; use crate::generator::passage::PassageGenerator; use crate::generator::phonetic::PhoneticGenerator; use crate::generator::punctuate; -use crate::generator::TextGenerator; use crate::generator::transition_table::TransitionTable; -use crate::session::input::{self, KeystrokeEvent}; use crate::session::drill::DrillState; +use crate::session::input::{self, KeystrokeEvent}; use crate::session::result::DrillResult; 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::theme::Theme; @@ -182,7 +182,8 @@ impl App { let focused = self.skill_tree.focused_key(scope, &self.key_stats); // Generate base lowercase text using only lowercase keys from scope - let lowercase_keys: Vec = all_keys.iter() + let lowercase_keys: Vec = all_keys + .iter() .copied() .filter(|ch| ch.is_ascii_lowercase() || *ch == ' ') .collect(); @@ -196,7 +197,8 @@ impl App { let mut text = generator.generate(&filter, lowercase_focused, word_count); // Apply capitalization if uppercase keys are in scope - let cap_keys: Vec = all_keys.iter() + let cap_keys: Vec = all_keys + .iter() .copied() .filter(|ch| ch.is_ascii_uppercase()) .collect(); @@ -206,9 +208,15 @@ impl App { } // Apply punctuation if punctuation keys are in scope - let punct_keys: Vec = all_keys.iter() + let punct_keys: Vec = all_keys + .iter() .copied() - .filter(|ch| matches!(ch, '.' | ',' | '\'' | ';' | ':' | '"' | '-' | '?' | '!' | '(' | ')')) + .filter(|ch| { + matches!( + ch, + '.' | ',' | '\'' | ';' | ':' | '"' | '-' | '?' | '!' | '(' | ')' + ) + }) .collect(); if !punct_keys.is_empty() { let mut rng = SmallRng::from_rng(&mut self.rng).unwrap(); @@ -216,7 +224,8 @@ impl App { } // Apply numbers if digit keys are in scope - let digit_keys: Vec = all_keys.iter() + let digit_keys: Vec = all_keys + .iter() .copied() .filter(|ch| ch.is_ascii_digit()) .collect(); @@ -236,16 +245,44 @@ impl App { ), }; if code_active { - let symbol_keys: Vec = all_keys.iter() + let symbol_keys: Vec = all_keys + .iter() .copied() - .filter(|ch| matches!(ch, - '=' | '+' | '*' | '/' | '-' | '{' | '}' | '[' | ']' | '<' | '>' | - '&' | '|' | '^' | '~' | '@' | '#' | '$' | '%' | '_' | '\\' | '`' - )) + .filter(|ch| { + matches!( + ch, + '=' | '+' + | '*' + | '/' + | '-' + | '{' + | '}' + | '[' + | ']' + | '<' + | '>' + | '&' + | '|' + | '^' + | '~' + | '@' + | '#' + | '$' + | '%' + | '_' + | '\\' + | '`' + ) + }) .collect(); if !symbol_keys.is_empty() { 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) { if let Some(ref drill) = self.drill { 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 { for kt in &result.per_key_times { @@ -329,8 +371,7 @@ impl App { } else { self.profile.streak_days = 1; } - self.profile.best_streak = - self.profile.best_streak.max(self.profile.streak_days); + self.profile.best_streak = self.profile.best_streak.max(self.profile.streak_days); self.profile.last_practice_date = Some(today); } @@ -447,8 +488,7 @@ impl App { } else { self.profile.streak_days = 1; } - self.profile.best_streak = - self.profile.best_streak.max(self.profile.streak_days); + self.profile.best_streak = self.profile.best_streak.max(self.profile.streak_days); self.profile.last_practice_date = Some(day); } } diff --git a/src/config.rs b/src/config.rs index 508d7a4..dbb97d6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,7 +22,7 @@ fn default_target_wpm() -> u32 { 35 } fn default_theme() -> String { - "catppuccin-mocha".to_string() + "terminal-default".to_string() } fn default_keyboard_layout() -> String { "qwerty".to_string() diff --git a/src/engine/filter.rs b/src/engine/filter.rs index 5e158d4..a36cd00 100644 --- a/src/engine/filter.rs +++ b/src/engine/filter.rs @@ -13,8 +13,6 @@ impl CharFilter { #[allow(dead_code)] pub fn filter_text(&self, text: &str) -> String { - text.chars() - .filter(|&ch| self.is_allowed(ch)) - .collect() + text.chars().filter(|&ch| self.is_allowed(ch)).collect() } } diff --git a/src/engine/key_stats.rs b/src/engine/key_stats.rs index fb5210a..63c60de 100644 --- a/src/engine/key_stats.rs +++ b/src/engine/key_stats.rs @@ -48,8 +48,7 @@ impl KeyStatsStore { if stat.sample_count == 1 { stat.filtered_time_ms = time_ms; } else { - stat.filtered_time_ms = - EMA_ALPHA * time_ms + (1.0 - EMA_ALPHA) * stat.filtered_time_ms; + 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); @@ -64,10 +63,7 @@ impl KeyStatsStore { } pub fn get_confidence(&self, key: char) -> f64 { - self.stats - .get(&key) - .map(|s| s.confidence) - .unwrap_or(0.0) + self.stats.get(&key).map(|s| s.confidence).unwrap_or(0.0) } #[allow(dead_code)] @@ -104,7 +100,10 @@ mod tests { let conf = store.get_confidence('t'); // At 175 CPM target, target_time = 60000/175 = 342.8ms // 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] @@ -115,6 +114,9 @@ mod tests { } let conf = store.get_confidence('a'); // 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}" + ); } } diff --git a/src/engine/skill_tree.rs b/src/engine/skill_tree.rs index b452416..f872801 100644 --- a/src/engine/skill_tree.rs +++ b/src/engine/skill_tree.rs @@ -78,8 +78,8 @@ pub struct BranchDefinition { const LOWERCASE_LEVELS: &[LevelDefinition] = &[LevelDefinition { name: "Frequency Order", keys: &[ - 'e', 't', 'a', 'o', 'i', 'n', 's', 'h', 'r', 'd', 'l', 'c', 'u', 'm', 'w', 'f', 'g', - 'y', 'p', 'b', 'v', 'k', 'j', 'x', 'q', 'z', + 'e', 't', 'a', 'o', 'i', 'n', 's', 'h', 'r', 'd', 'l', 'c', 'u', 'm', 'w', 'f', 'g', 'y', + 'p', 'b', 'v', 'k', 'j', 'x', 'q', 'z', ], }]; @@ -293,10 +293,7 @@ impl SkillTree { status: BranchStatus::Locked, current_level: 0, }; - self.progress - .branches - .get(id.to_key()) - .unwrap_or(&DEFAULT) + self.progress.branches.get(id.to_key()).unwrap_or(&DEFAULT) } pub fn branch_progress_mut(&mut self, id: BranchId) -> &mut BranchProgress { @@ -663,8 +660,14 @@ mod tests { #[test] fn test_initial_state() { let tree = SkillTree::default(); - assert_eq!(*tree.branch_status(BranchId::Lowercase), BranchStatus::InProgress); - assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::Locked); + assert_eq!( + *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); } @@ -711,12 +714,30 @@ mod tests { tree.update(&stats); } - assert_eq!(*tree.branch_status(BranchId::Lowercase), BranchStatus::Complete); - assert_eq!(*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); + assert_eq!( + *tree.branch_status(BranchId::Lowercase), + BranchStatus::Complete + ); + assert_eq!( + *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] @@ -726,7 +747,10 @@ mod tests { tree.branch_progress_mut(BranchId::Capitals).status = BranchStatus::Available; 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); } @@ -745,7 +769,10 @@ mod tests { tree.update(&stats); 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] @@ -766,7 +793,10 @@ mod tests { tree.update(&stats); } - assert_eq!(*tree.branch_status(BranchId::Capitals), BranchStatus::Complete); + assert_eq!( + *tree.branch_status(BranchId::Capitals), + BranchStatus::Complete + ); } #[test] diff --git a/src/event.rs b/src/event.rs index 2be4f6f..6e44af2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -20,23 +20,25 @@ impl EventHandler { let (tx, rx) = mpsc::channel(); let _tx = tx.clone(); - thread::spawn(move || loop { - if event::poll(tick_rate).unwrap_or(false) { - match event::read() { - Ok(Event::Key(key)) => { - if tx.send(AppEvent::Key(key)).is_err() { - return; + thread::spawn(move || { + loop { + if event::poll(tick_rate).unwrap_or(false) { + match event::read() { + Ok(Event::Key(key)) => { + if tx.send(AppEvent::Key(key)).is_err() { + return; + } } - } - Ok(Event::Resize(w, h)) => { - if tx.send(AppEvent::Resize(w, h)).is_err() { - return; + Ok(Event::Resize(w, h)) => { + if tx.send(AppEvent::Resize(w, h)).is_err() { + return; + } } + _ => {} } - _ => {} + } else if tx.send(AppEvent::Tick).is_err() { + return; } - } else if tx.send(AppEvent::Tick).is_err() { - return; } }); diff --git a/src/generator/cache.rs b/src/generator/cache.rs index 8eb973d..7cd917f 100644 --- a/src/generator/cache.rs +++ b/src/generator/cache.rs @@ -24,7 +24,13 @@ impl DiskCache { fn sanitize_key(key: &str) -> String { 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() } } diff --git a/src/generator/capitalize.rs b/src/generator/capitalize.rs index df981c6..9dafad4 100644 --- a/src/generator/capitalize.rs +++ b/src/generator/capitalize.rs @@ -1,5 +1,5 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; /// Post-processing pass that capitalizes words in generated text. /// 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 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 { let upper = ch.to_ascii_uppercase(); 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) { result.push(upper); continue; diff --git a/src/generator/code_patterns.rs b/src/generator/code_patterns.rs index e6029ca..98ce750 100644 --- a/src/generator/code_patterns.rs +++ b/src/generator/code_patterns.rs @@ -1,5 +1,5 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; /// Post-processing pass that inserts code-like expressions into text. /// Only uses symbols from `unlocked_symbols`. @@ -51,35 +51,47 @@ fn generate_code_expr( let w = word.to_string(); let idx = patterns.len(); 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('+') { let w = word.to_string(); let idx = patterns.len(); 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('*') { let w = word.to_string(); let idx = patterns.len(); 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('/') { let w = word.to_string(); let idx = patterns.len(); 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('-') { let w = word.to_string(); let idx = patterns.len(); 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 idx = patterns.len(); 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('+') { let w = word.to_string(); @@ -89,7 +101,9 @@ fn generate_code_expr( let w = word.to_string(); let idx = patterns.len(); 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('=') { let w = word.to_string(); @@ -101,19 +115,25 @@ fn generate_code_expr( let w = word.to_string(); let idx = patterns.len(); 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(']') { let w = word.to_string(); let idx = patterns.len(); 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('>') { let w = word.to_string(); let idx = patterns.len(); 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 @@ -121,19 +141,25 @@ fn generate_code_expr( let w = word.to_string(); let idx = patterns.len(); 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('|') { let w = word.to_string(); let idx = patterns.len(); 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('!') { let w = word.to_string(); let idx = patterns.len(); 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 @@ -141,31 +167,41 @@ fn generate_code_expr( let w = word.to_string(); let idx = patterns.len(); 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('#') { let w = word.to_string(); let idx = patterns.len(); 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('_') { let w = word.to_string(); let idx = patterns.len(); 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('$') { let w = word.to_string(); let idx = patterns.len(); 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('\\') { let w = word.to_string(); let idx = patterns.len(); 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() { diff --git a/src/generator/code_syntax.rs b/src/generator/code_syntax.rs index 66c0204..4b4c26f 100644 --- a/src/generator/code_syntax.rs +++ b/src/generator/code_syntax.rs @@ -1,9 +1,9 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; use crate::engine::filter::CharFilter; -use crate::generator::cache::{DiskCache, fetch_url}; use crate::generator::TextGenerator; +use crate::generator::cache::{DiskCache, fetch_url}; pub struct CodeSyntaxGenerator { rng: SmallRng, @@ -49,9 +49,7 @@ impl CodeSyntaxGenerator { "https://raw.githubusercontent.com/lodash/lodash/main/src/chunk.ts", "https://raw.githubusercontent.com/expressjs/express/master/lib/router/index.js", ], - "go" => vec![ - "https://raw.githubusercontent.com/golang/go/master/src/fmt/print.go", - ], + "go" => vec!["https://raw.githubusercontent.com/golang/go/master/src/fmt/print.go"], _ => vec![], }; diff --git a/src/generator/dictionary.rs b/src/generator/dictionary.rs index 303a7e8..89de0e5 100644 --- a/src/generator/dictionary.rs +++ b/src/generator/dictionary.rs @@ -23,11 +23,7 @@ impl Dictionary { self.words.clone() } - pub fn find_matching( - &self, - filter: &CharFilter, - focused: Option, - ) -> Vec<&str> { + pub fn find_matching(&self, filter: &CharFilter, focused: Option) -> Vec<&str> { let mut matching: Vec<&str> = self .words .iter() diff --git a/src/generator/mod.rs b/src/generator/mod.rs index d155b48..6f2d97b 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -14,5 +14,5 @@ use crate::engine::filter::CharFilter; pub trait TextGenerator { fn generate(&mut self, filter: &CharFilter, focused: Option, word_count: usize) - -> String; + -> String; } diff --git a/src/generator/numbers.rs b/src/generator/numbers.rs index 4f02b5f..8237a94 100644 --- a/src/generator/numbers.rs +++ b/src/generator/numbers.rs @@ -1,5 +1,5 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; /// Post-processing pass that inserts number expressions into text. /// Only uses digits from `unlocked_digits`. @@ -127,6 +127,9 @@ mod tests { 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 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}" + ); } } diff --git a/src/generator/passage.rs b/src/generator/passage.rs index a0acabd..5b87f4e 100644 --- a/src/generator/passage.rs +++ b/src/generator/passage.rs @@ -1,9 +1,9 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; use crate::engine::filter::CharFilter; -use crate::generator::cache::{DiskCache, fetch_url}; use crate::generator::TextGenerator; +use crate::generator::cache::{DiskCache, fetch_url}; const PASSAGES: &[&str] = &[ // Classic literature & speeches @@ -217,7 +217,9 @@ fn extract_paragraphs(text: &str) -> Vec { .collect::>() .join(" ") .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::() .to_lowercase(); diff --git a/src/generator/phonetic.rs b/src/generator/phonetic.rs index a062132..b15d0ed 100644 --- a/src/generator/phonetic.rs +++ b/src/generator/phonetic.rs @@ -1,10 +1,10 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; use crate::engine::filter::CharFilter; +use crate::generator::TextGenerator; use crate::generator::dictionary::Dictionary; use crate::generator::transition_table::TransitionTable; -use crate::generator::TextGenerator; const MIN_WORD_LEN: usize = 3; const MAX_WORD_LEN: usize = 10; @@ -149,7 +149,8 @@ impl PhoneticGenerator { if space_weight > 0.0 { let boost = 1.3f64.powi(word.len() as i32 - MIN_WORD_LEN as i32); 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)) { break; } @@ -164,11 +165,8 @@ impl PhoneticGenerator { // Get next character from transition table if let Some(probs) = self.table.segment(&prefix) { - let non_space: Vec<(char, f64)> = probs - .iter() - .filter(|(ch, _)| *ch != ' ') - .copied() - .collect(); + let non_space: Vec<(char, f64)> = + probs.iter().filter(|(ch, _)| *ch != ' ').copied().collect(); if let Some(next) = Self::pick_weighted_from(&mut self.rng, &non_space, filter) { word.push(next); } else { diff --git a/src/generator/punctuate.rs b/src/generator/punctuate.rs index 54e096e..e8486c8 100644 --- a/src/generator/punctuate.rs +++ b/src/generator/punctuate.rs @@ -1,5 +1,5 @@ -use rand::rngs::SmallRng; use rand::Rng; +use rand::rngs::SmallRng; /// Post-processing pass that inserts punctuation into generated text. /// Only uses punctuation chars from `unlocked_punct`. @@ -41,25 +41,41 @@ pub fn apply_punctuation( let mut w = word.to_string(); // 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) { w = make_contraction(&w, rng); } // 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) { w.push('-'); } // Sentence ending punctuation words_since_period += 1; - let end_sentence = words_since_period >= 8 && rng.gen_bool(0.15) - || words_since_period >= 12; + let end_sentence = + words_since_period >= 8 && rng.gen_bool(0.15) || words_since_period >= 12; if end_sentence && i < words.len() - 1 { - let q_prob = if focused_punct == Some('?') { 0.40 } else { 0.15 }; - let excl_prob = if focused_punct == Some('!') { 0.40 } else { 0.10 }; + let q_prob = if focused_punct == Some('?') { + 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) { w.push('?'); } else if has_exclaim && rng.gen_bool(excl_prob) { @@ -72,34 +88,62 @@ pub fn apply_punctuation( } else { // Comma after clause (~every 4-6 words) words_since_comma += 1; - let comma_prob = if focused_punct == Some(',') { 0.40 } else { 0.20 }; - if has_comma && words_since_comma >= 4 && rng.gen_bool(comma_prob) && i < words.len() - 1 { + let comma_prob = if focused_punct == Some(',') { + 0.40 + } else { + 0.20 + }; + if has_comma + && words_since_comma >= 4 + && rng.gen_bool(comma_prob) + && i < words.len() - 1 + { w.push(','); words_since_comma = 0; } // Semicolon between clauses (rare, boosted if focused) - let semi_prob = if focused_punct == Some(';') { 0.25 } else { 0.05 }; - if has_semicolon && words_since_comma >= 5 && rng.gen_bool(semi_prob) && i < words.len() - 1 { + let semi_prob = if focused_punct == Some(';') { + 0.25 + } else { + 0.05 + }; + if has_semicolon + && words_since_comma >= 5 + && rng.gen_bool(semi_prob) + && i < words.len() - 1 + { w.push(';'); words_since_comma = 0; } // 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 { w.push(':'); } } // 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() { w = format!("\"{w}"); } // 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() { w = format!("({w}"); } @@ -122,9 +166,15 @@ pub fn apply_punctuation( let mut open_parens = 0i32; for w in &result { for ch in w.chars() { - if ch == '"' { open_quotes += 1; } - if ch == '(' { open_parens += 1; } - if ch == ')' { open_parens -= 1; } + if ch == '"' { + open_quotes += 1; + } + if ch == '(' { + open_parens += 1; + } + if ch == ')' { + open_parens -= 1; + } } } if let Some(last) = result.last_mut() { diff --git a/src/generator/transition_table.rs b/src/generator/transition_table.rs index e6f059c..f5edbe6 100644 --- a/src/generator/transition_table.rs +++ b/src/generator/transition_table.rs @@ -108,24 +108,94 @@ impl TransitionTable { let mut table = Self::new(4); let common_patterns: &[(&str, f64)] = &[ - ("the", 10.0), ("and", 8.0), ("ing", 7.0), ("tion", 6.0), ("ent", 5.0), - ("ion", 5.0), ("her", 4.0), ("for", 4.0), ("are", 4.0), ("his", 4.0), - ("hat", 3.0), ("tha", 3.0), ("ere", 3.0), ("ate", 3.0), ("ith", 3.0), - ("ver", 3.0), ("all", 3.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), + ("the", 10.0), + ("and", 8.0), + ("ing", 7.0), + ("tion", 6.0), + ("ent", 5.0), + ("ion", 5.0), + ("her", 4.0), + ("for", 4.0), + ("are", 4.0), + ("his", 4.0), + ("hat", 3.0), + ("tha", 3.0), + ("ere", 3.0), + ("ate", 3.0), + ("ith", 3.0), + ("ver", 3.0), + ("all", 3.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 { @@ -142,8 +212,8 @@ impl TransitionTable { let vowels = ['a', 'e', 'i', 'o', 'u']; let consonants = [ - 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', - 'w', 'x', 'y', 'z', + 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'r', 's', 't', 'v', 'w', + 'x', 'y', 'z', ]; for &c in &consonants { diff --git a/src/main.rs b/src/main.rs index 95bba0a..070aca8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,28 +21,32 @@ use crossterm::execute; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, }; +use ratatui::Terminal; use ratatui::backend::CrosstermBackend; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget}; -use ratatui::Terminal; use app::{App, AppScreen, DrillMode}; use engine::skill_tree::DrillScope; -use session::result::DrillResult; -use ui::components::skill_tree::{SkillTreeWidget, selectable_branches}; use event::{AppEvent, EventHandler}; +use session::result::DrillResult; use ui::components::dashboard::Dashboard; use ui::components::keyboard_diagram::KeyboardDiagram; use ui::components::progress_bar::ProgressBar; +use ui::components::skill_tree::{SkillTreeWidget, selectable_branches}; use ui::components::stats_dashboard::StatsDashboard; use ui::components::stats_sidebar::StatsSidebar; use ui::components::typing_area::TypingArea; use ui::layout::AppLayout; #[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 { #[arg(short, long, help = "Theme name")] theme: Option, @@ -237,7 +241,12 @@ fn handle_drill_key(app: &mut App, key: KeyEvent) { if has_progress && app.drill_mode != DrillMode::Adaptive { // Non-adaptive: show result screen for partial 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.screen = AppScreen::DrillResult; @@ -283,8 +292,7 @@ fn handle_stats_key(app: &mut App, key: KeyEvent) { KeyCode::Char('j') | KeyCode::Down => { if !app.drill_history.is_empty() { let max_visible = app.drill_history.len().min(20) - 1; - app.history_selected = - (app.history_selected + 1).min(max_visible); + app.history_selected = (app.history_selected + 1).min(max_visible); } } 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::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 } + 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('3') => app.stats_tab = 2, 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 accuracy = drill.accuracy(); let errors = drill.typo_count(); - let header_text = format!( - " {mode_name} | WPM: {wpm:.0} | Acc: {accuracy:.1}% | Errors: {errors}" - ); + let header_text = + format!(" {mode_name} | WPM: {wpm:.0} | Acc: {accuracy:.1}% | Errors: {errors}"); let header = Paragraph::new(Line::from(Span::styled( &*header_text, 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 total = app.skill_tree.total_unique_keys as f64; let progress_val = (unlocked / total).min(1.0); - let progress = ProgressBar::new( - "Key Progress", - progress_val, - app.theme, - ); + let progress = ProgressBar::new("Key Progress", progress_val, app.theme); frame.render_widget(progress, main_layout[idx]); idx += 1; } @@ -550,7 +563,12 @@ fn render_drill(frame: &mut ratatui::Frame, app: &App) { } 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); } @@ -609,9 +627,15 @@ fn render_settings(frame: &mut ratatui::Frame, app: &App) { .unwrap_or("rust"); 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()), - ("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()), ]; @@ -633,7 +657,12 @@ fn render_settings(frame: &mut ratatui::Frame, app: &App) { let field_layout = Layout::default() .direction(Direction::Vertical) - .constraints(fields.iter().map(|_| Constraint::Length(3)).collect::>()) + .constraints( + fields + .iter() + .map(|_| Constraint::Length(3)) + .collect::>(), + ) .split(layout[1]); 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 value_text = format!(" < {value} >"); - let label_style = Style::default().fg(if is_selected { - colors.accent() - } else { - colors.fg() - }).add_modifier(if is_selected { Modifier::BOLD } else { Modifier::empty() }); + let label_style = Style::default() + .fg(if is_selected { + colors.accent() + } else { + colors.fg() + }) + .add_modifier(if is_selected { + Modifier::BOLD + } else { + Modifier::empty() + }); let value_style = Style::default().fg(if is_selected { colors.focused_key() diff --git a/src/session/mod.rs b/src/session/mod.rs index 2ea5241..1459c55 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -1,3 +1,3 @@ -pub mod input; pub mod drill; +pub mod input; pub mod result; diff --git a/src/session/result.rs b/src/session/result.rs index de51559..4b17cd4 100644 --- a/src/session/result.rs +++ b/src/session/result.rs @@ -1,8 +1,8 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::session::input::KeystrokeEvent; use crate::session::drill::DrillState; +use crate::session::input::KeystrokeEvent; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DrillResult { @@ -37,7 +37,12 @@ pub struct KeyTime { } 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 = events .windows(2) .map(|pair| { diff --git a/src/store/json_store.rs b/src/store/json_store.rs index ce974da..486ac0a 100644 --- a/src/store/json_store.rs +++ b/src/store/json_store.rs @@ -3,9 +3,9 @@ use std::io::Write; use std::path::PathBuf; 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 { base_dir: PathBuf, diff --git a/src/ui/components/activity_heatmap.rs b/src/ui/components/activity_heatmap.rs index ecc87cb..13d81f9 100644 --- a/src/ui/components/activity_heatmap.rs +++ b/src/ui/components/activity_heatmap.rs @@ -47,8 +47,8 @@ impl Widget for ActivityHeatmap<'_> { let weeks_to_show = weeks_to_show.min(26); let start_date = today - chrono::Duration::weeks(weeks_to_show as i64); // Align to Monday - let start_date = start_date - - chrono::Duration::days(start_date.weekday().num_days_from_monday() as i64); + let start_date = + start_date - chrono::Duration::days(start_date.weekday().num_days_from_monday() as i64); // Day-of-week labels let day_labels = ["M", " ", "W", " ", "F", " ", "S"]; diff --git a/src/ui/components/dashboard.rs b/src/ui/components/dashboard.rs index eccdca0..083ba12 100644 --- a/src/ui/components/dashboard.rs +++ b/src/ui/components/dashboard.rs @@ -54,8 +54,7 @@ impl Widget for Dashboard<'_> { Style::default().fg(colors.text_pending()), )); } - let title = Paragraph::new(Line::from(title_spans)) - .alignment(Alignment::Center); + let title = Paragraph::new(Line::from(title_spans)).alignment(Alignment::Center); title.render(layout[0], buf); let wpm_text = format!("{:.0} WPM", self.result.wpm); diff --git a/src/ui/components/keyboard_diagram.rs b/src/ui/components/keyboard_diagram.rs index 5410cc7..a42a3d5 100644 --- a/src/ui/components/keyboard_diagram.rs +++ b/src/ui/components/keyboard_diagram.rs @@ -91,11 +91,7 @@ impl Widget for KeyboardDiagram<'_> { return; } - let offsets: &[u16] = if self.compact { - &[0, 1, 3] - } else { - &[1, 3, 5] - }; + let offsets: &[u16] = if self.compact { &[0, 1, 3] } else { &[1, 3, 5] }; for (row_idx, row) in ROWS.iter().enumerate() { let y = inner.y + row_idx as u16; @@ -128,21 +124,13 @@ impl Widget for KeyboardDiagram<'_> { .bg(bg) .add_modifier(Modifier::BOLD) } else if is_next { - Style::default() - .fg(colors.bg()) - .bg(colors.accent()) + Style::default().fg(colors.bg()).bg(colors.accent()) } else if is_focused { - Style::default() - .fg(colors.bg()) - .bg(colors.focused_key()) + Style::default().fg(colors.bg()).bg(colors.focused_key()) } else if is_unlocked { - Style::default() - .fg(colors.fg()) - .bg(finger_color(key)) + Style::default().fg(colors.fg()).bg(finger_color(key)) } else { - Style::default() - .fg(colors.text_pending()) - .bg(colors.bg()) + Style::default().fg(colors.text_pending()).bg(colors.bg()) }; let display = if self.compact { diff --git a/src/ui/components/menu.rs b/src/ui/components/menu.rs index f162e0c..009325f 100644 --- a/src/ui/components/menu.rs +++ b/src/ui/components/menu.rs @@ -117,13 +117,29 @@ impl Widget for &Menu<'_> { .collect::>(), ) .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() { let is_selected = i == self.selected; let indicator = if is_selected { ">" } else { " " }; - let label_text = format!(" {indicator} [{key}] {label}", key = item.key, label = item.label); - let desc_text = format!(" {}", item.description); + let label_text = format!( + " {indicator} [{key: { let bp = self.skill_tree.branch_progress(branches[self.selected]); if *self.skill_tree.branch_status(branches[self.selected]) == BranchStatus::Locked { " 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 " } else { " [\u{2191}\u{2193}/jk] Navigate [q] Back " @@ -113,22 +113,29 @@ impl SkillTreeWidget<'_> { // Root: Lowercase a-z let lowercase_bp = self.skill_tree.branch_progress(BranchId::Lowercase); let lowercase_def = get_branch_definition(BranchId::Lowercase); - let lowercase_total = lowercase_def.levels.iter().map(|l| l.keys.len()).sum::(); - let lowercase_confident = self.skill_tree.branch_confident_keys(BranchId::Lowercase, self.key_stats); + let lowercase_total = lowercase_def + .levels + .iter() + .map(|l| l.keys.len()) + .sum::(); + let lowercase_confident = self + .skill_tree + .branch_confident_keys(BranchId::Lowercase, self.key_stats); let (prefix, style) = match lowercase_bp.status { BranchStatus::Complete => ( "\u{2605} ", - Style::default().fg(colors.text_correct()).add_modifier(Modifier::BOLD), + Style::default() + .fg(colors.text_correct()) + .add_modifier(Modifier::BOLD), ), BranchStatus::InProgress => ( "\u{25b6} ", - Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD), - ), - _ => ( - " ", - Style::default().fg(colors.text_pending()), + Style::default() + .fg(colors.accent()) + .add_modifier(Modifier::BOLD), ), + _ => (" ", Style::default().fg(colors.text_pending())), }; let status_text = match lowercase_bp.status { @@ -173,43 +180,56 @@ impl SkillTreeWidget<'_> { let bp = self.skill_tree.branch_progress(branch_id); let def = get_branch_definition(branch_id); let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::(); - 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 (prefix, style) = match bp.status { BranchStatus::Complete => ( "\u{2605} ", 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 { - Style::default().fg(colors.text_correct()).add_modifier(Modifier::BOLD) + Style::default() + .fg(colors.text_correct()) + .add_modifier(Modifier::BOLD) }, ), BranchStatus::InProgress => ( "\u{25b6} ", 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 { - Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD) + Style::default() + .fg(colors.accent()) + .add_modifier(Modifier::BOLD) }, ), BranchStatus::Available => ( " ", 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 { Style::default().fg(colors.fg()) }, ), - BranchStatus::Locked => ( - " ", - Style::default().fg(colors.text_pending()), - ), + BranchStatus::Locked => (" ", Style::default().fg(colors.text_pending())), }; let status_text = match bp.status { 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::Locked => format!("Locked 0/{total_keys} keys"), }; @@ -218,7 +238,10 @@ impl SkillTreeWidget<'_> { lines.push(Line::from(vec![ 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 { @@ -251,14 +274,18 @@ impl SkillTreeWidget<'_> { // Branch title with level info 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()), _ => format!("Level 0/{}", def.levels.len()), }; lines.push(Line::from(vec![ Span::styled( format!(" {}", def.name), - Style::default().fg(colors.accent()).add_modifier(Modifier::BOLD), + Style::default() + .fg(colors.accent()) + .add_modifier(Modifier::BOLD), ), Span::styled( format!(" {level_text}"), @@ -267,16 +294,19 @@ impl SkillTreeWidget<'_> { ])); // 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() { - let level_status = if bp.status == BranchStatus::Complete || level_idx < bp.current_level { - "complete" - } else if bp.status == BranchStatus::InProgress && level_idx == bp.current_level { - "in progress" - } else { - "locked" - }; + let level_status = + if bp.status == BranchStatus::Complete || level_idx < bp.current_level { + "complete" + } else if bp.status == BranchStatus::InProgress && level_idx == bp.current_level { + "in progress" + } else { + "locked" + }; let mut key_spans: Vec = Vec::new(); key_spans.push(Span::styled( @@ -324,7 +354,9 @@ impl SkillTreeWidget<'_> { // Average confidence let total_keys = def.levels.iter().map(|l| l.keys.len()).sum::(); 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()) .map(|&ch| self.key_stats.get_confidence(ch).min(1.0)) .sum(); @@ -334,7 +366,11 @@ impl SkillTreeWidget<'_> { }; 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()), ))); @@ -346,9 +382,5 @@ impl SkillTreeWidget<'_> { fn progress_bar_str(pct: f64, width: usize) -> String { let filled = (pct * width as f64).round() as usize; let empty = width.saturating_sub(filled); - format!( - "{}{}", - "\u{2588}".repeat(filled), - "\u{2591}".repeat(empty), - ) + format!("{}{}", "\u{2588}".repeat(filled), "\u{2591}".repeat(empty),) } diff --git a/src/ui/components/stats_dashboard.rs b/src/ui/components/stats_dashboard.rs index 326c63c..322a10b 100644 --- a/src/ui/components/stats_dashboard.rs +++ b/src/ui/components/stats_dashboard.rs @@ -83,10 +83,7 @@ impl Widget for StatsDashboard<'_> { } else { Style::default().fg(colors.text_pending()) }; - vec![ - Span::styled(format!(" {label} "), style), - Span::raw(" "), - ] + vec![Span::styled(format!(" {label} "), style), Span::raw(" ")] }) .collect(); Paragraph::new(Line::from(tab_spans)).render(layout[0], buf); @@ -168,18 +165,13 @@ impl StatsDashboard<'_> { .constraints([ Constraint::Length(6), // summary stats bordered box Constraint::Length(3), // progress bars - Constraint::Min(8), // charts + Constraint::Min(8), // charts ]) .split(area); // Summary stats as bordered table - let avg_wpm = - self.history.iter().map(|r| r.wpm).sum::() / self.history.len() as f64; - let best_wpm = self - .history - .iter() - .map(|r| r.wpm) - .fold(0.0f64, f64::max); + let avg_wpm = self.history.iter().map(|r| r.wpm).sum::() / self.history.len() as f64; + let best_wpm = self.history.iter().map(|r| r.wpm).fold(0.0f64, f64::max); let avg_accuracy = self.history.iter().map(|r| r.accuracy).sum::() / self.history.len() as f64; 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) let max_label = format!("{:.0}", max_wpm); 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 { 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 = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; @@ -342,7 +349,12 @@ impl StatsDashboard<'_> { // WPM label on top row if bar_spacing >= 3 { 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); - let avg_wpm = - self.history.iter().map(|r| r.wpm).sum::() / self.history.len() as f64; + let avg_wpm = self.history.iter().map(|r| r.wpm).sum::() / self.history.len() as f64; let avg_accuracy = self.history.iter().map(|r| r.accuracy).sum::() / self.history.len() as f64; @@ -454,7 +465,11 @@ impl StatsDashboard<'_> { ); // 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 next_level_score = ((level + 1) 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 { - "" - } else { - " (unranked)" - }; + let mode_str = if result.ranked { "" } else { " (unranked)" }; let row = format!( " {wpm_indicator}{idx_str} {wpm_str} {raw_str} {acc_str} {time_str:>6} {date_str} {mode}{mode_str}", mode = result.drill_mode, @@ -656,7 +667,7 @@ impl StatsDashboard<'_> { .constraints([ Constraint::Length(12), // Activity heatmap Constraint::Length(7), // Keyboard accuracy heatmap - Constraint::Min(5), // Slowest/Fastest/Stats + Constraint::Min(5), // Slowest/Fastest/Stats Constraint::Length(5), // Overall stats ]) .split(area); @@ -738,12 +749,7 @@ impl StatsDashboard<'_> { } else { format!("{key} ") }; - buf.set_string( - x, - y, - &display, - Style::default().fg(fg_color).bg(bg_color), - ); + buf.set_string(x, y, &display, Style::default().fg(fg_color).bg(bg_color)); } } } @@ -793,12 +799,7 @@ impl StatsDashboard<'_> { break; } let text = format!(" '{ch}' {time:.0}ms"); - buf.set_string( - inner.x, - y, - &text, - Style::default().fg(colors.error()), - ); + buf.set_string(inner.x, y, &text, Style::default().fg(colors.error())); } } @@ -826,12 +827,7 @@ impl StatsDashboard<'_> { break; } let text = format!(" '{ch}' {time:.0}ms"); - buf.set_string( - inner.x, - y, - &text, - Style::default().fg(colors.success()), - ); + buf.set_string(inner.x, y, &text, Style::default().fg(colors.success())); } } @@ -953,12 +949,7 @@ fn render_text_bar( } // Label on first line - buf.set_string( - area.x, - area.y, - label, - Style::default().fg(fill_color), - ); + buf.set_string(area.x, area.y, label, Style::default().fg(fill_color)); // Bar on second line using ┃ filled / dim ┃ empty let bar_width = (area.width as usize).saturating_sub(4); diff --git a/src/ui/components/stats_sidebar.rs b/src/ui/components/stats_sidebar.rs index 569227a..5ff6e54 100644 --- a/src/ui/components/stats_sidebar.rs +++ b/src/ui/components/stats_sidebar.rs @@ -22,7 +22,12 @@ impl<'a> StatsSidebar<'a> { history: &'a [DrillResult], theme: &'a Theme, ) -> Self { - Self { drill, last_result, history, theme } + Self { + drill, + last_result, + history, + theme, + } } } @@ -159,12 +164,10 @@ impl Widget for StatsSidebar<'_> { colors.text_pending() }; - let mut lines = vec![ - Line::from(vec![ - Span::styled("WPM: ", Style::default().fg(colors.fg())), - Span::styled(wpm_str, Style::default().fg(colors.accent())), - ]), - ]; + let mut lines = vec![Line::from(vec![ + Span::styled("WPM: ", Style::default().fg(colors.fg())), + Span::styled(wpm_str, Style::default().fg(colors.accent())), + ])]; if prior_count > 0 { lines.push(Line::from(vec![ diff --git a/src/ui/components/typing_area.rs b/src/ui/components/typing_area.rs index dcc553b..af20498 100644 --- a/src/ui/components/typing_area.rs +++ b/src/ui/components/typing_area.rs @@ -4,8 +4,8 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Paragraph, Widget, Wrap}; -use crate::session::input::CharStatus; use crate::session::drill::DrillState; +use crate::session::input::CharStatus; use crate::ui::theme::Theme; pub struct TypingArea<'a> { @@ -78,6 +78,7 @@ impl Widget for TypingArea<'_> { for token in &tokens { let idx = token.target_idx; + let target_ch = self.drill.target[idx]; let style = if idx < self.drill.cursor { match &self.drill.input[idx] { @@ -91,6 +92,7 @@ impl Widget for TypingArea<'_> { Style::default() .fg(colors.text_cursor_fg()) .bg(colors.text_cursor_bg()) + .add_modifier(Modifier::REVERSED | Modifier::BOLD) } else { Style::default().fg(colors.text_pending()) }; @@ -99,7 +101,6 @@ impl Widget for TypingArea<'_> { // but always show the token display for whitespace markers let display = if idx < self.drill.cursor { if let CharStatus::Incorrect(actual) = &self.drill.input[idx] { - let target_ch = self.drill.target[idx]; if target_ch == '\n' || target_ch == '\t' { // Show the whitespace marker even when incorrect token.display.clone() @@ -109,6 +110,8 @@ impl Widget for TypingArea<'_> { } else { token.display.clone() } + } else if idx == self.drill.cursor && target_ch == ' ' { + "\u{00b7}".to_string() } else { 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 = lines.into_iter().map(Line::from).collect(); let block = Block::bordered() diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 1070b0f..cd40af3 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; #[folder = "assets/themes/"] struct ThemeAssets; +const TERMINAL_DEFAULT_THEME: &str = "terminal-default"; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Theme { pub name: String, @@ -42,7 +44,10 @@ impl Theme { pub fn load(name: &str) -> Option { // Try user themes 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(theme) = toml::from_str::(&content) { return Some(theme); @@ -64,17 +69,20 @@ impl Theme { } pub fn available_themes() -> Vec { - ThemeAssets::iter() - .filter_map(|f| { - f.strip_suffix(".toml").map(|n| n.to_string()) - }) - .collect() + let mut themes: Vec = ThemeAssets::iter() + .filter_map(|f| f.strip_suffix(".toml").map(|n| n.to_string())) + .collect(); + themes.sort_unstable(); + if let Some(idx) = themes.iter().position(|t| t == TERMINAL_DEFAULT_THEME) { + themes.swap(0, idx); + } + themes } } impl Default for Theme { fn default() -> Self { - Self::load("catppuccin-mocha").unwrap_or_else(|| Self { + Self::load(TERMINAL_DEFAULT_THEME).unwrap_or_else(|| Self { name: "default".to_string(), colors: ThemeColors::default(), }) @@ -109,9 +117,10 @@ impl Default for ThemeColors { } impl ThemeColors { - pub fn parse_color(hex: &str) -> Color { - let hex = hex.trim_start_matches('#'); - if hex.len() == 6 { + pub fn parse_color(value: &str) -> Color { + let value = value.trim(); + let hex = value.trim_start_matches('#'); + if hex.len() == 6 && value.starts_with('#') { if let (Ok(r), Ok(g), Ok(b)) = ( u8::from_str_radix(&hex[0..2], 16), u8::from_str_radix(&hex[2..4], 16), @@ -120,27 +129,87 @@ impl ThemeColors { 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 fg(&self) -> Color { Self::parse_color(&self.fg) } - 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 text_incorrect_bg(&self) -> Color { Self::parse_color(&self.text_incorrect_bg) } - 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_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) } + pub fn bg(&self) -> Color { + Self::parse_color(&self.bg) + } + pub fn fg(&self) -> Color { + Self::parse_color(&self.fg) + } + 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 text_incorrect_bg(&self) -> Color { + Self::parse_color(&self.text_incorrect_bg) + } + 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_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) + } }