1use std::{collections::HashSet as Set, error};
2
3use chrono::{serde::ts_microseconds, DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::config::AppConfig;
7
8pub const DEFAULT_TEXT_WIDTH_PERCENT: u16 = 60;
9pub const FULL_TEXT_WIDTH_PERCENT: u16 = 95;
10const STARTING_SAMPLE_SIZE: usize = 100;
11
12pub type AppResult<T> = std::result::Result<T, Box<dyn error::Error>>;
13
14pub struct App {
15 pub running: bool,
16 pub book_lines: Vec<String>,
17 pub line_index: Vec<(usize, usize)>,
18 pub sample_start_index: usize,
19 pub sample_len: usize,
20 start_time: DateTime<Utc>,
21 pub display_line: usize,
22 pub text_width_percent: u16,
23 pub terminal_width: u16,
24 pub full_text_width: bool,
25 pub text: Text,
26}
27
28pub struct Text {
29 pub text: String,
30 pub cur_char: usize,
31 sample_log: Vec<Test>,
32 keypress_log: Vec<KeyPress>,
33 save: Box<dyn Fn(Vec<Test>, Vec<KeyPress>) -> AppResult<()>>,
34}
35
36impl Text {
37 pub fn new<F>(text: String, sample_log: Vec<Test>, save: F, cur_char: usize) -> Self
38 where
39 F: Fn(Vec<Test>, Vec<KeyPress>) -> AppResult<()> + 'static, {
40 Self {
41 text,
42 cur_char,
43 sample_log,
44 save: Box::new(save),
45 keypress_log: Default::default(),
46 }
47 }
48}
49
50impl App {
51 pub fn new(terminal_width: u16, text: Text) -> Self {
52 let mut ret = Self {
53 text,
54 terminal_width,
55 running: true,
56 text_width_percent: DEFAULT_TEXT_WIDTH_PERCENT,
57 full_text_width: false,
58 start_time: Default::default(),
59 sample_len: Default::default(),
60 sample_start_index: Default::default(),
61 book_lines: Default::default(),
62 line_index: Default::default(),
63 display_line: Default::default(),
64 };
65
66 let _ = ret.get_next_sample();
67 ret.generate_lines();
68
69 ret
70 }
71
72 pub fn quit(&mut self) -> AppResult<()> {
73 self.running = false;
74 (self.text.save)(self.text.sample_log.clone(), self.text.keypress_log.clone())
75 }
76
77 pub fn handle_char(&mut self, config: &AppConfig, c: char) -> AppResult<()> {
78 fn is_correct_char(myopia: bool, c: char, correct_char: char) -> bool {
79 let similar_char_groups = &[Set::from([',', '.', ';']), Set::from([';', ':']), Set::from(['n', 'h']), Set::from(['i', 'l', '1'])];
81
82 match myopia {
83 true => {
84 for char_group in similar_char_groups {
85 if char_group.contains(&c) || char_group.contains(&correct_char) {
86 return true;
87 }
88 }
89
90 c.to_lowercase().collect::<String>() == correct_char.to_lowercase().collect::<String>()
91 }
92 false => c == correct_char,
93 }
94 }
95 let actual_char = self.text.text.chars().nth(self.sample_start_index + self.text.cur_char).unwrap();
96 let correct = is_correct_char(config.myopia, c, actual_char);
97
98 if correct {
99 self.text.cur_char += 1
100 }
101 if !correct || self.text.cur_char == self.sample_len {
102 self.text.sample_log.push(Test {
103 succeeded: correct,
104 start_index: self.sample_start_index,
105 end_index: self.sample_start_index + self.text.cur_char,
106 started: self.start_time,
107 completed: Utc::now(),
108 });
109 self.start_time = Utc::now();
110 self.get_next_sample()?;
111
112 self.text.cur_char = 0;
113 }
114
115 let log_entry = &KeyPress { correct, key: c, time: Utc::now() };
116 self.text.keypress_log.push(log_entry.clone());
117
118 let &(cur_line, _) = self.line_index.get(self.sample_start_index + self.text.cur_char).unwrap();
119 self.display_line = cur_line;
120 Ok(())
121 }
122
123 pub fn generate_lines(&mut self) {
124 let max_line_len = (self.terminal_width as f64 * (self.text_width_percent as f64 / 100.0)) as usize;
125 let mut lines = Vec::new();
126 let mut line_index: Vec<(usize, usize)> = Vec::new();
127 let mut line = "".to_owned();
128 let mut word = "".to_owned();
129 let mut row_i = 0;
130 let mut column_i = 0;
131
132 for c in self.text.text.chars() {
133 word.push(c);
134 if c == ' ' {
135 if line.len() + word.len() < max_line_len {
136 line.push_str(&word);
137 } else {
138 lines.push(line);
139 line = word.to_owned();
140 row_i += 1;
141 column_i = 0;
142 }
143 for _ in 0..word.len() {
144 line_index.push((row_i, column_i));
145 column_i += 1;
146 }
147 word = "".to_owned();
148 }
149 }
150 if line.len() + word.len() < max_line_len {
151 line.push_str(&word);
152 lines.push(line);
153 } else {
154 lines.push(line);
155 lines.push(word.clone());
156 row_i += 1;
157 }
158 for _ in 0..word.len() {
159 line_index.push((row_i, column_i));
160 column_i += 1;
161 }
162
163 self.book_lines = lines;
164 self.display_line = line_index.get(self.sample_start_index).unwrap().0; self.line_index = line_index;
166 }
167
168 fn get_next_sample(&mut self) -> AppResult<()> {
169 let tests = &self.text.sample_log;
170
171 let mut start_index = 0;
172 for t in tests {
173 if t.succeeded && t.end_index > start_index {
174 start_index = t.end_index;
175 }
176 }
177
178 let avg_50 = tests.iter().map(|t| t.end_index - t.start_index).filter(|&len| len > 5).rev().take(50).sum::<usize>() / 50;
179 let max_10 = tests
180 .iter()
181 .map(|t| t.end_index - t.start_index)
182 .filter(|&len| len > 5)
183 .rev()
184 .take(10)
185 .max()
186 .unwrap_or(STARTING_SAMPLE_SIZE);
187 let best = usize::max(avg_50, max_10) + 5;
188
189 let wrong_num = tests
190 .iter()
191 .rev()
192 .take_while(|t| !t.succeeded)
193 .map(|t| t.end_index - t.start_index)
194 .filter(|&len| len > 5)
195 .count();
196
197 let full = self.text.text.chars().skip(start_index).take(best).collect::<String>();
198
199 let len = full.split_whitespace().rev().skip(usize::max(wrong_num, 1)).collect::<Vec<_>>().join(" ").len() + 1;
200
201 self.sample_start_index = usize::min(start_index, self.text.text.len() - 1);
202 self.sample_len = usize::min(len, self.text.text.len() - start_index - 1);
203 self.start_time = Utc::now();
204 Ok(())
205 }
206
207 pub fn get_rolling_average(&self) -> AppResult<usize> {
208 Ok(self
209 .text
210 .sample_log
211 .iter()
212 .map(|t| t.end_index - t.start_index)
213 .filter(|&len| len > 5)
214 .rev()
215 .take(10)
216 .sum::<usize>()
217 / 10)
218 }
219}
220
221#[derive(Serialize, Deserialize, Clone)]
222pub struct KeyPress {
223 correct: bool,
224 key: char,
225 #[serde(with = "ts_microseconds")]
226 time: DateTime<Utc>,
227}
228
229#[derive(Serialize, Deserialize, Clone)]
230pub struct Test {
231 succeeded: bool,
232 start_index: usize,
233 end_index: usize,
234 #[serde(with = "ts_microseconds")]
235 started: DateTime<Utc>,
236 #[serde(with = "ts_microseconds")]
237 completed: DateTime<Utc>,
238}