mod chrome;
#[cfg(target_family = "windows")]
use chrome::close_handle;
use chrome::{bind, bounds, close, eval, load, load_css, load_js, set_bounds, Chrome};
pub use chrome::{BindingContext, Bounds, JSError, JSObject, JSResult, WindowState};
mod locate;
pub use locate::tinyfiledialogs as dialog;
use locate::{locate_chrome, LocateChromeError};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
const DEFAULT_CHROME_ARGS: &[&str] = &[
"--disable-background-networking",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-infobars",
"--disable-extensions",
"--disable-features=site-per-process",
"--disable-hang-monitor",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-sync",
"--disable-translate",
"--disable-windows10-custom-titlebar",
"--metrics-recording-only",
"--no-first-run",
"--no-default-browser-check",
"--safebrowsing-disable-auto-update",
"--password-store=basic",
"--use-mock-keychain",
];
pub struct UI {
chrome: Arc<Chrome>,
_tmpdir: Option<tempfile::TempDir>,
waited: AtomicBool,
}
#[derive(Debug, thiserror::Error)]
pub enum UILaunchError {
#[error("Cannot create temporary directory: {0}")]
TempDirectoryCreationError(#[from] std::io::Error),
#[error("The path {0} specified by ALCRO_BROWSER_PATH does not exist")]
BrowserPathInvalid(String),
#[error("Error in locating chrome: {0}")]
LocateChromeError(#[from] LocateChromeError),
#[error("Error when initializing chrome: {0}")]
ChromeInitError(#[from] JSError),
}
impl UI {
fn new(
url: &str,
dir: Option<&std::path::Path>,
width: i32,
height: i32,
custom_args: &[&str],
) -> Result<UI, UILaunchError> {
let _tmpdir;
let dir = match dir {
Some(dir) => {
_tmpdir = None;
dir
}
None => {
_tmpdir = Some(tempfile::TempDir::new()?);
_tmpdir.as_ref().unwrap().path()
}
};
let mut args = Vec::from(DEFAULT_CHROME_ARGS);
let user_data_dir_arg = format!("--user-data-dir={}", dir.to_str().unwrap());
args.push(&user_data_dir_arg);
let window_size_arg = format!("--window-size={},{}", width, height);
args.push(&window_size_arg);
for arg in custom_args {
args.push(arg)
}
args.push("--remote-debugging-pipe");
let app_arg;
if custom_args.contains(&"--headless") {
args.push(url);
} else {
app_arg = format!("--app={}", url);
args.push(&app_arg);
}
let chrome_path = match std::env::var("ALCRO_BROWSER_PATH") {
Ok(path) => {
if std::fs::metadata(&path).is_ok() {
path
} else {
return Err(UILaunchError::BrowserPathInvalid(path));
}
}
Err(_) => locate_chrome()?,
};
let chrome = Chrome::new_with_args(&chrome_path, &args)?;
Ok(UI {
chrome,
_tmpdir,
waited: AtomicBool::new(false),
})
}
pub fn done(&self) -> bool {
self.chrome.done()
}
pub fn wait_finish(&self) {
self.chrome.wait_finish();
self.waited.store(true, Ordering::Relaxed);
}
pub fn close(&self) {
close(self.chrome.clone())
}
pub fn load(&self, content: Content) -> Result<(), JSError> {
let html: String;
let url = match content {
Content::Url(u) => u,
Content::Html(h) => {
html = format!("data:text/html,{}", h);
&html
}
};
load(self.chrome.clone(), url)
}
pub fn bind<F>(&self, name: &str, f: F) -> Result<(), JSError>
where
F: Fn(&[JSObject]) -> JSResult + Sync + Send + 'static,
{
let f = Arc::new(f);
bind(
self.chrome.clone(),
name,
Arc::new(move |context| {
let f = f.clone();
std::thread::spawn(move || {
let result = f(context.args());
context.complete(result);
});
}),
)
}
pub fn bind_async<F>(&self, name: &str, f: F) -> Result<(), JSError>
where
F: Fn(BindingContext) + Sync + Send + 'static,
{
bind(self.chrome.clone(), name, Arc::new(f))
}
#[cfg(feature = "tokio")]
pub fn bind_tokio<F, R>(&self, name: &str, f: F) -> Result<(), JSError>
where
F: Fn(Vec<JSObject>) -> R + Send + Sync + 'static,
R: std::future::Future<Output = JSResult> + Send + 'static,
{
let runtime = tokio::runtime::Handle::try_current()
.map_err(|err| JSError::from(JSObject::String(err.to_string())))?;
self.bind_async(name, move |context| {
let fut = f(context.args().to_vec());
runtime.spawn(async move {
let result = fut.await;
context.complete(result);
});
})
}
pub fn eval(&self, js: &str) -> JSResult {
eval(self.chrome.clone(), js)
}
pub fn load_js(&self, script: &str) -> Result<(), JSError> {
load_js(self.chrome.clone(), script)
}
pub fn load_css(&self, css: &str) -> Result<(), JSError> {
load_css(self.chrome.clone(), css)
}
pub fn set_bounds(&self, b: Bounds) -> Result<(), JSError> {
set_bounds(self.chrome.clone(), b)
}
pub fn bounds(&self) -> Result<Bounds, JSObject> {
bounds(self.chrome.clone())
}
}
impl Drop for UI {
fn drop(&mut self) {
if !self.waited.load(Ordering::Relaxed) && !self.done() {
self.close();
self.wait_finish();
}
#[cfg(target_family = "windows")]
close_handle(self.chrome.clone());
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Content<'a> {
Url(&'a str),
Html(&'a str),
}
pub struct UIBuilder<'a> {
content: Content<'a>,
dir: Option<&'a std::path::Path>,
width: i32,
height: i32,
custom_args: &'a [&'a str],
}
impl<'a> Default for UIBuilder<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a> UIBuilder<'a> {
pub fn new() -> Self {
UIBuilder {
content: Content::Html(""),
dir: None,
width: 800,
height: 600,
custom_args: &[],
}
}
pub fn run(&self) -> Result<UI, UILaunchError> {
let html: String;
let url = match self.content {
Content::Url(u) => u,
Content::Html(h) => {
html = format!("data:text/html,{}", h);
&html
}
};
UI::new(url, self.dir, self.width, self.height, self.custom_args)
}
pub fn content(&mut self, content: Content<'a>) -> &mut Self {
self.content = content;
self
}
pub fn user_data_dir(&mut self, dir: &'a std::path::Path) -> &mut Self {
self.dir = Some(dir);
self
}
pub fn size(&mut self, width: i32, height: i32) -> &mut Self {
self.width = width;
self.height = height;
self
}
pub fn custom_args(&mut self, custom_args: &'a [&'a str]) -> &mut Self {
self.custom_args = custom_args;
self
}
}