use std::path::PathBuf;
pub use app::Message;
pub(crate) const HOMEPAGE_HTML: &str =
include_str!(concat!(env!("OUT_DIR"), "/browser-index.html"));
pub(crate) fn homepage_html_arc() -> std::sync::Arc<str> {
static ONCE: std::sync::OnceLock<std::sync::Arc<str>> = std::sync::OnceLock::new();
ONCE.get_or_init(|| std::sync::Arc::from(HOMEPAGE_HTML)).clone()
}
mod app;
pub mod api;
mod downloads;
mod downloader;
mod history;
mod loader;
mod quicklinks;
mod settings;
mod spatial;
mod stash;
mod tab;
pub mod vault;
mod webview;
pub use quicklinks::{QuickLink, load as load_quicklinks};
pub use api::{BrowserHandle, TabTarget, ThemeMode, ApiReceiver};
#[derive(Debug, Clone)]
pub enum Target {
Default,
Url(String),
File(PathBuf),
Html(String),
UrlFromHome(String),
}
#[derive(Debug)]
pub enum BrowserError {
Iced(iced::Error),
}
impl std::fmt::Display for BrowserError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BrowserError::Iced(e) => write!(f, "iced: {e}"),
}
}
}
impl std::error::Error for BrowserError {}
impl From<iced::Error> for BrowserError {
fn from(e: iced::Error) -> Self {
BrowserError::Iced(e)
}
}
#[derive(Debug)]
pub(crate) enum BrowserEvent {
PageStarted(String),
PageFinished(String),
TitleChanged(String),
Ipc(String),
DownloadStarted(String, std::path::PathBuf, std::path::PathBuf),
DownloadCompleted(String, bool),
}
pub fn launch(target: Target) -> Result<(), BrowserError> {
use iced::{Size, window};
let icon: Option<window::Icon> = {
#[allow(unused_mut)]
let mut ico = None;
#[cfg(all(not(target_os = "macos"), has_app_icon))]
{
static BYTES: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/app-icon.png"));
ico = window::icon::from_file_data(BYTES, None).ok();
}
ico
};
iced::application(
move || app::BrowserState::new(target.clone()),
app::update,
app::view,
)
.subscription(app::subscription)
.theme(app::theme)
.window(window::Settings {
size: Size::new(1280.0, 820.0),
position: window::Position::Centered,
icon,
..Default::default()
})
.title("duct-tape")
.run()
.map_err(BrowserError::Iced)
}
pub fn launch_default() -> Result<(), BrowserError> {
launch(Target::Html(HOMEPAGE_HTML.to_string()))
}
pub fn launch_with_api(target: Target, api: api::ApiReceiver) -> Result<(), BrowserError> {
api::install_receiver(api.0);
launch(target)
}
pub fn launch_with_controller<F, Fut>(
target: Target,
controller: F,
) -> Result<(), BrowserError>
where
F: FnOnce(api::BrowserHandle) -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let (handle, receiver) = api::BrowserHandle::new();
api::install_receiver(receiver.0);
tokio::runtime::Handle::current().spawn(controller(handle));
tokio::task::block_in_place(|| launch(target))
}
pub(crate) mod urlencoding {
pub fn encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9'
| b'-' | b'_' | b'.' | b'~' => out.push(b as char),
b' ' => out.push('+'),
_ => {
out.push('%');
out.push(char::from_digit((b >> 4) as u32, 16).unwrap_or('0'));
out.push(char::from_digit((b & 0x0f) as u32, 16).unwrap_or('0'));
}
}
}
out
}
}
#[cfg(all(target_os = "macos", has_app_icon))]
pub(super) fn set_dock_icon() {
use std::ffi::c_void;
use objc2::{AnyThread, MainThreadMarker};
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::NSData;
static ICON: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/app-icon.png"));
unsafe {
let Some(mtm) = MainThreadMarker::new() else { return };
let data = NSData::initWithBytes_length(
NSData::alloc(),
ICON.as_ptr() as *const c_void,
ICON.len(),
);
if let Some(image) = NSImage::initWithData(NSImage::alloc(), &data) {
let app = NSApplication::sharedApplication(mtm);
app.setApplicationIconImage(Some(&image));
}
}
}