Skip to main content

zendriver/input/
mod.rs

1//! Realistic + raw input simulation: mouse paths, keyboard dispatch,
2//! per-tab pointer/modifier state.
3//!
4//! Most user code interacts with the input layer indirectly via
5//! [`crate::Element`] action methods. Public types re-exported here:
6//!
7//! - [`Key`] — single-key dispatch target.
8//! - [`SpecialKey`] — named non-character keys (Enter, F1, ...).
9//! - [`KeyModifiers`] — composable modifier bitflags.
10//! - [`KeySequence`] — mixed text / key / chord builder for `type_keys`.
11//! - [`MouseButton`] — mouse button enum.
12
13use std::sync::Arc;
14
15use rand::SeedableRng;
16use tokio::sync::Mutex;
17use zendriver_stealth::InputProfile;
18
19use crate::input::pointer_state::MouseButtonSet;
20
21pub mod bezier;
22pub mod keyboard;
23pub mod mouse;
24pub mod pointer_state;
25
26pub use keyboard::{Key, KeyModifiers, KeySequence, SpecialKey};
27pub use mouse::MouseButton;
28
29/// Per-Tab input state holder.
30///
31/// Wraps internal cursor + modifier state and a
32/// [`zendriver_stealth::InputProfile`] that controls realism
33/// (typing cadence, mouse jitter). One `InputController` lives on each
34/// [`crate::Tab`]; element actions ([`crate::Element::click`],
35/// [`crate::Element::type_text`], etc.) consult it.
36///
37/// Most user code does not construct this directly — it's built internally
38/// when a [`crate::Tab`] is registered, and accessed via [`crate::Tab::input`].
39#[derive(Debug)]
40pub struct InputController {
41    // Fields are exercised by tests and consumed by later P3 tasks
42    // (mouse dispatch, keyboard dispatch, actionability waits).
43    #[allow(dead_code)]
44    pub(crate) state: Mutex<InputState>,
45    #[allow(dead_code)]
46    pub(crate) profile: InputProfile,
47}
48
49#[allow(dead_code)]
50#[derive(Debug)]
51pub(crate) struct InputState {
52    pub pointer_x: f64,
53    pub pointer_y: f64,
54    pub buttons_held: MouseButtonSet,
55    pub modifiers_held: KeyModifiers,
56    pub rng: rand::rngs::SmallRng,
57}
58
59impl InputController {
60    /// Build an [`InputController`] from a
61    /// [`zendriver_stealth::InputProfile`].
62    ///
63    /// The internal RNG is seeded from OS entropy for unpredictable
64    /// typing/movement jitter.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use zendriver::input::InputController;
70    /// use zendriver_stealth::InputProfile;
71    /// let ic = InputController::new(InputProfile::native());
72    /// # let _ = ic;
73    /// ```
74    #[must_use]
75    pub fn new(profile: InputProfile) -> Arc<Self> {
76        Arc::new(Self {
77            state: Mutex::new(InputState {
78                pointer_x: 0.0,
79                pointer_y: 0.0,
80                buttons_held: MouseButtonSet::empty(),
81                modifiers_held: KeyModifiers::empty(),
82                rng: rand::rngs::SmallRng::from_entropy(),
83            }),
84            profile,
85        })
86    }
87
88    /// Test-only constructor with a seeded RNG for deterministic Bezier paths
89    /// and typing patterns.
90    #[cfg(any(test, feature = "testing"))]
91    #[must_use]
92    pub fn new_with_seed(profile: InputProfile, seed: u64) -> Arc<Self> {
93        Arc::new(Self {
94            state: Mutex::new(InputState {
95                pointer_x: 0.0,
96                pointer_y: 0.0,
97                buttons_held: MouseButtonSet::empty(),
98                modifiers_held: KeyModifiers::empty(),
99                rng: rand::rngs::SmallRng::seed_from_u64(seed),
100            }),
101            profile,
102        })
103    }
104}
105
106#[cfg(test)]
107#[allow(clippy::panic, clippy::unwrap_used)]
108mod tests {
109    use super::*;
110
111    #[tokio::test]
112    async fn new_initializes_zeroed_pointer_and_empty_buttons() {
113        let ic = InputController::new(InputProfile::native());
114        let s = ic.state.lock().await;
115        assert_eq!(s.pointer_x, 0.0);
116        assert_eq!(s.pointer_y, 0.0);
117        assert!(s.buttons_held.is_empty());
118        assert!(s.modifiers_held.is_empty());
119    }
120
121    #[tokio::test]
122    async fn new_with_seed_is_deterministic() {
123        // Two controllers built with the same seed must produce the same
124        // first RNG output. We compare raw u64 draws to avoid coupling this
125        // test to Bezier's specific jitter pattern.
126        let a = InputController::new_with_seed(InputProfile::native(), 42);
127        let b = InputController::new_with_seed(InputProfile::native(), 42);
128        let av = {
129            let mut s = a.state.lock().await;
130            rand::RngCore::next_u64(&mut s.rng)
131        };
132        let bv = {
133            let mut s = b.state.lock().await;
134            rand::RngCore::next_u64(&mut s.rng)
135        };
136        assert_eq!(av, bv);
137    }
138
139    #[test]
140    fn profile_is_stored_verbatim() {
141        let ic = InputController::new(InputProfile::spoofed());
142        assert!(ic.profile.typo_rate > 0.0);
143        let ic2 = InputController::new(InputProfile::native());
144        assert_eq!(ic2.profile.typo_rate, 0.0);
145    }
146}