use crate::backend::BackendKind;
use crate::context::ContextRef;
use crate::options::LaunchOptions;
use crate::page::Page;
use crate::state::{BrowserState, ConnectMode};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct Browser {
state: Arc<RwLock<BrowserState>>,
backend_kind: BackendKind,
}
impl Browser {
pub async fn launch(options: LaunchOptions) -> Result<Self, String> {
let mode = if let Some(url) = &options.ws_endpoint {
ConnectMode::ConnectUrl(url.clone())
} else if let Some(ac) = &options.auto_connect {
ConnectMode::AutoConnect {
channel: ac.channel.clone(),
user_data_dir: ac.user_data_dir.clone(),
}
} else {
ConnectMode::Launch
};
let backend_kind = options.backend;
let mut state = BrowserState::with_options(mode, options);
Box::pin(state.ensure_browser()).await?;
Ok(Self {
state: Arc::new(RwLock::new(state)),
backend_kind,
})
}
pub async fn connect(url: &str) -> Result<Self, String> {
Box::pin(Self::launch(LaunchOptions {
ws_endpoint: Some(url.to_string()),
..Default::default()
}))
.await
}
pub fn from_shared_state(state: Arc<RwLock<BrowserState>>, backend_kind: BackendKind) -> Self {
Self { state, backend_kind }
}
pub fn new_context(&self) -> ContextRef {
static CTX_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let id = CTX_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let name = format!("context-{id}");
ContextRef::new(self.state.clone(), name)
}
#[must_use]
pub fn default_context(&self) -> ContextRef {
ContextRef::new(self.state.clone(), "default".to_string())
}
pub async fn new_page(&self) -> Result<Arc<Page>, String> {
Box::pin(self.default_context().new_page()).await
}
pub async fn new_page_with_url(&self, url: &str) -> Result<Arc<Page>, String> {
let page = Box::pin(self.new_page()).await?;
page.goto(url, None).await?;
Ok(page)
}
pub async fn page(&self) -> Result<Arc<Page>, String> {
let ctx = self.default_context();
let mut pages = ctx.pages().await.unwrap_or_default();
if pages.is_empty() {
Box::pin(ctx.new_page()).await
} else {
Ok(pages.swap_remove(0))
}
}
pub async fn close(&self) -> Result<(), String> {
let mut state = self.state.write().await;
state.shutdown().await;
Ok(())
}
#[must_use]
pub fn state(&self) -> &Arc<RwLock<BrowserState>> {
&self.state
}
pub async fn contexts(&self) -> Vec<ContextRef> {
let state = self.state.read().await;
state
.list_contexts()
.await
.iter()
.map(|c| ContextRef::new(self.state.clone(), c.name.clone()))
.collect()
}
#[must_use]
pub fn version(&self) -> &'static str {
match self.backend_kind {
BackendKind::CdpPipe | BackendKind::CdpRaw => "Chromium",
#[cfg(target_os = "macos")]
BackendKind::WebKit => "WebKit",
BackendKind::Bidi => "BiDi",
}
}
pub async fn is_connected(&self) -> bool {
let state = self.state.read().await;
state.is_connected()
}
}