Skip to main content

browser_control/cli/
key.rs

1//! `browser-control key` — press a key on the focused element.
2//!
3//! Page-context command. The key goes to whatever currently has focus, so it
4//! addresses a tab rather than an element; focus something first (MCP
5//! `browser_click`, or `Tab` your way there).
6//!
7//! Native on both engines — CDP `Input.dispatchKeyEvent`, BiDi
8//! `input.performActions` — so no Node and no Playwright sidecar.
9
10use std::time::Duration;
11
12use anyhow::Result;
13
14use crate::cli::env_resolver::Source;
15use crate::cli::route;
16use crate::cli::trace::CommandTrace;
17use crate::cli::type_cmd::target_matching;
18use crate::session::backend::open_backend;
19use crate::session::keys::{parse_chord, Chord};
20use crate::session::with_scratch_recovery;
21
22const KEY_TIMEOUT: Duration = Duration::from_secs(30);
23
24pub async fn run(browser: Option<String>, key: String, target: Option<String>) -> Result<()> {
25    let mut trace = CommandTrace::new("key");
26    let result = run_inner(browser, &key, target, &mut trace).await;
27    trace.finish(result)?;
28    println!("pressed {key}");
29    Ok(())
30}
31
32async fn run_inner(
33    browser: Option<String>,
34    key: &str,
35    target: Option<String>,
36    trace: &mut CommandTrace,
37) -> Result<()> {
38    // Parse before touching the browser: a typo should cost nothing and say
39    // what was wrong.
40    let chord = parse_chord(key)?;
41
42    let r = route::preamble(browser, target.as_deref(), trace).await?;
43    let resolved = &r.resolved;
44
45    match (r.tab_name.clone(), target) {
46        // Path 1: <browser>/<tab> — named tab with recover-once.
47        (Some(name), None) => {
48            trace.route("named-tab").tab_name(&name);
49            let chord = chord.clone();
50            route::run_named_tab(
51                &r,
52                &name,
53                "named tabs (`<browser>/<name>`) require a registered browser; \
54                 external endpoints can't carry tab names",
55                move |b, target_id| {
56                    let chord: Chord = chord.clone();
57                    async move { b.press_key_on_tab(&target_id, &chord, KEY_TIMEOUT).await }
58                },
59            )
60            .await
61        }
62        // Path 2: bare browser → scratch tab with recover-once.
63        (None, None) => {
64            let browser_name = match &resolved.source {
65                Source::Registered { name } => name.clone(),
66                // A key press only makes sense against a tab we can name; an
67                // external endpoint has no registry row to key a scratch by.
68                Source::External => anyhow::bail!(
69                    "`key` needs a registered browser or an explicit `--target`; \
70                     external endpoints have no tab to focus"
71                ),
72            };
73            trace.route("scratch");
74            let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
75            with_scratch_recovery(&backend, &r.registry, &browser_name, move |b, target_id| {
76                let chord: Chord = chord.clone();
77                async move { b.press_key_on_tab(&target_id, &chord, KEY_TIMEOUT).await }
78            })
79            .await
80        }
81        // Path 3: bare browser, --target regex. Resolved through the backend
82        // we then use, not a throwaway PageSession: BiDi permits only one
83        // session per browser, so attaching twice fails with "Maximum number
84        // of active sessions".
85        (None, Some(regex)) => {
86            trace.route("target-regex");
87            let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
88            let target_id = target_matching(&backend, &regex).await?;
89            let out = backend
90                .press_key_on_tab(&target_id, &chord, KEY_TIMEOUT)
91                .await;
92            backend.release().await;
93            out
94        }
95        _ => unreachable!("mutual exclusion checked in preamble"),
96    }
97}