1use std::time::Duration;
15
16use crate::rng::Rng;
17use crate::InputMode;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Key {
28 pub code: u32,
29 pub character: Option<char>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum KeyStepKind {
35 Down,
37 Up,
39 Press,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct KeyStep {
49 pub at: Duration,
50 pub kind: KeyStepKind,
51 pub key: Key,
52}
53
54const HUMAN_DWELL_MIN_MS: f64 = 30.0;
58const HUMAN_DWELL_MAX_MS: f64 = 80.0;
59
60const HUMAN_GAP_MU: f64 = 4.7;
65const HUMAN_GAP_SIGMA: f64 = 0.3;
66const HUMAN_GAP_MIN_MS: f64 = 40.0;
67const HUMAN_GAP_MAX_MS: f64 = 260.0;
68
69const WORD_BOUNDARY_MULT: f64 = 1.7;
73
74const HUMAN_TYPO_RATE: f64 = 0.015;
79
80const CAREFUL_GAP_MS: f64 = 50.0;
82
83#[must_use]
98pub fn key_sequence(text: &str, mode: InputMode, seed: u64) -> Vec<KeyStep> {
99 match mode {
100 InputMode::Robotic => Vec::new(),
101 InputMode::Careful => careful_sequence(text),
102 InputMode::Human => human_sequence(text, seed),
103 }
104}
105
106fn careful_sequence(text: &str) -> Vec<KeyStep> {
107 let mut out = Vec::with_capacity(text.chars().count() * 2);
108 let mut t_ms = 0.0;
109 for ch in text.chars() {
110 let key = Key {
111 code: ch as u32,
112 character: Some(ch),
113 };
114 out.push(KeyStep {
115 at: ms(t_ms),
116 kind: KeyStepKind::Down,
117 key,
118 });
119 out.push(KeyStep {
120 at: ms(t_ms + 25.0),
121 kind: KeyStepKind::Up,
122 key,
123 });
124 t_ms += CAREFUL_GAP_MS;
125 }
126 out
127}
128
129fn human_sequence(text: &str, seed: u64) -> Vec<KeyStep> {
130 let mut rng = Rng::seed_from_u64(seed);
131 let mut out: Vec<KeyStep> = Vec::with_capacity(text.chars().count() * 2 + 4);
132 let mut t_ms = 0.0;
133 let chars: Vec<char> = text.chars().collect();
134
135 for (i, ch) in chars.iter().copied().enumerate() {
136 let gap_raw = (HUMAN_GAP_MU + HUMAN_GAP_SIGMA * rng.next_normal()).exp();
138 let mut gap = gap_raw.clamp(HUMAN_GAP_MIN_MS, HUMAN_GAP_MAX_MS);
139 if i > 0 && is_word_boundary(chars[i - 1]) {
140 gap *= WORD_BOUNDARY_MULT;
141 }
142 if i > 0 {
143 t_ms += gap;
144 }
145
146 if ch.is_ascii_alphabetic() && rng.next_f64() < HUMAN_TYPO_RATE {
154 let wrong = neighbor_letter(ch, &mut rng);
155 press(&mut out, t_ms, wrong, &mut rng);
156 t_ms += rng.next_uniform(60.0, 120.0);
157 press_backspace(&mut out, t_ms, &mut rng);
158 t_ms += rng.next_uniform(60.0, 120.0);
159 }
160
161 press(&mut out, t_ms, ch, &mut rng);
162 }
163 out
164}
165
166fn press(out: &mut Vec<KeyStep>, t_ms: f64, ch: char, rng: &mut Rng) {
167 let dwell = rng.next_uniform(HUMAN_DWELL_MIN_MS, HUMAN_DWELL_MAX_MS);
171 let key = Key {
172 code: ch as u32,
173 character: Some(ch),
174 };
175 out.push(KeyStep {
176 at: ms(t_ms),
177 kind: KeyStepKind::Down,
178 key,
179 });
180 out.push(KeyStep {
181 at: ms(t_ms + dwell),
182 kind: KeyStepKind::Up,
183 key,
184 });
185}
186
187fn press_backspace(out: &mut Vec<KeyStep>, t_ms: f64, rng: &mut Rng) {
188 let dwell = rng.next_uniform(HUMAN_DWELL_MIN_MS, HUMAN_DWELL_MAX_MS);
189 let key = Key {
192 code: 0x2A,
193 character: None,
194 };
195 out.push(KeyStep {
196 at: ms(t_ms),
197 kind: KeyStepKind::Down,
198 key,
199 });
200 out.push(KeyStep {
201 at: ms(t_ms + dwell),
202 kind: KeyStepKind::Up,
203 key,
204 });
205}
206
207fn is_word_boundary(ch: char) -> bool {
208 ch == ' ' || ch == '\n' || ch == '\t' || ch == ',' || ch == '.' || ch == ';' || ch == ':'
209}
210
211fn neighbor_letter(ch: char, rng: &mut Rng) -> char {
216 let lower = ch.to_ascii_lowercase();
217 let neighbors: &[char] = match lower {
218 'a' => &['s', 'q', 'w', 'z'],
219 'b' => &['v', 'n', 'g', 'h'],
220 'c' => &['x', 'v', 'd', 'f'],
221 'd' => &['s', 'f', 'e', 'r', 'c'],
222 'e' => &['w', 'r', 's', 'd'],
223 'f' => &['d', 'g', 'r', 't', 'v'],
224 'g' => &['f', 'h', 't', 'y', 'b'],
225 'h' => &['g', 'j', 'y', 'u', 'n'],
226 'i' => &['u', 'o', 'k'],
227 'j' => &['h', 'k', 'u', 'i', 'm'],
228 'k' => &['j', 'l', 'i', 'o'],
229 'l' => &['k', 'o', 'p'],
230 'm' => &['n', 'j', 'k'],
231 'n' => &['b', 'm', 'h', 'j'],
232 'o' => &['i', 'p', 'k', 'l'],
233 'p' => &['o', 'l'],
234 'q' => &['w', 'a'],
235 'r' => &['e', 't', 'd', 'f'],
236 's' => &['a', 'd', 'w', 'e', 'z', 'x'],
237 't' => &['r', 'y', 'f', 'g'],
238 'u' => &['y', 'i', 'h', 'j'],
239 'v' => &['c', 'b', 'f', 'g'],
240 'w' => &['q', 'e', 'a', 's'],
241 'x' => &['z', 'c', 's', 'd'],
242 'y' => &['t', 'u', 'g', 'h'],
243 'z' => &['a', 's', 'x'],
244 _ => return ch,
245 };
246 #[allow(clippy::cast_possible_truncation)]
249 let i = (rng.next_u64() as usize) % neighbors.len();
250 let n = neighbors[i];
251 if ch.is_ascii_uppercase() {
252 n.to_ascii_uppercase()
253 } else {
254 n
255 }
256}
257
258fn ms(value: f64) -> Duration {
259 let v = value.max(0.0).round();
260 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
261 let ms_int = v as u64;
262 Duration::from_millis(ms_int)
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn robotic_is_empty() {
271 let steps = key_sequence("hello", InputMode::Robotic, 0);
272 assert!(steps.is_empty());
273 }
274
275 #[test]
276 fn careful_one_down_one_up_per_char() {
277 let steps = key_sequence("hi!", InputMode::Careful, 0);
278 assert_eq!(steps.len(), 6);
279 let downs = steps.iter().filter(|s| s.kind == KeyStepKind::Down).count();
280 let ups = steps.iter().filter(|s| s.kind == KeyStepKind::Up).count();
281 assert_eq!(downs, 3);
282 assert_eq!(ups, 3);
283 }
284
285 #[test]
286 fn careful_fixed_cadence() {
287 let steps = key_sequence("abc", InputMode::Careful, 0);
288 let down_times: Vec<u128> = steps
290 .iter()
291 .filter(|s| s.kind == KeyStepKind::Down)
292 .map(|s| s.at.as_millis())
293 .collect();
294 assert_eq!(down_times, vec![0, 50, 100]);
295 }
296
297 #[test]
298 fn human_empty_text_yields_empty_sequence() {
299 let steps = key_sequence("", InputMode::Human, 7);
300 assert!(steps.is_empty());
301 }
302
303 #[test]
304 fn human_single_char_has_down_then_up() {
305 let steps = key_sequence("x", InputMode::Human, 7);
306 assert_eq!(steps.len(), 2);
307 assert_eq!(steps[0].kind, KeyStepKind::Down);
308 assert_eq!(steps[1].kind, KeyStepKind::Up);
309 assert_eq!(steps[0].key.character, Some('x'));
310 }
311
312 #[test]
313 fn human_is_deterministic_under_seed() {
314 let a = key_sequence("hello world", InputMode::Human, 1234);
315 let b = key_sequence("hello world", InputMode::Human, 1234);
316 assert_eq!(a, b);
317 }
318
319 #[test]
320 fn human_seed_change_changes_timing() {
321 let a = key_sequence("hello world", InputMode::Human, 1);
322 let b = key_sequence("hello world", InputMode::Human, 2);
323 assert_ne!(a.last().unwrap().at, b.last().unwrap().at);
326 }
327
328 #[test]
329 fn human_word_boundary_pauses_longer() {
330 let trials = 30;
337 let mut letter_gaps: Vec<u128> = Vec::new();
338 let mut boundary_gaps: Vec<u128> = Vec::new();
339 for seed in 0..trials {
340 let s = key_sequence("rt yu", InputMode::Careful, seed);
344 assert_eq!(s.iter().filter(|s| s.kind == KeyStepKind::Down).count(), 5);
348 }
349 for seed in 0..trials {
350 let s = key_sequence("ab cd", InputMode::Human, seed);
351 let downs: Vec<u128> = s
352 .iter()
353 .filter(|s| s.kind == KeyStepKind::Down)
354 .map(|s| s.at.as_millis())
355 .collect();
356 if downs.len() != 5 {
360 continue;
361 }
362 letter_gaps.push(downs[1] - downs[0]); boundary_gaps.push(downs[3] - downs[2]); }
365 let median = |mut v: Vec<u128>| {
366 v.sort_unstable();
367 v[v.len() / 2]
368 };
369 let m_letter = median(letter_gaps);
370 let m_boundary = median(boundary_gaps);
371 assert!(
372 m_boundary > m_letter,
373 "boundary gap median {m_boundary}ms should exceed letter gap median {m_letter}ms"
374 );
375 }
376
377 #[test]
378 fn human_mean_gap_in_expected_band() {
379 let mut all_gaps: Vec<u128> = Vec::new();
383 for seed in 0..50 {
384 let s = key_sequence("the quick brown fox", InputMode::Human, seed);
385 let downs: Vec<u128> = s
386 .iter()
387 .filter(|s| s.kind == KeyStepKind::Down)
388 .map(|s| s.at.as_millis())
389 .collect();
390 for w in downs.windows(2) {
391 all_gaps.push(w[1] - w[0]);
392 }
393 }
394 #[allow(clippy::cast_precision_loss)]
395 let n = all_gaps.len() as f64;
396 #[allow(clippy::cast_precision_loss)]
397 let mean: f64 = all_gaps.iter().sum::<u128>() as f64 / n;
398 assert!(
399 (80.0..=180.0).contains(&mean),
400 "mean inter-key gap {mean}ms outside 80–180 band"
401 );
402 }
403
404 #[test]
405 fn human_emits_typos_over_long_text() {
406 let any_backspace = (0..16).any(|seed| {
410 let s = key_sequence(
411 &"the quick brown fox jumps over the lazy dog ".repeat(8),
412 InputMode::Human,
413 seed,
414 );
415 s.iter().any(|step| step.key.code == 0x2A)
416 });
417 assert!(any_backspace, "no typos in 16 long-text realizations");
418 }
419
420 #[test]
421 fn human_steps_monotonic_in_time() {
422 let steps = key_sequence("hello world", InputMode::Human, 42);
423 for w in steps.windows(2) {
424 assert!(
425 w[0].at <= w[1].at,
426 "non-monotonic: {:?} then {:?}",
427 w[0],
428 w[1]
429 );
430 }
431 }
432}