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