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 builder;
7mod keys;
8mod state;
9
10use std::sync::Arc;
11use std::time::Duration;
12
13use tokio::sync::Mutex;
14use tracing::{debug, instrument};
15use viewpoint_cdp::CdpConnection;
16use viewpoint_cdp::protocol::input::{DispatchKeyEventParams, InsertTextParams, KeyEventType};
17
18use crate::error::LocatorError;
19
20pub use builder::KeyboardPressBuilder;
21pub use keys::{KeyDefinition, get_key_definition};
22use state::{KeyboardState, is_modifier_key, is_uppercase_letter};
23
24/// Keyboard controller for direct keyboard input.
25///
26/// Provides methods for pressing keys, typing text, and managing modifier state.
27///
28/// # Example
29///
30/// ```
31/// # #[cfg(feature = "integration")]
32/// # tokio_test::block_on(async {
33/// # use viewpoint_core::Browser;
34/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
35/// # let context = browser.new_context().await.unwrap();
36/// # let page = context.new_page().await.unwrap();
37/// # page.goto("about:blank").goto().await.unwrap();
38///
39/// // Press a single key
40/// page.keyboard().press("Enter").await.unwrap();
41///
42/// // Type text character by character
43/// page.keyboard().type_text("Hello").await.unwrap();
44///
45/// // Use key combinations
46/// page.keyboard().press("Control+a").await.unwrap();
47///
48/// // Hold modifier and press keys
49/// page.keyboard().down("Shift").await.unwrap();
50/// page.keyboard().press("a").await.unwrap(); // Types 'A'
51/// page.keyboard().up("Shift").await.unwrap();
52/// # });
53/// ```
54#[derive(Debug)]
55pub struct Keyboard {
56    /// CDP connection.
57    connection: Arc<CdpConnection>,
58    /// Session ID for the page.
59    session_id: String,
60    /// Main frame ID for navigation detection.
61    frame_id: String,
62    /// Keyboard state.
63    state: Mutex<KeyboardState>,
64}
65
66impl Keyboard {
67    /// Create a new keyboard controller.
68    pub(crate) fn new(
69        connection: Arc<CdpConnection>,
70        session_id: String,
71        frame_id: String,
72    ) -> Self {
73        Self {
74            connection,
75            session_id,
76            frame_id,
77            state: Mutex::new(KeyboardState::new()),
78        }
79    }
80
81    /// Get the connection (for builder access).
82    pub(crate) fn connection(&self) -> &Arc<CdpConnection> {
83        &self.connection
84    }
85
86    /// Get the session ID (for builder access).
87    pub(crate) fn session_id(&self) -> &str {
88        &self.session_id
89    }
90
91    /// Get the frame ID (for builder access).
92    pub(crate) fn frame_id(&self) -> &str {
93        &self.frame_id
94    }
95
96    /// Press and release a key or key combination.
97    ///
98    /// Returns a builder that can be configured with additional options, or awaited
99    /// directly for a simple key press.
100    ///
101    /// # Arguments
102    ///
103    /// * `key` - Key to press. Can be:
104    ///   - A single key: `"Enter"`, `"a"`, `"F1"`
105    ///   - A key combination: `"Control+c"`, `"Shift+Tab"`
106    ///   - `ControlOrMeta` for cross-platform shortcuts
107    pub fn press(&self, key: &str) -> KeyboardPressBuilder<'_> {
108        KeyboardPressBuilder::new(self, key)
109    }
110
111    /// Internal method to perform the actual key press.
112    pub(crate) async fn press_internal(
113        &self,
114        key: &str,
115        delay: Option<Duration>,
116    ) -> Result<(), LocatorError> {
117        // Parse key combination
118        let parts: Vec<&str> = key.split('+').collect();
119        let actual_key = parts.last().copied().unwrap_or(key);
120
121        // Press modifiers
122        for part in &parts[..parts.len().saturating_sub(1)] {
123            let modifier_key = self.resolve_modifier(part);
124            self.down(&modifier_key).await?;
125        }
126
127        // Check if actual_key is uppercase and we need to add Shift
128        let need_shift = is_uppercase_letter(actual_key);
129        if need_shift {
130            self.down("Shift").await?;
131        }
132
133        // Press the actual key
134        self.down(actual_key).await?;
135
136        if let Some(d) = delay {
137            tokio::time::sleep(d).await;
138        }
139
140        self.up(actual_key).await?;
141
142        // Release Shift if we added it
143        if need_shift {
144            self.up("Shift").await?;
145        }
146
147        // Release modifiers in reverse order
148        for part in parts[..parts.len().saturating_sub(1)].iter().rev() {
149            let modifier_key = self.resolve_modifier(part);
150            self.up(&modifier_key).await?;
151        }
152
153        Ok(())
154    }
155
156    /// Resolve platform-specific modifier keys.
157    fn resolve_modifier(&self, key: &str) -> String {
158        match key {
159            "ControlOrMeta" => {
160                // On macOS use Meta, on other platforms use Control
161                if cfg!(target_os = "macos") {
162                    "Meta".to_string()
163                } else {
164                    "Control".to_string()
165                }
166            }
167            _ => key.to_string(),
168        }
169    }
170
171    /// Press and hold a key.
172    ///
173    /// The key will remain pressed until `up()` is called.
174    #[instrument(level = "debug", skip(self), fields(key = %key))]
175    pub async fn down(&self, key: &str) -> Result<(), LocatorError> {
176        let def = get_key_definition(key)
177            .ok_or_else(|| LocatorError::EvaluationError(format!("Unknown key: {key}")))?;
178
179        let is_repeat = {
180            let mut state = self.state.lock().await;
181            state.key_down(key)
182        };
183
184        let state = self.state.lock().await;
185        let current_modifiers = state.modifiers;
186        drop(state);
187
188        debug!(code = def.code, key = def.key, is_repeat, "Key down");
189
190        let params = DispatchKeyEventParams {
191            event_type: KeyEventType::KeyDown,
192            modifiers: Some(current_modifiers),
193            timestamp: None,
194            text: def.text.map(String::from),
195            unmodified_text: def.text.map(String::from),
196            key_identifier: None,
197            code: Some(def.code.to_string()),
198            key: Some(def.key.to_string()),
199            windows_virtual_key_code: Some(def.key_code),
200            native_virtual_key_code: Some(def.key_code),
201            auto_repeat: Some(is_repeat),
202            is_keypad: Some(def.is_keypad),
203            is_system_key: None,
204            commands: None,
205        };
206
207        self.dispatch_key_event(params).await?;
208
209        // Send char event for printable characters
210        if !is_modifier_key(key) {
211            if let Some(text) = def.text {
212                let char_params = DispatchKeyEventParams {
213                    event_type: KeyEventType::Char,
214                    modifiers: Some(current_modifiers),
215                    timestamp: None,
216                    text: Some(text.to_string()),
217                    unmodified_text: Some(text.to_string()),
218                    key_identifier: None,
219                    code: Some(def.code.to_string()),
220                    key: Some(def.key.to_string()),
221                    windows_virtual_key_code: Some(def.key_code),
222                    native_virtual_key_code: Some(def.key_code),
223                    auto_repeat: None,
224                    is_keypad: Some(def.is_keypad),
225                    is_system_key: None,
226                    commands: None,
227                };
228                self.dispatch_key_event(char_params).await?;
229            }
230        }
231
232        Ok(())
233    }
234
235    /// Release a held key.
236    #[instrument(level = "debug", skip(self), fields(key = %key))]
237    pub async fn up(&self, key: &str) -> Result<(), LocatorError> {
238        let def = get_key_definition(key)
239            .ok_or_else(|| LocatorError::EvaluationError(format!("Unknown key: {key}")))?;
240
241        {
242            let mut state = self.state.lock().await;
243            state.key_up(key);
244        }
245
246        let state = self.state.lock().await;
247        let current_modifiers = state.modifiers;
248        drop(state);
249
250        debug!(code = def.code, key = def.key, "Key up");
251
252        let params = DispatchKeyEventParams {
253            event_type: KeyEventType::KeyUp,
254            modifiers: Some(current_modifiers),
255            timestamp: None,
256            text: None,
257            unmodified_text: None,
258            key_identifier: None,
259            code: Some(def.code.to_string()),
260            key: Some(def.key.to_string()),
261            windows_virtual_key_code: Some(def.key_code),
262            native_virtual_key_code: Some(def.key_code),
263            auto_repeat: None,
264            is_keypad: Some(def.is_keypad),
265            is_system_key: None,
266            commands: None,
267        };
268
269        self.dispatch_key_event(params).await
270    }
271
272    /// Type text character by character with key events.
273    ///
274    /// This generates individual key events for each character.
275    /// Use `insert_text()` for faster text entry without key events.
276    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
277    pub async fn type_text(&self, text: &str) -> Result<(), LocatorError> {
278        self.type_text_with_delay(text, None).await
279    }
280
281    /// Type text with a delay between each character.
282    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
283    pub async fn type_text_with_delay(
284        &self,
285        text: &str,
286        delay: Option<Duration>,
287    ) -> Result<(), LocatorError> {
288        for ch in text.chars() {
289            let char_str = ch.to_string();
290
291            // Get key definition if available, otherwise just send char event
292            if get_key_definition(&char_str).is_some() {
293                // Check if we need Shift for this character
294                let need_shift = ch.is_ascii_uppercase();
295                if need_shift {
296                    self.down("Shift").await?;
297                }
298
299                self.down(&char_str).await?;
300                self.up(&char_str).await?;
301
302                if need_shift {
303                    self.up("Shift").await?;
304                }
305            } else {
306                // For characters without key definitions, send char event directly
307                let params = DispatchKeyEventParams {
308                    event_type: KeyEventType::Char,
309                    modifiers: None,
310                    timestamp: None,
311                    text: Some(char_str.clone()),
312                    unmodified_text: Some(char_str),
313                    key_identifier: None,
314                    code: None,
315                    key: None,
316                    windows_virtual_key_code: None,
317                    native_virtual_key_code: None,
318                    auto_repeat: None,
319                    is_keypad: None,
320                    is_system_key: None,
321                    commands: None,
322                };
323                self.dispatch_key_event(params).await?;
324            }
325
326            if let Some(d) = delay {
327                tokio::time::sleep(d).await;
328            }
329        }
330
331        Ok(())
332    }
333
334    /// Insert text directly without generating key events.
335    ///
336    /// This is faster than `type_text()` and works with non-ASCII characters.
337    /// No keydown/keyup events are dispatched.
338    #[instrument(level = "debug", skip(self), fields(text_len = text.len()))]
339    pub async fn insert_text(&self, text: &str) -> Result<(), LocatorError> {
340        debug!("Inserting text directly");
341
342        self.connection
343            .send_command::<_, serde_json::Value>(
344                "Input.insertText",
345                Some(InsertTextParams {
346                    text: text.to_string(),
347                }),
348                Some(&self.session_id),
349            )
350            .await?;
351
352        Ok(())
353    }
354
355    /// Dispatch a key event to the browser.
356    async fn dispatch_key_event(&self, params: DispatchKeyEventParams) -> Result<(), LocatorError> {
357        self.connection
358            .send_command::<_, serde_json::Value>(
359                "Input.dispatchKeyEvent",
360                Some(params),
361                Some(&self.session_id),
362            )
363            .await?;
364        Ok(())
365    }
366}