browser_control/cli/
start.rs1use 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 profile: Option<PathBuf>,
30 json: bool,
31) -> Result<()> {
32 let installed = detect::list_installed();
33 if installed.is_empty() {
34 anyhow::bail!("no supported browsers installed; run `browser-control list-installed`");
35 }
36
37 let resolved_kind: Kind = match browser.as_deref() {
38 None => first_chromium_or_first(&installed)
39 .ok_or_else(|| anyhow!("no chromium-based browser installed"))?,
40 Some(s) => Kind::parse(s).ok_or_else(|| {
41 anyhow!("unknown browser kind `{s}`; valid: chrome, edge, chromium, brave, firefox")
42 })?,
43 };
44
45 let installed_match = installed
46 .iter()
47 .find(|i| i.kind == resolved_kind)
48 .cloned()
49 .ok_or_else(|| {
50 anyhow!(
51 "browser `{}` is not installed on this machine",
52 resolved_kind.as_str()
53 )
54 })?;
55
56 let registry = Registry::open()?;
57 if let Some(row) = registry.first_alive_by_kind(resolved_kind)? {
58 let res = to_result(&row, true);
59 emit(&res, json)?;
60 return Ok(());
61 }
62
63 let name = registry::naming::generate_default(resolved_kind, ®istry)?;
64 let profile_dir = match profile {
65 Some(p) => p,
66 None => paths::profiles_dir()?.join(&name),
67 };
68 std::fs::create_dir_all(&profile_dir).context("creating profile directory")?;
69 let opts = LaunchOpts {
70 headless,
71 profile_dir: profile_dir.clone(),
72 };
73 let handle = launch::launch(&installed_match, opts)
74 .await
75 .with_context(|| format!("launching {}", installed_match.executable.display()))?;
76
77 let row = BrowserRow {
78 name: name.clone(),
79 kind: resolved_kind,
80 engine: handle.engine,
81 pid: handle.pid,
82 endpoint: handle.endpoint.clone(),
83 port: handle.port,
84 profile_dir: handle.profile_dir.clone(),
85 executable: installed_match.executable.clone(),
86 headless,
87 started_at: registry::now_iso8601(),
88 };
89 registry.insert(&row)?;
90 let _pid = handle.forget();
91
92 let res = to_result(&row, false);
93 emit(&res, json)?;
94 Ok(())
95}
96
97fn first_chromium_or_first(installed: &[Installed]) -> Option<Kind> {
98 installed
99 .iter()
100 .find(|i| i.kind.is_chromium())
101 .map(|i| i.kind)
102 .or_else(|| installed.first().map(|i| i.kind))
103}
104
105fn to_result(row: &BrowserRow, reused: bool) -> StartResult {
106 StartResult {
107 name: row.name.clone(),
108 kind: row.kind,
109 pid: row.pid,
110 engine: row.engine,
111 endpoint: row.endpoint.clone(),
112 profile: row.profile_dir.clone(),
113 headless: row.headless,
114 started_at: row.started_at.clone(),
115 reused,
116 }
117}
118
119fn emit(res: &StartResult, json: bool) -> Result<()> {
120 if json {
121 print_json(&mut std::io::stdout(), res)?;
122 } else {
123 let reused = if res.reused { " (reused)" } else { "" };
124 println!("Started {}{}", res.name, reused);
125 println!(" kind: {}", res.kind.as_str());
126 println!(" pid: {}", res.pid);
127 println!(" engine: {:?}", res.engine);
128 println!(" endpoint: {}", res.endpoint);
129 println!(" profile: {}", res.profile.display());
130 }
131 Ok(())
132}