viewpoint_core/page/keyboard/
mod.rs

1//! Keyboard input handling.
2//!
3//! Provides direct keyboard control for simulating key presses, key holds,
4//! and text input.
5
6mod keys;
7
8use std::collections::HashSet;
9use std::sync::Arc;
10use std::time::Duration;
11
12use tokio::sync::Mutex;
13use tracing::{debug, instrument};
14use viewpoint_cdp::protocol::input::{
15    DispatchKeyEventParams, InsertTextParams, KeyEventType, modifiers,
16};
17use viewpoint_cdp::CdpConnection;
18
19use crate::error::LocatorError;
20
21pub use keys::{KeyDefinition, get_key_definition};
22
23/// Check if a key is an uppercase letter that requires Shift.
24fn is_uppercase_letter(key: &str) -> bool {
25    key.len() == 1 && key.chars().next().is_some_and(|c| c.is_ascii_uppercase())
26}
27
28/// Check if a key is a modifier key.
29fn is_modifier_key(key: &str) -> bool {
30    matches!(
31        key,
32        "Alt" | "AltLeft" | "AltRight"
33            | "Control" | "ControlLeft" | "ControlRight"
34            | "Meta" | "MetaLeft" | "MetaRight"
35            | "Shift" | "ShiftLeft" | "ShiftRight"
36    )
37}
38
39/// Keyboard state tracking.
40#[derive(Debug)]
41struct KeyboardState {
42    /// Currently pressed modifier keys.
43    modifiers: i32,
44    /// Set of currently held keys.
45    pressed_keys: HashSet<String>,
46}
47
48impl KeyboardState {
49    fn new() -> Self {
50        Self {
51            modifiers: 0,
52            pressed_keys: HashSet::new(),
53        }
54    }
55
56    fn key_down(&mut self, key: &str) -> bool {
57        let is_repeat = self.pressed_keys.contains(key);
58        self.pressed_keys.insert(key.to_string());
59
60        // Update modifiers
61        match key {
62            "Alt" | "AltLeft" | "AltRight" => self.modifiers |= modifiers::ALT,
63            "Control" | "ControlLeft" | "ControlRight" => self.modifiers |= modifiers::CTRL,
64            "Meta" | "MetaLeft" | "MetaRight" => self.modifiers |= modifiers::META,
65            "Shift" | "ShiftLeft" | "ShiftRight" => self.modifiers |= modifiers::SHIFT,
66            _ => {}
67        }
68
69        is_repeat
70    }
71
72    fn key_up(&mut self, key: &str) {
73        self.pressed_keys.remove(key);
74
75        // Update modifiers
76        match key {
77            "Alt" | "AltLeft" | "AltRight" => self.modifiers &= !modifiers::ALT,
78            "Control" | "ControlLeft" | "ControlRight" => self.modifiers &= !modifiers::CTRL,
79            "Meta" | "MetaLeft" | "MetaRight" => self.modifiers &= !modifiers::META,
80            "Shift" | "ShiftLeft" | "ShiftRight" => self.modifiers &= !modifiers::SHIFT,
81            _ => {}
82        }
83    }
84}
85
86/// Keyboard controller for direct keyboard input.
87///
88/// Provides methods for pressing keys, typing text, and managing modifier state.
89///
90/// # Example
91///
92/// ```ignore
93/// // Press a single key
94/// page.keyboard().press("Enter").await?;
95///
96/// // Type text character by character
97/// page.keyboard().type_text("Hello").await?;
98///
99/// // Use key combinations
100/// page.keyboard().press("Control+a").await?;
101///
102/// // Hold modifier and press keys
103/// page.keyboard().down("Shift").await?;
104/// page.keyboard().press("a").await?; // Types 'A'
105/// page.keyboard().up("Shift").await?;
106/// ```
107#[derive(Debug)]
108pub struct Keyboard {
109    /// CDP connection.
110    connection: Arc<CdpConnection>,
111    /// Session ID for the page.
112    session_id: String,
113    /// Keyboard state.
114    state: Mutex<KeyboardState>,
115}
116
117impl Keyboard {
118    /// Create a new keyboard controller.
119    pub(crate) fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
120        Self {
121            connection,
122            session_id,
123            state: Mutex::new(KeyboardState::new()),
124        }
125    }
126
127    /// Press and release a key or key combination.
128    ///
129    /// # Arguments
130    ///
131    /// * `key` - Key to press. Can be:
132    ///   - A single key: `"Enter"`, `"a"`, `"F1"`
133    ///   - A key combination: `"Control+c"`, `"Shift+Tab"`
134    ///   - `ControlOrMeta` for cross-platform shortcuts
135    ///
136    /// # Example
137    ///
138    /// ```ignore
139    /// page.keyboard().press("Enter").await?;
140    /// page.keyboard().press("Control+a").await?;
141    /// page.keyboard().press("ControlOrMeta+c").await?;
142    /// ```
143    #[instrument(level = "debug", skip(self), fields(key = %key))]
144    pub async fn press(&self, key: &str) -> Result<(), LocatorError> {
145        self.press_with_delay(key, None).await
146    }
147
148    /// Press and release a key with a delay between down and up.
149    #[instrument(level = "debug", skip(self), fields(key = %key))]
150    pub async fn press_with_delay(
151        &self,
152        key: &str,
153        delay: Option<Duration>,
154    ) -> Result<(), LocatorError> {
155        // Parse key combination
156        let parts: Vec<&str> = key.split('+').collect();
157        let actual_key = parts.last().copied().unwrap_or(key);
158
159        // Press modifiers
160        for part in &parts[..parts.len().saturating_sub(1)] {
161            let modifier_key = self.resolve_modifier(part);
162            self.down(&modifier_key).await?;
163        }
164
165        // Check if actual_key is uppercase and we need to add Shift
166        let need_shift = is_uppercase_letter(actual_key);
167        if need_shift {
168            self.down("Shift").await?;
169        }
170
171        // Press the actual key
172        self.down(actual_key).await?;
173
174        if let Some(d) = delay {
175            tokio::time::sleep(d).await;
176        }
177
178        self.up(actual_key).await?;
179
180        // Release Shift if we added it
181        if need_shift {
182            self.up("Shift").await?;
183        }
184
185        // Release modifiers in reverse order
186        for part in parts[..parts.len().saturating_sub(1)].iter().rev() {
187            let modifier_key = self.resolve_modifier(part);
188            self.up(&modifier_key).await?;
189        }
190
191        Ok(())
192    }
193
194    /// Resolve platform-specific modifier keys.
195    fn resolve_modifier(&self, key: &str) -> String {
196        match key {
197            "ControlOrMeta" => {
198                // On macOS use Meta, on other platforms use Control
199                if cfg!(target_os = "macos") {
200                    "Meta".to_string()
201                } else {
202                    "Control".to_string()
203                }
204            }
205            _ => key.to_string(),
206        }
207    }
208
209    /// Press and hold a key.
210    ///
211    /// The key will remain pressed until `up()` is called.
212    ///
213    /// # Example
214    ///
215    /// ```ignore
216    /// page.keyboard().down("Shift").await?;
217    /// page.keyboard().press("a").await?; // Types 'A'
218    /// page.keyboard().up("Shift").await?;
219    /// ```
220    #[instrument(level = "debug", skip(self), fields(key = %key))]
221    pub async fn down(&self, key: &str) -> Result<(), LocatorError> {
222        let def = get_key_definition(key).ok_or_else(|| {
223            LocatorError::EvaluationError(format!("Unknown key: {key}"))
224        })?;
225
226        let is_repeat = {
227            let mut state = self.state.lock().await;
228            state.key_down(key)
229        };
230
231        let state = self.state.lock().await;
232        let current_modifiers = state.modifiers;
233        drop(state);
234
235        debug!(code = def.code, key = def.key, is_repeat, "Key down");
236
237        let params = DispatchKeyEventParams {
238            event_type: KeyEventType::KeyDown,
239            modifiers: Some(current_modifiers),
240            timestamp: None,
241            text: def.text.map(String::from),
242            unmodified_text: def.text.map(String::from),
243            key_identifier: None,
244            code: Some(def.code.to_string()),
245            key: Some(def.key.to_string()),
246            windows_virtual_key_code: Some(def.key_code),
247            native_virtual_key_code: Some(def.key_code),
248            auto_repeat: Some(is_repeat),
249            is_keypad: Some(def.is_keypad),
250            is_system_key: None,
251            commands: None,
252        };
253
254        self.dispatch_key_event(params).await?;
255
256        // Send char event for printable characters
257        if !is_modifier_key(key) {
258            if let Some(text) = def.text {
259                let char_params = DispatchKeyEventParams {
260                    event_type: KeyEventType::Char,
261                    modifiers: Some(current_modifiers),
262                    timestamp: None,
263                    text: Some(text.to_string()),
264                    unmodified_text: Some(text.to_string()),
265                    key_identifier: None,
266                    code: Some(def.code.to_string()),
267                    key: Some(def.key.to_string()),
268                    windows_virtual_key_code: Some(def.key_code),
269                    native_virtual_key_code: Some(def.key_code),
270                    auto_repeat: None,
271                    is_keypad: Some(def.is_keypad),
272                    is_system_key: None,
273                    commands: None,
274                };
275                self.dispatch_key_event(char_params).await?;
276            }
277        }
278
279        Ok(())
280    }
281
282    /// Release a held key.
283    ///
284    /// # Example
285    ///
286    /// ```ignore
287    /// page.keyboard().down("Shift").await?;
288    /// // ... do stuff with Shift held
289    /// page.keyboard().up("Shift").await?;
290    /// ```
291    #[instrument(level = "debug", skip(self), fields(key = %key))]
292    pub async fn up(&self, key: &str) -> Result<(), LocatorError> {
293        let def = get_key_definition(key).ok_or_else(|| {
294            LocatorError::EvaluationError(format!("Unknown key: {key}"))
295        })?;
296
297        {
298            let mut state = self.state.lock().await;
299            state.key_up(key);
300        }
301
302        let state = self.state.lock().await;
303        let current_modifiers = state.modifiers;
304        drop(state);
305
306        debug!(code = def.code, key = def.key, "Key up");
307
308        let params = DispatchKeyEventParams {
309            event_type: KeyEventType::KeyUp,
310            modifiers: Some(current_modifiers),
311            timestamp: None,
312            text: None,
313            unmodified_text: None,
314            key_identifier: None,
315            code: Some(def.code.to_string()),
316            key: Some(def.key.to_string()),
317            windows_virtual_key_code: Some(def.key_code),
318            native_virtual_key_code: Some(def.key_code),
319            auto_repeat: None,
320            is_keypad: Some(def.is_keypad),
321            is_system_key: None,
322            commands: None,
323        };
324
325        self.dispatch_key_event(params).await
326    }
327
328    /// Type text character by character with key events.
329    ///
330    /// This generates individual key events for each character.
331    /// Use `insert_text()` for faster text entry without key events.
332    ///
333    /// # Example
334    ///
335    /// ```ignore
336    /// page.keyboard().type_text("Hello, World!").await?;
337    /// ```
338    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
339    pub async fn type_text(&self, text: &str) -> Result<(), LocatorError> {
340        self.type_text_with_delay(text, None).await
341    }
342
343    /// Type text with a delay between each character.
344    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
345    pub async fn type_text_with_delay(
346        &self,
347        text: &str,
348        delay: Option<Duration>,
349    ) -> Result<(), LocatorError> {
350        for ch in text.chars() {
351            let char_str = ch.to_string();
352
353            // Get key definition if available, otherwise just send char event
354            if let Some(_def) = get_key_definition(&char_str) {
355                // Check if we need Shift for this character
356                let need_shift = ch.is_ascii_uppercase();
357                if need_shift {
358                    self.down("Shift").await?;
359                }
360
361                self.down(&char_str).await?;
362                self.up(&char_str).await?;
363
364                if need_shift {
365                    self.up("Shift").await?;
366                }
367            } else {
368                // For characters without key definitions, send char event directly
369                let params = DispatchKeyEventParams {
370                    event_type: KeyEventType::Char,
371                    modifiers: None,
372                    timestamp: None,
373                    text: Some(char_str.clone()),
374                    unmodified_text: Some(char_str),
375                    key_identifier: None,
376                    code: None,
377                    key: None,
378                    windows_virtual_key_code: None,
379                    native_virtual_key_code: None,
380                    auto_repeat: None,
381                    is_keypad: None,
382                    is_system_key: None,
383                    commands: None,
384                };
385                self.dispatch_key_event(params).await?;
386            }
387
388            if let Some(d) = delay {
389                tokio::time::sleep(d).await;
390            }
391        }
392
393        Ok(())
394    }
395
396    /// Insert text directly without generating key events.
397    ///
398    /// This is faster than `type_text()` and works with non-ASCII characters.
399    /// No keydown/keyup events are dispatched.
400    ///
401    /// # Example
402    ///
403    /// ```ignore
404    /// page.keyboard().insert_text("Hello 👋 你好").await?;
405    /// ```
406    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
407    pub async fn insert_text(&self, text: &str) -> Result<(), LocatorError> {
408        debug!("Inserting text directly");
409
410        self.connection
411            .send_command::<_, serde_json::Value>(
412                "Input.insertText",
413                Some(InsertTextParams {
414                    text: text.to_string(),
415                }),
416                Some(&self.session_id),
417            )
418            .await?;
419
420        Ok(())
421    }
422
423    /// Dispatch a key event to the browser.
424    async fn dispatch_key_event(&self, params: DispatchKeyEventParams) -> Result<(), LocatorError> {
425        self.connection
426            .send_command::<_, serde_json::Value>(
427                "Input.dispatchKeyEvent",
428                Some(params),
429                Some(&self.session_id),
430            )
431            .await?;
432        Ok(())
433    }
434}