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