rust_expect/send/
human.rs

1//! Human-like typing simulation.
2//!
3//! This module provides functionality for simulating human typing patterns,
4//! including variable delays, typos, and corrections.
5
6use std::time::Duration;
7
8use rand::Rng;
9
10use crate::config::HumanTypingConfig;
11use crate::error::Result;
12
13/// A human-like typing simulator.
14pub struct HumanTyper {
15    /// Configuration for typing behavior.
16    config: HumanTypingConfig,
17    /// Random number generator.
18    rng: rand::rngs::ThreadRng,
19}
20
21impl HumanTyper {
22    /// Create a new human typer with default configuration.
23    #[must_use]
24    pub fn new() -> Self {
25        Self {
26            config: HumanTypingConfig::default(),
27            rng: rand::rng(),
28        }
29    }
30
31    /// Create a new human typer with custom configuration.
32    #[must_use]
33    pub fn with_config(config: HumanTypingConfig) -> Self {
34        Self {
35            config,
36            rng: rand::rng(),
37        }
38    }
39
40    /// Get the configuration.
41    #[must_use]
42    pub const fn config(&self) -> &HumanTypingConfig {
43        &self.config
44    }
45
46    /// Set the configuration.
47    pub const fn set_config(&mut self, config: HumanTypingConfig) {
48        self.config = config;
49    }
50
51    /// Generate a random delay between key presses.
52    pub fn next_delay(&mut self) -> Duration {
53        let base = self.config.base_delay.as_millis() as f64;
54        let variance = self.config.variance.as_millis() as f64;
55
56        // Normal-ish distribution around base delay
57        let offset = self.rng.random_range(-1.0..1.0) * variance;
58        let delay_ms = (base + offset).max(10.0);
59
60        Duration::from_millis(delay_ms as u64)
61    }
62
63    /// Check if a typo should be made based on configuration.
64    pub fn should_make_typo(&mut self) -> bool {
65        self.config.typo_chance > 0.0 && self.rng.random::<f32>() < self.config.typo_chance
66    }
67
68    /// Generate a typo for a character.
69    ///
70    /// Returns the typo character and whether a correction should follow.
71    pub fn make_typo(&mut self, c: char) -> (char, bool) {
72        // Get nearby keys on QWERTY layout
73        let nearby = get_nearby_keys(c);
74
75        if nearby.is_empty() {
76            return (c, false);
77        }
78
79        let idx = self.rng.random_range(0..nearby.len());
80        let typo = nearby[idx];
81        let should_correct = self.config.correction_chance > 0.0
82            && self.rng.random::<f32>() < self.config.correction_chance;
83
84        (typo, should_correct)
85    }
86
87    /// Generate pause duration for thinking.
88    pub fn thinking_pause(&mut self) -> Duration {
89        let base_ms: f64 = 500.0;
90        let variance_ms: f64 = 300.0;
91        let offset: f64 = self.rng.random_range(-1.0..1.0) * variance_ms;
92        Duration::from_millis((base_ms + offset).max(100.0) as u64)
93    }
94
95    /// Plan the keystrokes for a string, including delays and potential typos.
96    pub fn plan_typing(&mut self, text: &str) -> Vec<TypeEvent> {
97        let mut events = Vec::new();
98
99        for c in text.chars() {
100            // Possibly make a typo
101            if self.should_make_typo() && c.is_alphabetic() {
102                let (typo, should_correct) = self.make_typo(c);
103
104                events.push(TypeEvent::Char(typo));
105                events.push(TypeEvent::Delay(self.next_delay()));
106
107                if should_correct {
108                    // Pause to "notice" the mistake
109                    events.push(TypeEvent::Delay(self.thinking_pause()));
110                    // Delete the typo
111                    events.push(TypeEvent::Backspace);
112                    events.push(TypeEvent::Delay(self.next_delay()));
113                    // Type the correct character
114                    events.push(TypeEvent::Char(c));
115                    events.push(TypeEvent::Delay(self.next_delay()));
116                }
117            } else {
118                events.push(TypeEvent::Char(c));
119                events.push(TypeEvent::Delay(self.next_delay()));
120            }
121
122            // Add longer pause at word boundaries
123            if c == ' ' || c == '.' || c == ',' || c == '\n' {
124                events.push(TypeEvent::Delay(Duration::from_millis(
125                    self.rng.random_range(50..150),
126                )));
127            }
128        }
129
130        events
131    }
132}
133
134impl Default for HumanTyper {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140/// An event in the typing sequence.
141#[derive(Debug, Clone)]
142pub enum TypeEvent {
143    /// Type a character.
144    Char(char),
145    /// Wait for a duration.
146    Delay(Duration),
147    /// Press backspace.
148    Backspace,
149    /// Send a control character.
150    Control(u8),
151}
152
153impl TypeEvent {
154    /// Get the bytes to send for this event.
155    #[must_use]
156    pub fn as_bytes(&self) -> Option<Vec<u8>> {
157        match self {
158            Self::Char(c) => {
159                let mut buf = [0u8; 4];
160                let s = c.encode_utf8(&mut buf);
161                Some(s.as_bytes().to_vec())
162            }
163            Self::Backspace => Some(vec![0x7f]),
164            Self::Control(c) => Some(vec![*c]),
165            Self::Delay(_) => None,
166        }
167    }
168}
169
170/// Get nearby keys on a QWERTY keyboard layout.
171fn get_nearby_keys(c: char) -> Vec<char> {
172    let c_lower = c.to_ascii_lowercase();
173
174    let nearby = match c_lower {
175        'q' => vec!['w', 'a', 's'],
176        'w' => vec!['q', 'e', 'a', 's', 'd'],
177        'e' => vec!['w', 'r', 's', 'd', 'f'],
178        'r' => vec!['e', 't', 'd', 'f', 'g'],
179        't' => vec!['r', 'y', 'f', 'g', 'h'],
180        'y' => vec!['t', 'u', 'g', 'h', 'j'],
181        'u' => vec!['y', 'i', 'h', 'j', 'k'],
182        'i' => vec!['u', 'o', 'j', 'k', 'l'],
183        'o' => vec!['i', 'p', 'k', 'l'],
184        'p' => vec!['o', 'l'],
185        'a' => vec!['q', 'w', 's', 'z'],
186        's' => vec!['q', 'w', 'e', 'a', 'd', 'z', 'x'],
187        'd' => vec!['w', 'e', 'r', 's', 'f', 'x', 'c'],
188        'f' => vec!['e', 'r', 't', 'd', 'g', 'c', 'v'],
189        'g' => vec!['r', 't', 'y', 'f', 'h', 'v', 'b'],
190        'h' => vec!['t', 'y', 'u', 'g', 'j', 'b', 'n'],
191        'j' => vec!['y', 'u', 'i', 'h', 'k', 'n', 'm'],
192        'k' => vec!['u', 'i', 'o', 'j', 'l', 'm'],
193        'l' => vec!['i', 'o', 'p', 'k'],
194        'z' => vec!['a', 's', 'x'],
195        'x' => vec!['s', 'd', 'z', 'c'],
196        'c' => vec!['d', 'f', 'x', 'v'],
197        'v' => vec!['f', 'g', 'c', 'b'],
198        'b' => vec!['g', 'h', 'v', 'n'],
199        'n' => vec!['h', 'j', 'b', 'm'],
200        'm' => vec!['j', 'k', 'n'],
201        _ => vec![],
202    };
203
204    // Preserve case
205    if c.is_uppercase() {
206        nearby.into_iter().map(|c| c.to_ascii_uppercase()).collect()
207    } else {
208        nearby
209    }
210}
211
212/// Typing speed presets.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum TypingSpeed {
215    /// Very slow typing (hunt and peck).
216    VerySlow,
217    /// Slow typing (beginner).
218    Slow,
219    /// Normal typing speed.
220    Normal,
221    /// Fast typing speed.
222    Fast,
223    /// Very fast typing (professional).
224    VeryFast,
225}
226
227impl TypingSpeed {
228    /// Get the configuration for this typing speed.
229    #[must_use]
230    pub fn config(self) -> HumanTypingConfig {
231        match self {
232            Self::VerySlow => HumanTypingConfig {
233                base_delay: Duration::from_millis(300),
234                variance: Duration::from_millis(150),
235                typo_chance: 0.03,
236                correction_chance: 0.95,
237            },
238            Self::Slow => HumanTypingConfig {
239                base_delay: Duration::from_millis(180),
240                variance: Duration::from_millis(80),
241                typo_chance: 0.02,
242                correction_chance: 0.9,
243            },
244            Self::Normal => HumanTypingConfig::default(),
245            Self::Fast => HumanTypingConfig {
246                base_delay: Duration::from_millis(60),
247                variance: Duration::from_millis(30),
248                typo_chance: 0.02,
249                correction_chance: 0.8,
250            },
251            Self::VeryFast => HumanTypingConfig {
252                base_delay: Duration::from_millis(30),
253                variance: Duration::from_millis(15),
254                typo_chance: 0.03,
255                correction_chance: 0.7,
256            },
257        }
258    }
259}
260
261/// Extension trait for human-like typing.
262pub trait HumanSend {
263    /// Send text with human-like typing patterns.
264    fn send_human(
265        &mut self,
266        text: &str,
267        config: HumanTypingConfig,
268    ) -> impl std::future::Future<Output = Result<()>> + Send;
269
270    /// Send text with a preset typing speed.
271    fn send_human_speed(
272        &mut self,
273        text: &str,
274        speed: TypingSpeed,
275    ) -> impl std::future::Future<Output = Result<()>> + Send {
276        self.send_human(text, speed.config())
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn human_typer_delay() {
286        let mut typer = HumanTyper::new();
287
288        let delay = typer.next_delay();
289        assert!(delay.as_millis() >= 10);
290    }
291
292    #[test]
293    fn human_typer_plan() {
294        let mut typer = HumanTyper::with_config(HumanTypingConfig {
295            typo_chance: 0.0, // Disable typos for predictable testing
296            ..Default::default()
297        });
298
299        let events = typer.plan_typing("hi");
300
301        // Should have: Char('h'), Delay, Char('i'), Delay
302        assert!(events.len() >= 4);
303        assert!(matches!(events[0], TypeEvent::Char('h')));
304        assert!(matches!(events[2], TypeEvent::Char('i')));
305    }
306
307    #[test]
308    fn nearby_keys() {
309        let nearby = get_nearby_keys('f');
310        assert!(nearby.contains(&'d'));
311        assert!(nearby.contains(&'g'));
312        assert!(!nearby.contains(&'z'));
313
314        let nearby_upper = get_nearby_keys('F');
315        assert!(nearby_upper.contains(&'D'));
316        assert!(nearby_upper.contains(&'G'));
317    }
318
319    #[test]
320    fn typing_speed_config() {
321        let slow = TypingSpeed::Slow.config();
322        let fast = TypingSpeed::Fast.config();
323
324        assert!(slow.base_delay > fast.base_delay);
325    }
326}