Skip to main content

browser_control/cli/
show.rs

1//! `show` subcommand: explicitly reveal a controlled browser for login or
2//! debugging. Normal automation keeps browser windows/tabs in the background.
3
4use anyhow::{Context, Result};
5use serde::Serialize;
6
7use crate::cli::env_resolver::Source;
8use crate::cli::mcp::{acquire_bidi_lock_if_needed, resolve_browser};
9use crate::cli::output::print_json;
10use crate::detect::Engine;
11use crate::registry::Registry;
12use crate::session::backend::open_backend;
13
14#[derive(Debug, Serialize)]
15struct ShowResult {
16    name: String,
17    engine: Engine,
18    endpoint: String,
19    target_id: String,
20    os_activated: bool,
21}
22
23pub async fn run(browser: Option<String>, json: bool) -> Result<()> {
24    let registry = Registry::open()?;
25    let resolved = resolve_browser(browser).await?;
26    let name = match &resolved.source {
27        Source::Registered { name } => name.clone(),
28        Source::External => "<external>".to_string(),
29    };
30
31    let _bidi_lock = acquire_bidi_lock_if_needed(&registry, &resolved)?;
32    let backend = open_backend(&resolved.endpoint, resolved.engine).await?;
33    let target_id = backend.target_for_show().await?;
34
35    let os_activated = activate_resolved_app(&registry, &resolved.source)
36        .with_context(|| format!("activating browser app for {name}"))?;
37    backend.show_tab(&target_id).await?;
38
39    let result = ShowResult {
40        name,
41        engine: resolved.engine,
42        endpoint: resolved.endpoint,
43        target_id,
44        os_activated,
45    };
46    if json {
47        print_json(&mut std::io::stdout(), &result)?;
48    } else {
49        println!("Shown {}", result.name);
50        println!("  engine:    {:?}", result.engine);
51        println!("  endpoint:  {}", result.endpoint);
52        println!("  target_id: {}", result.target_id);
53    }
54    Ok(())
55}
56
57pub(crate) fn activate_resolved_app(registry: &Registry, source: &Source) -> Result<bool> {
58    let Source::Registered { name } = source else {
59        return Ok(false);
60    };
61    let Some(row) = registry.get_by_name(name)? else {
62        return Ok(false);
63    };
64    platform_activate(&row.executable)
65}
66
67#[cfg(target_os = "macos")]
68fn platform_activate(executable: &std::path::Path) -> Result<bool> {
69    let Some(app) = app_bundle_for_executable(executable) else {
70        return Ok(false);
71    };
72    let status = std::process::Command::new("open")
73        .arg(&app)
74        .status()
75        .with_context(|| format!("running `open {}`", app.display()))?;
76    if status.success() {
77        Ok(true)
78    } else {
79        anyhow::bail!("`open {}` exited with {status}", app.display())
80    }
81}
82
83#[cfg(not(target_os = "macos"))]
84fn platform_activate(_executable: &std::path::Path) -> Result<bool> {
85    Ok(false)
86}
87
88#[cfg(target_os = "macos")]
89fn app_bundle_for_executable(executable: &std::path::Path) -> Option<std::path::PathBuf> {
90    executable
91        .ancestors()
92        .find(|p| p.extension().and_then(|e| e.to_str()) == Some("app"))
93        .map(std::path::Path::to_path_buf)
94}
95
96#[cfg(test)]
97mod tests {
98    #[cfg(target_os = "macos")]
99    #[test]
100    fn finds_app_bundle_for_macos_executable() {
101        let path =
102            std::path::Path::new("/Applications/Brave Browser.app/Contents/MacOS/Brave Browser");
103        assert_eq!(
104            super::app_bundle_for_executable(path).as_deref(),
105            Some(std::path::Path::new("/Applications/Brave Browser.app"))
106        );
107    }
108}