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/// ```
93/// # #[cfg(feature = "integration")]
94/// # tokio_test::block_on(async {
95/// # use viewpoint_core::Browser;
96/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
97/// # let context = browser.new_context().await.unwrap();
98/// # let page = context.new_page().await.unwrap();
99/// # page.goto("about:blank").goto().await.unwrap();
100///
101/// // Press a single key
102/// page.keyboard().press("Enter").await.unwrap();
103///
104/// // Type text character by character
105/// page.keyboard().type_text("Hello").await.unwrap();
106///
107/// // Use key combinations
108/// page.keyboard().press("Control+a").await.unwrap();
109///
110/// // Hold modifier and press keys
111/// page.keyboard().down("Shift").await.unwrap();
112/// page.keyboard().press("a").await.unwrap(); // Types 'A'
113/// page.keyboard().up("Shift").await.unwrap();
114/// # });
115/// ```
116#[derive(Debug)]
117pub struct Keyboard {
118    /// CDP connection.
119    connection: Arc<CdpConnection>,
120    /// Session ID for the page.
121    session_id: String,
122    /// Keyboard state.
123    state: Mutex<KeyboardState>,
124}
125
126impl Keyboard {
127    /// Create a new keyboard controller.
128    pub(crate) fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
129        Self {
130            connection,
131            session_id,
132            state: Mutex::new(KeyboardState::new()),
133        }
134    }
135
136    /// Press and release a key or key combination.
137    ///
138    /// # Arguments
139    ///
140    /// * `key` - Key to press. Can be:
141    ///   - A single key: `"Enter"`, `"a"`, `"F1"`
142    ///   - A key combination: `"Control+c"`, `"Shift+Tab"`
143    ///   - `ControlOrMeta` for cross-platform shortcuts
144    ///
145    /// # Example
146    ///
147    /// ```no_run
148    /// use viewpoint_core::Page;
149    ///
150    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
151    /// page.keyboard().press("Enter").await?;
152    /// page.keyboard().press("Control+a").await?;
153    /// page.keyboard().press("ControlOrMeta+c").await?;
154    /// # Ok(())
155    /// # }
156    /// ```
157    #[instrument(level = "debug", skip(self), fields(key = %key))]
158    pub async fn press(&self, key: &str) -> Result<(), LocatorError> {
159        self.press_with_delay(key, None).await
160    }
161
162    /// Press and release a key with a delay between down and up.
163    #[instrument(level = "debug", skip(self), fields(key = %key))]
164    pub async fn press_with_delay(
165        &self,
166        key: &str,
167        delay: Option<Duration>,
168    ) -> Result<(), LocatorError> {
169        // Parse key combination
170        let parts: Vec<&str> = key.split('+').collect();
171        let actual_key = parts.last().copied().unwrap_or(key);
172
173        // Press modifiers
174        for part in &parts[..parts.len().saturating_sub(1)] {
175            let modifier_key = self.resolve_modifier(part);
176            self.down(&modifier_key).await?;
177        }
178
179        // Check if actual_key is uppercase and we need to add Shift
180        let need_shift = is_uppercase_letter(actual_key);
181        if need_shift {
182            self.down("Shift").await?;
183        }
184
185        // Press the actual key
186        self.down(actual_key).await?;
187
188        if let Some(d) = delay {
189            tokio::time::sleep(d).await;
190        }
191
192        self.up(actual_key).await?;
193
194        // Release Shift if we added it
195        if need_shift {
196            self.up("Shift").await?;
197        }
198
199        // Release modifiers in reverse order
200        for part in parts[..parts.len().saturating_sub(1)].iter().rev() {
201            let modifier_key = self.resolve_modifier(part);
202            self.up(&modifier_key).await?;
203        }
204
205        Ok(())
206    }
207
208    /// Resolve platform-specific modifier keys.
209    fn resolve_modifier(&self, key: &str) -> String {
210        match key {
211            "ControlOrMeta" => {
212                // On macOS use Meta, on other platforms use Control
213                if cfg!(target_os = "macos") {
214                    "Meta".to_string()
215                } else {
216                    "Control".to_string()
217                }
218            }
219            _ => key.to_string(),
220        }
221    }
222
223    /// Press and hold a key.
224    ///
225    /// The key will remain pressed until `up()` is called.
226    ///
227    /// # Example
228    ///
229    /// ```no_run
230    /// use viewpoint_core::Page;
231    ///
232    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
233    /// page.keyboard().down("Shift").await?;
234    /// page.keyboard().press("a").await?; // Types 'A'
235    /// page.keyboard().up("Shift").await?;
236    /// # Ok(())
237    /// # }
238    /// ```
239    #[instrument(level = "debug", skip(self), fields(key = %key))]
240    pub async fn down(&self, key: &str) -> Result<(), LocatorError> {
241        let def = get_key_definition(key).ok_or_else(|| {
242            LocatorError::EvaluationError(format!("Unknown key: {key}"))
243        })?;
244
245        let is_repeat = {
246            let mut state = self.state.lock().await;
247            state.key_down(key)
248        };
249
250        let state = self.state.lock().await;
251        let current_modifiers = state.modifiers;
252        drop(state);
253
254        debug!(code = def.code, key = def.key, is_repeat, "Key down");
255
256        let params = DispatchKeyEventParams {
257            event_type: KeyEventType::KeyDown,
258            modifiers: Some(current_modifiers),
259            timestamp: None,
260            text: def.text.map(String::from),
261            unmodified_text: def.text.map(String::from),
262            key_identifier: None,
263            code: Some(def.code.to_string()),
264            key: Some(def.key.to_string()),
265            windows_virtual_key_code: Some(def.key_code),
266            native_virtual_key_code: Some(def.key_code),
267            auto_repeat: Some(is_repeat),
268            is_keypad: Some(def.is_keypad),
269            is_system_key: None,
270            commands: None,
271        };
272
273        self.dispatch_key_event(params).await?;
274
275        // Send char event for printable characters
276        if !is_modifier_key(key) {
277            if let Some(text) = def.text {
278                let char_params = DispatchKeyEventParams {
279                    event_type: KeyEventType::Char,
280                    modifiers: Some(current_modifiers),
281                    timestamp: None,
282                    text: Some(text.to_string()),
283                    unmodified_text: Some(text.to_string()),
284                    key_identifier: None,
285                    code: Some(def.code.to_string()),
286                    key: Some(def.key.to_string()),
287                    windows_virtual_key_code: Some(def.key_code),
288                    native_virtual_key_code: Some(def.key_code),
289                    auto_repeat: None,
290                    is_keypad: Some(def.is_keypad),
291                    is_system_key: None,
292                    commands: None,
293                };
294                self.dispatch_key_event(char_params).await?;
295            }
296        }
297
298        Ok(())
299    }
300
301    /// Release a held key.
302    ///
303    /// # Example
304    ///
305    /// ```no_run
306    /// use viewpoint_core::Page;
307    ///
308    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
309    /// page.keyboard().down("Shift").await?;
310    /// // ... do stuff with Shift held
311    /// page.keyboard().up("Shift").await?;
312    /// # Ok(())
313    /// # }
314    /// ```
315    #[instrument(level = "debug", skip(self), fields(key = %key))]
316    pub async fn up(&self, key: &str) -> Result<(), LocatorError> {
317        let def = get_key_definition(key).ok_or_else(|| {
318            LocatorError::EvaluationError(format!("Unknown key: {key}"))
319        })?;
320
321        {
322            let mut state = self.state.lock().await;
323            state.key_up(key);
324        }
325
326        let state = self.state.lock().await;
327        let current_modifiers = state.modifiers;
328        drop(state);
329
330        debug!(code = def.code, key = def.key, "Key up");
331
332        let params = DispatchKeyEventParams {
333            event_type: KeyEventType::KeyUp,
334            modifiers: Some(current_modifiers),
335            timestamp: None,
336            text: None,
337            unmodified_text: None,
338            key_identifier: None,
339            code: Some(def.code.to_string()),
340            key: Some(def.key.to_string()),
341            windows_virtual_key_code: Some(def.key_code),
342            native_virtual_key_code: Some(def.key_code),
343            auto_repeat: None,
344            is_keypad: Some(def.is_keypad),
345            is_system_key: None,
346            commands: None,
347        };
348
349        self.dispatch_key_event(params).await
350    }
351
352    /// Type text character by character with key events.
353    ///
354    /// This generates individual key events for each character.
355    /// Use `insert_text()` for faster text entry without key events.
356    ///
357    /// # Example
358    ///
359    /// ```no_run
360    /// use viewpoint_core::Page;
361    ///
362    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
363    /// page.keyboard().type_text("Hello, World!").await?;
364    /// # Ok(())
365    /// # }
366    /// ```
367    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
368    pub async fn type_text(&self, text: &str) -> Result<(), LocatorError> {
369        self.type_text_with_delay(text, None).await
370    }
371
372    /// Type text with a delay between each character.
373    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
374    pub async fn type_text_with_delay(
375        &self,
376        text: &str,
377        delay: Option<Duration>,
378    ) -> Result<(), LocatorError> {
379        for ch in text.chars() {
380            let char_str = ch.to_string();
381
382            // Get key definition if available, otherwise just send char event
383            if let Some(_def) = get_key_definition(&char_str) {
384                // Check if we need Shift for this character
385                let need_shift = ch.is_ascii_uppercase();
386                if need_shift {
387                    self.down("Shift").await?;
388                }
389
390                self.down(&char_str).await?;
391                self.up(&char_str).await?;
392
393                if need_shift {
394                    self.up("Shift").await?;
395                }
396            } else {
397                // For characters without key definitions, send char event directly
398                let params = DispatchKeyEventParams {
399                    event_type: KeyEventType::Char,
400                    modifiers: None,
401                    timestamp: None,
402                    text: Some(char_str.clone()),
403                    unmodified_text: Some(char_str),
404                    key_identifier: None,
405                    code: None,
406                    key: None,
407                    windows_virtual_key_code: None,
408                    native_virtual_key_code: None,
409                    auto_repeat: None,
410                    is_keypad: None,
411                    is_system_key: None,
412                    commands: None,
413                };
414                self.dispatch_key_event(params).await?;
415            }
416
417            if let Some(d) = delay {
418                tokio::time::sleep(d).await;
419            }
420        }
421
422        Ok(())
423    }
424
425    /// Insert text directly without generating key events.
426    ///
427    /// This is faster than `type_text()` and works with non-ASCII characters.
428    /// No keydown/keyup events are dispatched.
429    ///
430    /// # Example
431    ///
432    /// ```no_run
433    /// use viewpoint_core::Page;
434    ///
435    /// # async fn example(page: &Page) -> Result<(), viewpoint_core::CoreError> {
436    /// page.keyboard().insert_text("Hello 👋 你好").await?;
437    /// # Ok(())
438    /// # }
439    /// ```
440    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
441    pub async fn insert_text(&self, text: &str) -> Result<(), LocatorError> {
442        debug!("Inserting text directly");
443
444        self.connection
445            .send_command::<_, serde_json::Value>(
446                "Input.insertText",
447                Some(InsertTextParams {
448                    text: text.to_string(),
449                }),
450                Some(&self.session_id),
451            )
452            .await?;
453
454        Ok(())
455    }
456
457    /// Dispatch a key event to the browser.
458    async fn dispatch_key_event(&self, params: DispatchKeyEventParams) -> Result<(), LocatorError> {
459        self.connection
460            .send_command::<_, serde_json::Value>(
461                "Input.dispatchKeyEvent",
462                Some(params),
463                Some(&self.session_id),
464            )
465            .await?;
466        Ok(())
467    }
468}