Add more themes and rustfmt
This commit is contained in:
@@ -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"];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -117,13 +117,29 @@ impl Widget for &Menu<'_> {
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.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:<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![
|
||||
Line::from(Span::styled(
|
||||
|
||||
@@ -6,8 +6,7 @@ use ratatui::widgets::{Block, Paragraph, Widget};
|
||||
|
||||
use crate::engine::key_stats::KeyStatsStore;
|
||||
use crate::engine::skill_tree::{
|
||||
BranchId, BranchStatus, DrillScope, SkillTree as SkillTreeEngine,
|
||||
get_branch_definition,
|
||||
BranchId, BranchStatus, DrillScope, SkillTree as SkillTreeEngine, get_branch_definition,
|
||||
};
|
||||
use crate::ui::theme::Theme;
|
||||
|
||||
@@ -88,7 +87,8 @@ impl Widget for SkillTreeWidget<'_> {
|
||||
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::<usize>();
|
||||
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::<usize>();
|
||||
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::<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 (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<Span> = 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::<usize>();
|
||||
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),)
|
||||
}
|
||||
|
||||
@@ -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::<f64>() / 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::<f64>() / 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::<f64>() / 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::<f64>() / self.history.len() as f64;
|
||||
let avg_wpm = self.history.iter().map(|r| r.wpm).sum::<f64>() / self.history.len() as f64;
|
||||
let avg_accuracy =
|
||||
self.history.iter().map(|r| r.accuracy).sum::<f64>() / 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);
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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<Line> = lines.into_iter().map(Line::from).collect();
|
||||
|
||||
let block = Block::bordered()
|
||||
|
||||
Reference in New Issue
Block a user