Skip to main content

browser_control/cli/
start.rs

1//! `start` subcommand: launch a browser and register it.
2
3use anyhow::{anyhow, Context, Result};
4use serde::Serialize;
5use std::path::PathBuf;
6
7use crate::cli::output::print_json;
8use crate::detect::{self, Engine, Installed, Kind};
9use crate::launch::{self, LaunchOpts};
10use crate::paths;
11use crate::registry::{self, BrowserRow, Registry};
12
13#[derive(Debug, Serialize)]
14pub struct StartResult {
15    pub name: String,
16    pub kind: Kind,
17    pub pid: u32,
18    pub engine: Engine,
19    pub endpoint: String,
20    pub profile: PathBuf,
21    pub headless: bool,
22    pub started_at: String,
23    pub reused: bool,
24}
25
26pub async fn run(
27    browser: Option<String>,
28    headless: bool,
29    no_wait: bool,
30    wait_timeout: u64,
31    json: bool,
32) -> Result<()> {
33    let res = ensure_started(browser, headless, no_wait, wait_timeout).await?;
34    emit(&res, json)?;
35    Ok(())
36}
37
38/// Ensure a browser of the requested kind is running and registered.
39///
40/// This is the programmatic form of `browser-control start`: it reuses the
41/// most recent live registry row for the selected kind, otherwise launches a
42/// new browser with the same default profile semantics as the CLI command.
43pub async fn ensure_started(
44    browser: Option<String>,
45    headless: bool,
46    no_wait: bool,
47    wait_timeout: u64,
48) -> Result<StartResult> {
49    let installed = detect::list_installed();
50    if installed.is_empty() {
51        anyhow::bail!("no supported browsers installed; run `browser-control list-installed`");
52    }
53
54    let resolved_kind: Kind = match browser.as_deref() {
55        None => first_chromium_or_first(&installed)
56            .ok_or_else(|| anyhow!("no chromium-based browser installed"))?,
57        Some(s) => Kind::parse(s).ok_or_else(|| {
58            anyhow!("unknown browser kind `{s}`; valid: chrome, edge, chromium, brave, firefox")
59        })?,
60    };
61
62    let installed_match = installed
63        .iter()
64        .find(|i| i.kind == resolved_kind)
65        .cloned()
66        .ok_or_else(|| {
67            anyhow!(
68                "browser `{}` is not installed on this machine",
69                resolved_kind.as_str()
70            )
71        })?;
72
73    let registry = Registry::open()?;
74    if let Some(row) = registry.first_alive_by_kind(resolved_kind)? {
75        if !no_wait {
76            crate::cli::wait::wait_until_ready(
77                &row.endpoint,
78                row.engine,
79                std::time::Duration::from_secs(wait_timeout),
80            )
81            .await?;
82        }
83        return Ok(to_result(&row, true));
84    }
85
86    let name = registry::naming::generate_default(resolved_kind, &registry)?;
87    let profile_dir = paths::default_profile_dir(resolved_kind)?;
88    std::fs::create_dir_all(&profile_dir).context("creating profile directory")?;
89    let opts = LaunchOpts {
90        headless,
91        profile_dir: profile_dir.clone(),
92    };
93    let handle = launch::launch(&installed_match, opts)
94        .await
95        .with_context(|| format!("launching {}", installed_match.executable.display()))?;
96
97    let row = BrowserRow {
98        name: name.clone(),
99        kind: resolved_kind,
100        engine: handle.engine,
101        pid: handle.pid,
102        endpoint: handle.endpoint.clone(),
103        port: handle.port,
104        profile_dir: handle.profile_dir.clone(),
105        executable: installed_match.executable.clone(),
106        headless,
107        started_at: registry::now_iso8601(),
108    };
109    registry.insert(&row)?;
110    let _pid = handle.forget();
111
112    if !no_wait {
113        crate::cli::wait::wait_until_ready(
114            &row.endpoint,
115            row.engine,
116            std::time::Duration::from_secs(wait_timeout),
117        )
118        .await?;
119    }
120
121    Ok(to_result(&row, false))
122}
123
124pub(crate) fn first_chromium_or_first(installed: &[Installed]) -> Option<Kind> {
125    installed
126        .iter()
127        .find(|i| i.kind.is_chromium())
128        .map(|i| i.kind)
129        .or_else(|| installed.first().map(|i| i.kind))
130}
131
132fn to_result(row: &BrowserRow, reused: bool) -> StartResult {
133    StartResult {
134        name: row.name.clone(),
135        kind: row.kind,
136        pid: row.pid,
137        engine: row.engine,
138        endpoint: row.endpoint.clone(),
139        profile: row.profile_dir.clone(),
140        headless: row.headless,
141        started_at: row.started_at.clone(),
142        reused,
143    }
144}
145
146fn emit(res: &StartResult, json: bool) -> Result<()> {
147    if json {
148        print_json(&mut std::io::stdout(), res)?;
149    } else {
150        let reused = if res.reused { " (reused)" } else { "" };
151        println!("Started {}{}", res.name, reused);
152        println!("  kind:     {}", res.kind.as_str());
153        println!("  pid:      {}", res.pid);
154        println!("  engine:   {:?}", res.engine);
155        println!("  endpoint: {}", res.endpoint);
156        println!("  profile:  {}", res.profile.display());
157    }
158    Ok(())
159}