Import/export feature for config and data

This commit is contained in:
2026-02-22 07:36:34 +00:00
parent 9cc8a214ad
commit 9deffc3d1d
15 changed files with 1717 additions and 125 deletions

View File

@@ -1,6 +1,7 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::keyboard::display::BACKSPACE;
use crate::session::drill::DrillState;
use crate::session::input::KeystrokeEvent;
@@ -52,17 +53,50 @@ impl DrillResult {
ranked: bool,
partial: bool,
) -> Self {
let per_key_times: Vec<KeyTime> = events
.windows(2)
.map(|pair| {
let dt = pair[1].timestamp.duration_since(pair[0].timestamp);
KeyTime {
key: pair[1].expected,
time_ms: dt.as_secs_f64() * 1000.0,
correct: pair[1].correct,
let mut per_key_times: Vec<KeyTime> = Vec::new();
let mut pending_backspace = false;
for pair in events.windows(2) {
let prev = &pair[0];
let curr = &pair[1];
let dt = curr.timestamp.duration_since(prev.timestamp).as_secs_f64() * 1000.0;
// Track per-key expected-char timing/accuracy for normal typing keys.
// Backspace attempts are tracked separately below.
if curr.actual != BACKSPACE {
per_key_times.push(KeyTime {
key: curr.expected,
time_ms: dt,
correct: curr.correct,
});
}
// Backspace attempt tracking:
// - Any incorrect non-backspace key creates a pending backspace need.
// - While pending, every next key press is a backspace attempt.
// - Backspace press = correct attempt; anything else = incorrect attempt
// and the requirement stays pending.
if pending_backspace {
if curr.actual == BACKSPACE {
per_key_times.push(KeyTime {
key: BACKSPACE,
time_ms: dt,
correct: true,
});
pending_backspace = false;
} else {
per_key_times.push(KeyTime {
key: BACKSPACE,
time_ms: dt,
correct: false,
});
pending_backspace = true;
}
})
.collect();
}
if curr.actual != BACKSPACE && !curr.correct {
pending_backspace = true;
}
}
let total_chars = drill.target.len();
let typo_count = drill.typo_flags.len();
@@ -89,3 +123,65 @@ impl DrillResult {
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use super::*;
fn ev(expected: char, actual: char, ms: u64, correct: bool, start: Instant) -> KeystrokeEvent {
KeystrokeEvent {
expected,
actual,
timestamp: start + Duration::from_millis(ms),
correct,
}
}
#[test]
fn tracks_backspace_success_after_incorrect_key() {
let drill = DrillState::new("ab");
let t0 = Instant::now();
let events = vec![
ev('a', 'a', 0, true, t0),
ev('b', 'x', 100, false, t0),
ev(BACKSPACE, BACKSPACE, 220, true, t0),
ev('b', 'b', 350, true, t0),
];
let result = DrillResult::from_drill(&drill, &events, "adaptive", true, false);
let backspace: Vec<&KeyTime> = result
.per_key_times
.iter()
.filter(|kt| kt.key == BACKSPACE)
.collect();
assert_eq!(backspace.len(), 1);
assert!(backspace[0].correct);
assert!((backspace[0].time_ms - 120.0).abs() < 0.1);
}
#[test]
fn tracks_backspace_error_until_user_backspaces() {
let drill = DrillState::new("abc");
let t0 = Instant::now();
let events = vec![
ev('a', 'a', 0, true, t0),
ev('b', 'x', 100, false, t0),
ev('c', 'c', 220, true, t0),
ev(BACKSPACE, BACKSPACE, 400, true, t0),
];
let result = DrillResult::from_drill(&drill, &events, "adaptive", true, false);
let backspace: Vec<&KeyTime> = result
.per_key_times
.iter()
.filter(|kt| kt.key == BACKSPACE)
.collect();
assert_eq!(backspace.len(), 2);
assert!(!backspace[0].correct);
assert!(backspace[1].correct);
assert!((backspace[0].time_ms - 120.0).abs() < 0.1);
assert!((backspace[1].time_ms - 180.0).abs() < 0.1);
}
}