Skip to main content

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