Fix kitty protocol, caps lock, code source desc
This commit is contained in:
@@ -785,7 +785,7 @@ pub fn build_code_download_queue(lang_key: &str, cache_dir: &str) -> Vec<(String
|
||||
pub struct CodeSyntaxGenerator {
|
||||
rng: SmallRng,
|
||||
language: String,
|
||||
fetched_snippets: Vec<String>,
|
||||
fetched_snippets: Vec<(String, String)>, // (snippet, repo_key)
|
||||
last_source: String,
|
||||
}
|
||||
|
||||
@@ -816,11 +816,17 @@ impl CodeSyntaxGenerator {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
if name_str.starts_with(&prefix) && name_str.ends_with(".txt") {
|
||||
// Extract repo key from filename: {language}_{repo}.txt
|
||||
let repo_key = name_str
|
||||
.strip_prefix(&prefix)
|
||||
.and_then(|s| s.strip_suffix(".txt"))
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
if let Ok(content) = fs::read_to_string(entry.path()) {
|
||||
let snippets: Vec<String> = content
|
||||
let snippets: Vec<(String, String)> = content
|
||||
.split("\n---SNIPPET---\n")
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.map(|s| (s.to_string(), repo_key.clone()))
|
||||
.collect();
|
||||
self.fetched_snippets.extend(snippets);
|
||||
}
|
||||
@@ -1660,7 +1666,7 @@ impl TextGenerator for CodeSyntaxGenerator {
|
||||
candidates.push((false, i));
|
||||
}
|
||||
}
|
||||
for (i, snippet) in self.fetched_snippets.iter().enumerate() {
|
||||
for (i, (snippet, _)) in self.fetched_snippets.iter().enumerate() {
|
||||
if approx_token_count(snippet) >= min_units {
|
||||
candidates.push((true, i));
|
||||
}
|
||||
@@ -1681,22 +1687,29 @@ impl TextGenerator for CodeSyntaxGenerator {
|
||||
|
||||
let pick = self.rng.gen_range(0..candidates.len());
|
||||
let (is_fetched, idx) = candidates[pick];
|
||||
let used_fetched = is_fetched;
|
||||
|
||||
let selected = if is_fetched {
|
||||
self.fetched_snippets
|
||||
let display_name = CODE_LANGUAGES
|
||||
.iter()
|
||||
.find(|l| l.key == self.language)
|
||||
.map(|l| l.display_name)
|
||||
.unwrap_or(&self.language);
|
||||
|
||||
let (selected, repo_key) = if is_fetched {
|
||||
let (snippet, repo) = self
|
||||
.fetched_snippets
|
||||
.get(idx)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| embedded[0])
|
||||
.map(|(s, r)| (s.as_str(), Some(r.as_str())))
|
||||
.unwrap_or((embedded[0], None));
|
||||
(snippet, repo)
|
||||
} else {
|
||||
embedded.get(idx).copied().unwrap_or(embedded[0])
|
||||
(embedded.get(idx).copied().unwrap_or(embedded[0]), None)
|
||||
};
|
||||
let text = fit_snippet_to_target(selected, target_words);
|
||||
|
||||
self.last_source = if used_fetched {
|
||||
format!("GitHub source cache ({})", self.language)
|
||||
self.last_source = if let Some(repo) = repo_key {
|
||||
format!("{} \u{b7} {}", display_name, repo)
|
||||
} else {
|
||||
format!("Built-in snippets ({})", self.language)
|
||||
format!("{} \u{b7} built-in", display_name)
|
||||
};
|
||||
|
||||
text
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::keyboard::display::{BACKSPACE, ENTER, SPACE, TAB};
|
||||
use crate::keyboard::finger::{Finger, FingerAssignment, Hand};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -732,10 +733,18 @@ impl KeyboardModel {
|
||||
|
||||
/// Get finger assignment for a character, looking it up in the model.
|
||||
pub fn finger_for_char(&self, ch: char) -> FingerAssignment {
|
||||
if let Some((row_idx, col_idx)) = self.find_key_position(ch) {
|
||||
self.finger_for_position(row_idx, col_idx)
|
||||
} else {
|
||||
FingerAssignment::new(Hand::Right, Finger::Index)
|
||||
match ch {
|
||||
// Built-in/non-grid keys in this app.
|
||||
TAB => FingerAssignment::new(Hand::Left, Finger::Pinky),
|
||||
ENTER | BACKSPACE => FingerAssignment::new(Hand::Right, Finger::Pinky),
|
||||
SPACE => FingerAssignment::new(Hand::Right, Finger::Thumb),
|
||||
_ => {
|
||||
if let Some((row_idx, col_idx)) = self.find_key_position(ch) {
|
||||
self.finger_for_position(row_idx, col_idx)
|
||||
} else {
|
||||
FingerAssignment::new(Hand::Right, Finger::Index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,4 +825,25 @@ mod tests {
|
||||
let _ = model.finger_for_char('!');
|
||||
let _ = model.finger_for_char('{');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_finger_for_meta_keys() {
|
||||
let model = KeyboardModel::qwerty();
|
||||
assert_eq!(
|
||||
model.finger_for_char(TAB),
|
||||
FingerAssignment::new(Hand::Left, Finger::Pinky)
|
||||
);
|
||||
assert_eq!(
|
||||
model.finger_for_char(ENTER),
|
||||
FingerAssignment::new(Hand::Right, Finger::Pinky)
|
||||
);
|
||||
assert_eq!(
|
||||
model.finger_for_char(BACKSPACE),
|
||||
FingerAssignment::new(Hand::Right, Finger::Pinky)
|
||||
);
|
||||
assert_eq!(
|
||||
model.finger_for_char(SPACE),
|
||||
FingerAssignment::new(Hand::Right, Finger::Thumb)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
348
src/main.rs
348
src/main.rs
@@ -80,12 +80,22 @@ fn main() -> Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
|
||||
// Try to enable keyboard enhancement for Release event support
|
||||
// Request kitty keyboard protocol enhancements from the terminal.
|
||||
// - DISAMBIGUATE_ESCAPE_CODES: CSI u sequences for unambiguous key IDs,
|
||||
// enables CAPS_LOCK/NUM_LOCK state detection.
|
||||
// - REPORT_EVENT_TYPES: key release and repeat events (for depressed-key
|
||||
// tracking in the keyboard diagram).
|
||||
// - REPORT_ALL_KEYS_AS_ESCAPE_CODES: standalone modifier key events
|
||||
// (LeftShift, RightShift, etc.) so we can track shift state independently.
|
||||
// Falls back gracefully in terminals that don't support the protocol (tmux,
|
||||
// mosh, SSH, older emulators) — the execute! returns Err and we use
|
||||
// timer-based fallbacks instead.
|
||||
let keyboard_enhanced = execute!(
|
||||
io::stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
|
||||
| KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES,
|
||||
)
|
||||
)
|
||||
.is_ok();
|
||||
@@ -135,17 +145,25 @@ fn run_app(
|
||||
{
|
||||
app.process_code_download_tick();
|
||||
}
|
||||
// Fallback: clear depressed keys after 150ms if no Release event received
|
||||
// Fallback: clear depressed keys and shift state on a timer.
|
||||
// Needed because not all terminals send Release events (e.g.
|
||||
// WezTerm doesn't implement REPORT_EVENT_TYPES). Terminals that
|
||||
// DO send Release events handle cleanup in handle_key instead,
|
||||
// and the repeated Press events they send while a key is held
|
||||
// keep resetting last_key_time so this fallback never fires.
|
||||
// This causes a brief flicker (key clears, then re-appears when
|
||||
// OS key repeat kicks in after ~300-500ms), but that's an
|
||||
// acceptable tradeoff for responsive key press visualization.
|
||||
if let Some(last) = app.last_key_time {
|
||||
if last.elapsed() > Duration::from_millis(150) && !app.depressed_keys.is_empty()
|
||||
{
|
||||
app.depressed_keys.clear();
|
||||
if last.elapsed() > Duration::from_millis(150) {
|
||||
if !app.depressed_keys.is_empty() {
|
||||
app.depressed_keys.clear();
|
||||
}
|
||||
if app.shift_held {
|
||||
app.shift_held = false;
|
||||
}
|
||||
app.last_key_time = None;
|
||||
}
|
||||
// Clear shift_held after 200ms as fallback
|
||||
if last.elapsed() > Duration::from_millis(200) && app.shift_held {
|
||||
app.shift_held = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::Resize(_, _) => {}
|
||||
@@ -159,15 +177,35 @@ fn run_app(
|
||||
|
||||
fn handle_key(app: &mut App, key: KeyEvent) {
|
||||
// Track caps lock state via Kitty protocol metadata (KeyEventState::CAPS_LOCK).
|
||||
// This only works in terminals with native Kitty keyboard protocol support
|
||||
// (Kitty, WezTerm, foot, Ghostty). In tmux/mosh/SSH, the protocol is stripped
|
||||
// and crossterm infers SHIFT from character case, making it impossible to
|
||||
// distinguish Shift+a from CapsLock+a.
|
||||
app.caps_lock = key.state.contains(KeyEventState::CAPS_LOCK);
|
||||
// Only Modifier key events reliably report lock state in WezTerm; regular
|
||||
// character events have empty state (0x0). So we only set caps_lock=true
|
||||
// when CAPS_LOCK appears, and only clear it from Modifier events that
|
||||
// reliably report state.
|
||||
if key.state.contains(KeyEventState::CAPS_LOCK) {
|
||||
app.caps_lock = true;
|
||||
} else if matches!(key.code, KeyCode::Modifier(_) | KeyCode::CapsLock) {
|
||||
// Modifier events and CapsLock key events reliably report lock state.
|
||||
// If CAPS_LOCK isn't in state, caps lock was toggled off.
|
||||
app.caps_lock = false;
|
||||
}
|
||||
|
||||
// Determine whether the physical Shift key is held. When caps lock is on,
|
||||
// crossterm infers SHIFT from uppercase chars, so we need a heuristic:
|
||||
// caps lock + shift inverts case, producing lowercase. So if caps lock is
|
||||
// on and the char is uppercase, that's caps lock alone, not shift.
|
||||
let infer_shift = |ch: char, mods: KeyModifiers, caps: bool| -> bool {
|
||||
let has_shift = mods.contains(KeyModifiers::SHIFT);
|
||||
if caps && ch.is_ascii_alphabetic() {
|
||||
// Caps lock on: shift would invert to lowercase
|
||||
has_shift && ch.is_ascii_lowercase()
|
||||
} else {
|
||||
has_shift
|
||||
}
|
||||
};
|
||||
|
||||
// Track depressed keys and shift state for keyboard diagram
|
||||
match (&key.code, key.kind) {
|
||||
(KeyCode::Modifier(ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift), KeyEventKind::Press) => {
|
||||
(KeyCode::Modifier(ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift), KeyEventKind::Press | KeyEventKind::Repeat) => {
|
||||
app.shift_held = true;
|
||||
app.last_key_time = Some(Instant::now());
|
||||
return; // Don't dispatch bare shift presses to screen handlers
|
||||
@@ -179,7 +217,7 @@ fn handle_key(app: &mut App, key: KeyEvent) {
|
||||
(KeyCode::Char(ch), KeyEventKind::Press) => {
|
||||
app.depressed_keys.insert(ch.to_ascii_lowercase());
|
||||
app.last_key_time = Some(Instant::now());
|
||||
app.shift_held = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
app.shift_held = infer_shift(*ch, key.modifiers, app.caps_lock);
|
||||
}
|
||||
(KeyCode::Char(ch), KeyEventKind::Release) => {
|
||||
app.depressed_keys.remove(&ch.to_ascii_lowercase());
|
||||
@@ -1778,6 +1816,260 @@ mod review_tests {
|
||||
let result = next_available_path(custom_path.to_str().unwrap());
|
||||
assert!(result.ends_with("my-backup-2.json"));
|
||||
}
|
||||
|
||||
// --- Keyboard state tracking tests ---
|
||||
|
||||
/// Helper to build a KeyEvent with specific state flags.
|
||||
fn key_event_with_state(
|
||||
code: KeyCode,
|
||||
modifiers: KeyModifiers,
|
||||
kind: KeyEventKind,
|
||||
state: KeyEventState,
|
||||
) -> KeyEvent {
|
||||
KeyEvent {
|
||||
code,
|
||||
modifiers,
|
||||
kind,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_set_from_state_flag() {
|
||||
let mut app = App::new();
|
||||
assert!(!app.caps_lock);
|
||||
|
||||
// Modifier event with CAPS_LOCK in state turns it on
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::CAPS_LOCK,
|
||||
),
|
||||
);
|
||||
assert!(app.caps_lock);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_not_cleared_by_char_event_with_empty_state() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
|
||||
// Character event with empty state should NOT clear caps_lock
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('A'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(app.caps_lock, "char event with empty state must not clear caps_lock");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_cleared_by_modifier_event_without_caps_flag() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
|
||||
// Modifier event WITHOUT CAPS_LOCK in state clears it
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.caps_lock, "modifier event without CAPS_LOCK flag should clear caps_lock");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_on_uppercase_char_does_not_set_shift() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("ABC"));
|
||||
|
||||
// Caps lock on, typing 'A' — crossterm may report SHIFT modifier,
|
||||
// but this is caps lock, not physical shift
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('A'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.shift_held, "uppercase char with caps lock should not set shift_held");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_on_lowercase_char_with_shift_sets_shift() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
|
||||
// Caps lock on + shift held produces lowercase: shift IS held
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(app.shift_held, "lowercase char with caps+shift should set shift_held");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_off_uppercase_char_with_shift_sets_shift() {
|
||||
let mut app = App::new();
|
||||
assert!(!app.caps_lock);
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("ABC"));
|
||||
|
||||
// Normal shift+a = 'A', caps lock off
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('A'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(app.shift_held, "uppercase char without caps lock should set shift_held");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_off_lowercase_char_without_shift_clears_shift() {
|
||||
let mut app = App::new();
|
||||
app.shift_held = true;
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
|
||||
// Normal lowercase typing, no shift
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.shift_held, "lowercase char without shift should clear shift_held");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_modifier_press_sets_shift_held() {
|
||||
let mut app = App::new();
|
||||
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(app.shift_held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_modifier_release_clears_shift_held() {
|
||||
let mut app = App::new();
|
||||
app.shift_held = true;
|
||||
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightShift),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Release,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.shift_held);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depressed_keys_tracks_char_press() {
|
||||
let mut app = App::new();
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("abc"));
|
||||
|
||||
handle_key(&mut app, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
|
||||
assert!(app.depressed_keys.contains(&'a'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn depressed_keys_release_removes_char() {
|
||||
let mut app = App::new();
|
||||
app.depressed_keys.insert('a');
|
||||
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Release,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.depressed_keys.contains(&'a'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_cleared_by_capslock_key_without_caps_flag() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
|
||||
// Pressing CapsLock key to toggle off: event has KeyCode::CapsLock
|
||||
// but state no longer contains CAPS_LOCK
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::CapsLock,
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(!app.caps_lock, "CapsLock key event without CAPS_LOCK state should clear caps_lock");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_lock_non_alpha_char_with_shift_still_sets_shift() {
|
||||
let mut app = App::new();
|
||||
app.caps_lock = true;
|
||||
app.screen = AppScreen::Drill;
|
||||
app.drill = Some(crate::session::drill::DrillState::new("!@#"));
|
||||
|
||||
// Caps lock doesn't affect non-alpha chars like '!', so SHIFT
|
||||
// modifier should be trusted as-is
|
||||
handle_key(
|
||||
&mut app,
|
||||
key_event_with_state(
|
||||
KeyCode::Char('!'),
|
||||
KeyModifiers::SHIFT,
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NONE,
|
||||
),
|
||||
);
|
||||
assert!(app.shift_held, "non-alpha char with shift should set shift_held regardless of caps");
|
||||
}
|
||||
}
|
||||
|
||||
fn render_result(frame: &mut ratatui::Frame, app: &App) {
|
||||
@@ -3112,11 +3404,8 @@ fn render_keyboard_detail_panel(frame: &mut ratatui::Frame, app: &App, area: Rec
|
||||
"No data".to_string()
|
||||
};
|
||||
|
||||
let (branch_name, level_name) = if let Some((branch, level, pos)) = find_key_branch(selected) {
|
||||
(branch.name.to_string(), format!("{level} (key #{pos})"))
|
||||
} else {
|
||||
("Unknown".to_string(), "Unknown".to_string())
|
||||
};
|
||||
let branch_info = find_key_branch(selected)
|
||||
.map(|(branch, level, pos)| (branch.name.to_string(), format!("{level} (key #{pos})")));
|
||||
|
||||
// Ranked-only mastery display (same semantics as skill tree per-key progress)
|
||||
let ranked_conf = app.ranked_key_stats.get_confidence(selected).min(1.0);
|
||||
@@ -3138,12 +3427,15 @@ fn render_keyboard_detail_panel(frame: &mut ratatui::Frame, app: &App, area: Rec
|
||||
format!("Overall Accuracy: {}", fmt_acc(overall_acc)),
|
||||
];
|
||||
|
||||
let mut right_col: Vec<String> = vec![
|
||||
format!("Branch: {branch_name}"),
|
||||
format!("Level: {level_name}"),
|
||||
format!("Unlocked: {}", if is_unlocked { "Yes" } else { "No" }),
|
||||
format!("In Focus?: {}", if in_focus { "Yes" } else { "No" }),
|
||||
];
|
||||
let mut right_col: Vec<String> = Vec::new();
|
||||
if let Some((branch_name, level_name)) = branch_info {
|
||||
right_col.push(format!("Branch: {branch_name}"));
|
||||
right_col.push(format!("Level: {level_name}"));
|
||||
} else {
|
||||
right_col.push("Built-in Key".to_string());
|
||||
}
|
||||
right_col.push(format!("Unlocked: {}", if is_unlocked { "Yes" } else { "No" }));
|
||||
right_col.push(format!("In Focus?: {}", if in_focus { "Yes" } else { "No" }));
|
||||
if is_unlocked {
|
||||
right_col.push(format!("Mastery: {mastery_text}"));
|
||||
} else {
|
||||
|
||||
@@ -210,7 +210,13 @@ impl KeyboardDiagram<'_> {
|
||||
break;
|
||||
}
|
||||
|
||||
let display_char = if self.shift_held {
|
||||
// Caps lock inverts shift for alpha keys only
|
||||
let show_shifted = if physical_key.base.is_ascii_alphabetic() {
|
||||
self.shift_held ^ self.caps_lock
|
||||
} else {
|
||||
self.shift_held
|
||||
};
|
||||
let display_char = if show_shifted {
|
||||
physical_key.shifted
|
||||
} else {
|
||||
physical_key.base
|
||||
@@ -328,7 +334,13 @@ impl KeyboardDiagram<'_> {
|
||||
break;
|
||||
}
|
||||
|
||||
let display_char = if self.shift_held {
|
||||
// Caps lock inverts shift for alpha keys only
|
||||
let show_shifted = if physical_key.base.is_ascii_alphabetic() {
|
||||
self.shift_held ^ self.caps_lock
|
||||
} else {
|
||||
self.shift_held
|
||||
};
|
||||
let display_char = if show_shifted {
|
||||
physical_key.shifted
|
||||
} else {
|
||||
physical_key.base
|
||||
@@ -444,7 +456,13 @@ impl KeyboardDiagram<'_> {
|
||||
break;
|
||||
}
|
||||
|
||||
let display_char = if self.shift_held {
|
||||
// Caps lock inverts shift for alpha keys only
|
||||
let show_shifted = if physical_key.base.is_ascii_alphabetic() {
|
||||
self.shift_held ^ self.caps_lock
|
||||
} else {
|
||||
self.shift_held
|
||||
};
|
||||
let display_char = if show_shifted {
|
||||
physical_key.shifted
|
||||
} else {
|
||||
physical_key.base
|
||||
|
||||
@@ -6,7 +6,7 @@ use ratatui::widgets::{Block, Clear, Paragraph, Widget};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use crate::engine::key_stats::KeyStatsStore;
|
||||
use crate::keyboard::display::{self, BACKSPACE, ENTER, SPACE, TAB};
|
||||
use crate::keyboard::display::{self, BACKSPACE, ENTER, MODIFIER_SENTINELS, SPACE, TAB};
|
||||
use crate::keyboard::model::KeyboardModel;
|
||||
use crate::session::result::DrillResult;
|
||||
use crate::ui::components::activity_heatmap::ActivityHeatmap;
|
||||
@@ -1052,9 +1052,9 @@ impl StatsDashboard<'_> {
|
||||
}
|
||||
// Include modifier/whitespace keys
|
||||
all_keys.insert(SPACE);
|
||||
all_keys.insert(TAB);
|
||||
all_keys.insert(ENTER);
|
||||
all_keys.insert(BACKSPACE);
|
||||
for &key in MODIFIER_SENTINELS {
|
||||
all_keys.insert(key);
|
||||
}
|
||||
|
||||
let mut key_accuracies: Vec<(char, f64)> = all_keys
|
||||
.into_iter()
|
||||
@@ -1130,9 +1130,9 @@ impl StatsDashboard<'_> {
|
||||
}
|
||||
}
|
||||
all_keys.insert(SPACE);
|
||||
all_keys.insert(TAB);
|
||||
all_keys.insert(ENTER);
|
||||
all_keys.insert(BACKSPACE);
|
||||
for &key in MODIFIER_SENTINELS {
|
||||
all_keys.insert(key);
|
||||
}
|
||||
|
||||
let mut key_accuracies: Vec<(char, f64)> = all_keys
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user