Skip to main content

rustenium/input/bidi/
keyboard.rs

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