Skip to main content

rustenium/input/bidi/
keyboard.rs

1use rand::Rng;
2use rustenium_bidi_definitions::browsing_context::types::BrowsingContext;
3use rustenium_bidi_definitions::input::command_builders::PerformActionsBuilder;
4use rustenium_bidi_definitions::input::type_builders::{
5    KeySourceActionsBuilder, KeyDownActionBuilder, KeyUpActionBuilder, PauseActionBuilder,
6};
7use rustenium_bidi_definitions::input::types::{
8    KeySourceActionsType, KeyDownActionType, KeyUpActionType, PauseActionType,
9};
10use rustenium_core::error::CommandResultError;
11use rustenium_core::BidiSession;
12use rustenium_core::transport::ConnectionTransport;
13use crate::error::bidi::InputError;
14use std::sync::Arc;
15use tokio::sync::Mutex;
16
17use super::KEYBOARD_ID;
18
19/// Randomised delay range in milliseconds.
20///
21/// Produces naturally varying timing — each operation picks a random integer value
22/// between `min` and `max` ms rather than a fixed duration.
23/// `min` may be 0 (allowing instant presses); `max` must be at least 1ms.
24///
25/// # Human latency reference
26/// - Fast typist:    `DelayRange::new(30, 80)`
27/// - Average typist: `DelayRange::new(60, 140)`
28/// - Slow/careful:   `DelayRange::new(100, 250)`
29#[derive(Debug, Clone, Copy)]
30pub struct DelayRange {
31    pub min: u64,
32    pub max: u64,
33}
34
35impl DelayRange {
36    /// Returns `None` if `max` is 0 or `min > max`.
37    pub fn new(min: u64, max: u64) -> Option<Self> {
38        if max == 0 || min > max { return None; }
39        Some(Self { min, max })
40    }
41}
42
43/// Options for a single key press (KeyDown → optional hold → KeyUp).
44///
45/// `delay` controls how long the key is held. When `None`, no pause is inserted.
46#[derive(Debug, Clone, Default)]
47pub struct KeyPressOptions {
48    pub delay: Option<DelayRange>,
49}
50
51#[derive(Default, Clone)]
52pub struct KeyPressOptionsBuilder {
53    delay: Option<DelayRange>,
54}
55
56impl KeyPressOptionsBuilder {
57    pub fn delay(mut self, min: u64, max: u64) -> Self { self.delay = DelayRange::new(min, max); self }
58    pub fn build(self) -> KeyPressOptions { KeyPressOptions { delay: self.delay } }
59}
60
61/// Options for typing a string of text character by character.
62///
63/// `delay` controls the hold duration (KeyDown → KeyUp) per character.
64/// `gap_multiplier` scales the post-KeyUp pause relative to the hold; defaults to `1.2` (20% longer).
65/// When `delay` is `None`, all characters are batched into a single `performActions` call with no pauses.
66///
67/// # Reasonable human values
68/// - `delay`:          `DelayRange::new(60, 140)` for average typing speed
69/// - `gap_multiplier`: `1.1`–`1.3` — slightly longer pause between keys than the hold itself
70#[derive(Debug, Clone)]
71pub struct KeyboardTypeOptions {
72    pub delay: Option<DelayRange>,
73    pub gap_multiplier: f64,
74}
75
76impl Default for KeyboardTypeOptions {
77    fn default() -> Self {
78        Self { delay: None, gap_multiplier: 1.2 }
79    }
80}
81
82#[derive(Clone)]
83pub struct KeyboardTypeOptionsBuilder {
84    delay: Option<DelayRange>,
85    gap_multiplier: f64,
86}
87
88impl Default for KeyboardTypeOptionsBuilder {
89    fn default() -> Self {
90        Self { delay: None, gap_multiplier: 1.2 }
91    }
92}
93
94impl KeyboardTypeOptionsBuilder {
95    pub fn delay(mut self, min: u64, max: u64) -> Self { self.delay = DelayRange::new(min, max); self }
96    pub fn gap_multiplier(mut self, v: f64) -> Self { self.gap_multiplier = v; self }
97    pub fn build(self) -> KeyboardTypeOptions {
98        KeyboardTypeOptions { delay: self.delay, gap_multiplier: self.gap_multiplier }
99    }
100}
101
102pub(crate) fn get_bidi_key_value(key: &str) -> Result<String, InputError> {
103    let key = match key {
104        "\r" | "\n" => "Enter",
105        _ => key,
106    };
107
108    if key.chars().count() == 1 {
109        return Ok(key.to_string());
110    }
111
112    let value = match key {
113        "Cancel" => "\u{E001}",
114        "Help" => "\u{E002}",
115        "Backspace" => "\u{E003}",
116        "Tab" => "\u{E004}",
117        "Clear" => "\u{E005}",
118        "Enter" => "\u{E007}",
119        "Shift" | "ShiftLeft" => "\u{E008}",
120        "Control" | "ControlLeft" => "\u{E009}",
121        "Alt" | "AltLeft" => "\u{E00A}",
122        "Pause" => "\u{E00B}",
123        "Escape" => "\u{E00C}",
124        "PageUp" => "\u{E00E}",
125        "PageDown" => "\u{E00F}",
126        "End" => "\u{E010}",
127        "Home" => "\u{E011}",
128        "ArrowLeft" => "\u{E012}",
129        "ArrowUp" => "\u{E013}",
130        "ArrowRight" => "\u{E014}",
131        "ArrowDown" => "\u{E015}",
132        "Insert" => "\u{E016}",
133        "Delete" => "\u{E017}",
134        "NumpadEqual" => "\u{E019}",
135        "Numpad0" => "\u{E01A}",
136        "Numpad1" => "\u{E01B}",
137        "Numpad2" => "\u{E01C}",
138        "Numpad3" => "\u{E01D}",
139        "Numpad4" => "\u{E01E}",
140        "Numpad5" => "\u{E01F}",
141        "Numpad6" => "\u{E020}",
142        "Numpad7" => "\u{E021}",
143        "Numpad8" => "\u{E022}",
144        "Numpad9" => "\u{E023}",
145        "NumpadMultiply" => "\u{E024}",
146        "NumpadAdd" => "\u{E025}",
147        "NumpadSubtract" => "\u{E027}",
148        "NumpadDecimal" => "\u{E028}",
149        "NumpadDivide" => "\u{E029}",
150        "F1" => "\u{E031}",
151        "F2" => "\u{E032}",
152        "F3" => "\u{E033}",
153        "F4" => "\u{E034}",
154        "F5" => "\u{E035}",
155        "F6" => "\u{E036}",
156        "F7" => "\u{E037}",
157        "F8" => "\u{E038}",
158        "F9" => "\u{E039}",
159        "F10" => "\u{E03A}",
160        "F11" => "\u{E03B}",
161        "F12" => "\u{E03C}",
162        "Meta" | "MetaLeft" => "\u{E03D}",
163        "ShiftRight" => "\u{E050}",
164        "ControlRight" => "\u{E051}",
165        "AltRight" => "\u{E052}",
166        "MetaRight" => "\u{E053}",
167        "Digit0" => "0",
168        "Digit1" => "1",
169        "Digit2" => "2",
170        "Digit3" => "3",
171        "Digit4" => "4",
172        "Digit5" => "5",
173        "Digit6" => "6",
174        "Digit7" => "7",
175        "Digit8" => "8",
176        "Digit9" => "9",
177        "KeyA" => "a",
178        "KeyB" => "b",
179        "KeyC" => "c",
180        "KeyD" => "d",
181        "KeyE" => "e",
182        "KeyF" => "f",
183        "KeyG" => "g",
184        "KeyH" => "h",
185        "KeyI" => "i",
186        "KeyJ" => "j",
187        "KeyK" => "k",
188        "KeyL" => "l",
189        "KeyM" => "m",
190        "KeyN" => "n",
191        "KeyO" => "o",
192        "KeyP" => "p",
193        "KeyQ" => "q",
194        "KeyR" => "r",
195        "KeyS" => "s",
196        "KeyT" => "t",
197        "KeyU" => "u",
198        "KeyV" => "v",
199        "KeyW" => "w",
200        "KeyX" => "x",
201        "KeyY" => "y",
202        "KeyZ" => "z",
203        "Semicolon" => ";",
204        "Equal" => "=",
205        "Comma" => ",",
206        "Minus" => "-",
207        "Period" => ".",
208        "Slash" => "/",
209        "Backquote" => "`",
210        "BracketLeft" => "[",
211        "Backslash" => "\\",
212        "BracketRight" => "]",
213        "Quote" => "\"",
214        _ => return Err(InputError::UnknownKey(key.to_string())),
215    };
216
217    Ok(value.to_string())
218}
219
220pub struct BidiKeyboard<OT: ConnectionTransport> {
221    session: Arc<Mutex<BidiSession<OT>>>,
222}
223
224impl<OT: ConnectionTransport> BidiKeyboard<OT> {
225    pub fn new(session: Arc<Mutex<BidiSession<OT>>>) -> Self {
226        Self { session }
227    }
228
229    pub async fn down(&self, key: &str, context: &BrowsingContext) -> Result<(), InputError> {
230        tracing::debug!(key, "keyboard down start");
231        let key_value = get_bidi_key_value(key)?;
232
233        let command = PerformActionsBuilder::default()
234            .context(context.clone())
235            .action(
236                KeySourceActionsBuilder::default()
237                    .r#type(KeySourceActionsType::Key)
238                    .id(KEYBOARD_ID)
239                    .action(KeyDownActionBuilder::default()
240                        .r#type(KeyDownActionType::KeyDown)
241                        .value(key_value)
242                        .build().unwrap())
243                    .build().unwrap()
244            )
245            .build().unwrap();
246
247        self.session.lock().await.send(command).await
248            .map_err(|e| InputError::CommandResultError(CommandResultError::SessionSendError(e)))?;
249        tracing::debug!(key, "keyboard down done");
250        Ok(())
251    }
252
253    pub async fn up(&self, key: &str, context: &BrowsingContext) -> Result<(), InputError> {
254        tracing::debug!(key, "keyboard up start");
255        let key_value = get_bidi_key_value(key)?;
256
257        let command = PerformActionsBuilder::default()
258            .context(context.clone())
259            .action(
260                KeySourceActionsBuilder::default()
261                    .r#type(KeySourceActionsType::Key)
262                    .id(KEYBOARD_ID)
263                    .action(KeyUpActionBuilder::default()
264                        .r#type(KeyUpActionType::KeyUp)
265                        .value(key_value)
266                        .build().unwrap())
267                    .build().unwrap()
268            )
269            .build().unwrap();
270
271        self.session.lock().await.send(command).await
272            .map_err(|e| InputError::CommandResultError(CommandResultError::SessionSendError(e)))?;
273        tracing::debug!(key, "keyboard up done");
274        Ok(())
275    }
276
277    pub async fn press(
278        &self,
279        key: &str,
280        context: &BrowsingContext,
281        options: Option<KeyPressOptions>,
282    ) -> Result<(), InputError> {
283        tracing::debug!(key, "keyboard press start");
284        let key_value = get_bidi_key_value(key)?;
285        let delay = options.and_then(|o| o.delay);
286        let hold = delay.map(|range| {
287            let mut rng = rand::rng();
288            rng.random_range(range.min..=range.max)
289        });
290
291        let mut key_actions = KeySourceActionsBuilder::default()
292            .r#type(KeySourceActionsType::Key)
293            .id(KEYBOARD_ID)
294            .action(KeyDownActionBuilder::default()
295                .r#type(KeyDownActionType::KeyDown)
296                .value(key_value.clone())
297                .build().unwrap());
298
299        if let Some(h) = hold {
300            if h > 0 {
301                key_actions = key_actions.action(PauseActionBuilder::default()
302                    .r#type(PauseActionType::Pause)
303                    .duration(h)
304                    .build().unwrap());
305            }
306        }
307
308        key_actions = key_actions.action(KeyUpActionBuilder::default()
309            .r#type(KeyUpActionType::KeyUp)
310            .value(key_value)
311            .build().unwrap());
312
313        let command = PerformActionsBuilder::default()
314            .context(context.clone())
315            .action(key_actions.build().unwrap())
316            .build().unwrap();
317
318        self.session.lock().await.send(command).await
319            .map_err(|e| InputError::CommandResultError(CommandResultError::SessionSendError(e)))?;
320        tracing::debug!(key, "keyboard press done");
321        Ok(())
322    }
323
324    pub async fn type_text(
325        &self,
326        text: &str,
327        context: &BrowsingContext,
328        options: Option<KeyboardTypeOptions>,
329    ) -> Result<(), InputError> {
330        tracing::debug!(text, "keyboard type_text start");
331        let options = options.unwrap_or_default();
332        let delay = options.delay;
333        let gap_multiplier = options.gap_multiplier;
334
335        let timings: Vec<(u64, u64)> = match delay {
336            None => vec![],
337            Some(range) => {
338                let mut rng = rand::rng();
339                text.chars().map(|_| {
340                    let hold = rng.random_range(range.min..=range.max);
341                    let gap = ((hold as f64) * gap_multiplier).round() as u64;
342                    (hold, gap)
343                }).collect()
344            }
345        };
346
347        let mut key_actions = KeySourceActionsBuilder::default()
348            .r#type(KeySourceActionsType::Key)
349            .id(KEYBOARD_ID);
350
351        for (i, ch) in text.chars().enumerate() {
352            tracing::debug!(key = %ch, "keyboard type_text key");
353            let key_value = get_bidi_key_value(&ch.to_string())?;
354
355            key_actions = key_actions.action(KeyDownActionBuilder::default()
356                .r#type(KeyDownActionType::KeyDown)
357                .value(key_value.clone())
358                .build().unwrap());
359
360            if let Some(&(hold, _)) = timings.get(i) {
361                if hold > 0 {
362                    key_actions = key_actions.action(PauseActionBuilder::default()
363                        .r#type(PauseActionType::Pause)
364                        .duration(hold)
365                        .build().unwrap());
366                }
367            }
368
369            key_actions = key_actions.action(KeyUpActionBuilder::default()
370                .r#type(KeyUpActionType::KeyUp)
371                .value(key_value)
372                .build().unwrap());
373
374            if let Some(&(_, gap)) = timings.get(i) {
375                if gap > 0 {
376                    key_actions = key_actions.action(PauseActionBuilder::default()
377                        .r#type(PauseActionType::Pause)
378                        .duration(gap)
379                        .build().unwrap());
380                }
381            }
382        }
383
384        let command = PerformActionsBuilder::default()
385            .context(context.clone())
386            .action(key_actions.build().unwrap())
387            .build().unwrap();
388
389        self.session.lock().await.send(command).await
390            .map_err(|e| InputError::CommandResultError(CommandResultError::SessionSendError(e)))?;
391        tracing::debug!("keyboard type_text done");
392        Ok(())
393    }
394}
395
396impl<OT: ConnectionTransport> crate::input::keyboard::Keyboard for BidiKeyboard<OT> {
397    async fn down(&self, key: &str, context: &BrowsingContext) -> Result<(), InputError> {
398        self.down(key, context).await
399    }
400    async fn up(&self, key: &str, context: &BrowsingContext) -> Result<(), InputError> {
401        self.up(key, context).await
402    }
403    async fn press(&self, key: &str, context: &BrowsingContext, options: Option<KeyPressOptions>) -> Result<(), InputError> {
404        self.press(key, context, options).await
405    }
406    async fn type_text(&self, text: &str, context: &BrowsingContext, options: Option<KeyboardTypeOptions>) -> Result<(), InputError> {
407        self.type_text(text, context, options).await
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::get_bidi_key_value;
414
415    #[test]
416    fn single_char_keys_pass_through() {
417        for ch in ['a', 'Z', '5', '!', ' '] {
418            let result = get_bidi_key_value(&ch.to_string()).unwrap();
419            assert_eq!(result, ch.to_string());
420        }
421    }
422
423    #[test]
424    fn special_keys_map_correctly() {
425        let cases = vec![
426            ("Enter", "\u{E007}"),
427            ("Tab", "\u{E004}"),
428            ("Backspace", "\u{E003}"),
429            ("Escape", "\u{E00C}"),
430            ("ArrowUp", "\u{E013}"),
431            ("ArrowDown", "\u{E015}"),
432            ("ArrowLeft", "\u{E012}"),
433            ("ArrowRight", "\u{E014}"),
434            ("F1", "\u{E031}"),
435            ("F12", "\u{E03C}"),
436            ("Shift", "\u{E008}"),
437            ("Control", "\u{E009}"),
438            ("Alt", "\u{E00A}"),
439            ("Meta", "\u{E03D}"),
440            ("Delete", "\u{E017}"),
441            ("Home", "\u{E011}"),
442            ("End", "\u{E010}"),
443            ("PageUp", "\u{E00E}"),
444            ("PageDown", "\u{E00F}"),
445        ];
446        for (key, expected) in cases {
447            assert_eq!(get_bidi_key_value(key).unwrap(), expected, "failed for key: {}", key);
448        }
449    }
450
451    #[test]
452    fn code_style_keys_map_to_chars() {
453        let cases = vec![
454            ("KeyA", "a"), ("KeyZ", "z"),
455            ("Digit0", "0"), ("Digit9", "9"),
456            ("Semicolon", ";"), ("Slash", "/"),
457            ("BracketLeft", "["), ("BracketRight", "]"),
458        ];
459        for (key, expected) in cases {
460            assert_eq!(get_bidi_key_value(key).unwrap(), expected, "failed for key: {}", key);
461        }
462    }
463
464    #[test]
465    fn newline_and_carriage_return_map_to_enter() {
466        assert_eq!(get_bidi_key_value("\n").unwrap(), "\u{E007}");
467        assert_eq!(get_bidi_key_value("\r").unwrap(), "\u{E007}");
468    }
469
470    #[test]
471    fn unknown_key_returns_error() {
472        let result = get_bidi_key_value("NonExistentKey");
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn left_right_modifier_variants() {
478        assert_eq!(get_bidi_key_value("ShiftLeft").unwrap(), "\u{E008}");
479        assert_eq!(get_bidi_key_value("ControlLeft").unwrap(), "\u{E009}");
480        assert_eq!(get_bidi_key_value("AltLeft").unwrap(), "\u{E00A}");
481        assert_eq!(get_bidi_key_value("MetaLeft").unwrap(), "\u{E03D}");
482        assert_eq!(get_bidi_key_value("ShiftRight").unwrap(), "\u{E050}");
483        assert_eq!(get_bidi_key_value("ControlRight").unwrap(), "\u{E051}");
484        assert_eq!(get_bidi_key_value("AltRight").unwrap(), "\u{E052}");
485        assert_eq!(get_bidi_key_value("MetaRight").unwrap(), "\u{E053}");
486    }
487}