use crate::content::{ContentProvider, Mode};
use crate::stats::Stats;
#[derive(Debug, Clone)]
pub struct TypingTest {
pub mode: Mode,
pub language: String,
pub target_text: String,
pub user_input: String,
pub cursor_position: usize,
pub stats: Stats,
pub duration_secs: u64,
pub elapsed_secs: u64,
pub is_active: bool,
pub is_finished: bool,
pub has_started: bool,
}
impl TypingTest {
pub fn new(mode: Mode, language: String, duration_secs: u64) -> Self {
let provider = ContentProvider::new(mode, &language);
let target_text = if mode == Mode::Text {
provider.generate_text(200)
} else {
provider.generate_code_snippet()
};
Self {
mode,
language,
target_text,
user_input: String::new(),
cursor_position: 0,
stats: Stats::default(),
duration_secs,
elapsed_secs: 0,
is_active: false,
is_finished: false,
has_started: false,
}
}
pub fn start(&mut self) {
self.is_active = true;
self.has_started = true;
}
pub fn tick(&mut self) {
if self.is_active && !self.is_finished {
self.elapsed_secs += 1;
if self.elapsed_secs >= self.duration_secs {
self.finish();
}
}
}
pub fn finish(&mut self) {
self.is_active = false;
self.is_finished = true;
}
pub fn handle_input(&mut self, c: char) {
if self.is_finished {
return;
}
if !self.has_started {
self.start();
}
let expected = self.target_text.chars().nth(self.cursor_position);
let correct = expected == Some(c);
self.user_input.push(c);
self.stats.record_char(correct);
self.cursor_position += 1;
if self.cursor_position >= self.target_text.len() {
self.finish();
}
}
pub fn skip_to_end_of_line(&mut self) -> bool {
if self.is_finished {
return false;
}
if !self.has_started {
self.start();
}
let target_chars: Vec<char> = self.target_text.chars().collect();
for i in self.cursor_position..target_chars.len() {
if target_chars[i] == '\n' {
for _ in self.cursor_position..i {
self.user_input.push('\0');
self.stats.record_char(false); }
self.cursor_position = i;
return true;
}
}
false
}
pub fn handle_backspace(&mut self) {
if !self.is_active || self.is_finished || self.cursor_position == 0 {
return;
}
self.stats.record_backspace();
if self.mode == Mode::Code && self.is_at_line_start() {
let chars_to_remove = self
.user_input
.chars()
.rev()
.take_while(|c| *c != '\n')
.count()
+ 1;
let new_len = self.user_input.len().saturating_sub(chars_to_remove);
self.user_input.truncate(new_len);
self.cursor_position = self.user_input.len();
} else {
self.user_input.pop();
self.cursor_position -= 1;
}
}
fn is_at_line_start(&self) -> bool {
let last_newline_pos = self.user_input.rfind('\n');
let current_line_start = match last_newline_pos {
Some(pos) => pos + 1,
None => 0,
};
self.user_input
.chars()
.skip(current_line_start)
.all(|c| c.is_whitespace())
}
pub fn get_display_text(&self, max_chars: usize) -> (String, String, String) {
let typed = self.user_input.chars().take(max_chars).collect::<String>();
let remaining = self
.target_text
.chars()
.skip(self.cursor_position)
.take(max_chars.saturating_sub(typed.chars().count()))
.collect::<String>();
let overflow = if self.cursor_position + remaining.chars().count() < self.target_text.len() {
"...".to_string()
} else {
String::new()
};
(typed, remaining, overflow)
}
pub fn current_char_status(&self) -> Vec<(char, CharStatus)> {
let mut result = Vec::new();
for (i, target_char) in self.target_text.chars().enumerate() {
let status = if i >= self.user_input.len() {
CharStatus::Untyped
} else {
let typed_char = self.user_input.chars().nth(i).unwrap();
if typed_char == target_char {
CharStatus::Correct
} else {
CharStatus::Incorrect
}
};
result.push((target_char, status));
}
result
}
pub fn wpm(&self) -> f64 {
self.stats.wpm(self.elapsed_secs.max(1))
}
pub fn raw_wpm(&self) -> f64 {
self.stats.raw_wpm(self.elapsed_secs.max(1))
}
pub fn accuracy(&self) -> f64 {
self.stats.accuracy()
}
pub fn progress_pct(&self) -> f64 {
if self.target_text.is_empty() {
return 0.0;
}
(self.cursor_position as f64 / self.target_text.len() as f64) * 100.0
}
pub fn remaining_secs(&self) -> u64 {
self.duration_secs.saturating_sub(self.elapsed_secs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharStatus {
Untyped,
Correct,
Incorrect,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_typing_flow() {
let mut test = TypingTest::new(Mode::Text, "english".to_string(), 60);
test.start();
assert!(test.is_active);
assert!(test.has_started);
}
#[test]
fn test_handle_input() {
let mut test = TypingTest::new(Mode::Text, "english".to_string(), 60);
test.target_text = "hello world".to_string();
test.start();
for c in "hello".chars() {
test.handle_input(c);
}
assert_eq!(test.cursor_position, 5);
assert_eq!(test.user_input, "hello");
}
#[test]
fn test_backspace() {
let mut test = TypingTest::new(Mode::Text, "english".to_string(), 60);
test.target_text = "hello".to_string();
test.start();
test.handle_input('h');
test.handle_input('e');
assert_eq!(test.cursor_position, 2);
test.handle_backspace();
assert_eq!(test.cursor_position, 1);
assert_eq!(test.user_input, "h");
}
#[test]
fn test_char_status() {
let mut test = TypingTest::new(Mode::Text, "english".to_string(), 60);
test.target_text = "abc".to_string();
test.start();
test.handle_input('a');
test.handle_input('x'); test.handle_input('c');
let status = test.current_char_status();
assert_eq!(status[0].1, CharStatus::Correct);
assert_eq!(status[1].1, CharStatus::Incorrect);
assert_eq!(status[2].1, CharStatus::Correct);
}
}