waybetter/crates/waybetter-host/src/lib.rs
CRT 1edc6440ef host: the popover host (webview, session, z-order, close rules) is the host's
popover_host owns the one popover webview of an app: creation (recreated above tabs created after
it), placement through popover::place, the session (seq, source tab, app meta flattened into the
JSON the page reads), the toggle-closed window, close on layout change, navigation (the app says
whether the context moved on), shell raise, and the popover_only / current caller checks. The app
passes a PopoverConfig (page url, open event, macOS ground). Tests: layout staleness and the
session JSON shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:33:10 +07:00

160 lines
7.6 KiB
Rust

//! wayBetter native host.
//!
//! A wayBetter app is one native window with a trusted local **shell** webview (tab strip, address
//! bars, split panes, settings) and one native child webview per browser tab, positioned over the
//! pane rectangles the shell reports. One site (x.com, web.whatsapp.com, kick.com, ...) is the
//! app's *site*: its tabs get the app's page runtime and may call a small set of site-only commands.
//!
//! This crate holds the site-agnostic parts, all headless-testable:
//! - [`site`]: the site adapter ([`site::SiteSpec`], [`site::AccountRules`]).
//! - [`policy`]: webview labels, origin checks and the tab rectangle clamp (security boundary).
//! - [`profiles`]: account profiles, one WebView data directory each, with the account seen inside.
//! - [`plugins`]: owner-installed `*.plugin.js` files, custom CSS and bundled-plugin upgrades;
//! [`plugins::migrate`]: the one-time move of replaced plugin files to feature flags;
//! [`plugins::commands`]: the palette commands site tabs report and the script that runs one.
//! - [`hotfix`]: the signed, data-only hotfix channel.
//! - [`update`]: silent-update scheduling and the crash handshake.
//! - [`tabs`]: tab layout/state payloads, link routing and tab-chrome validation.
//! - [`bridge`]: the tab bridge script every tab gets (URL/title/focus reports, shortcut and link-drag forwarding).
//! - [`layout`]: native placement bookkeeping (unchanged bounds skipped, window-resize follow).
//! - [`i18n`]: the strings native UI and site tabs get in the owner's language (validated data), OS locale.
//! - [`backup`]: the settings backup envelope, secret scrubbing and the plugin/account sections.
//! - [`sync`]: encrypted settings sync through a folder the owner syncs (Argon2id + XChaCha20-Poly1305).
//! - [`theme`]: the owner's theme choice and account/pane themes (envelope checks, which theme a tab
//! shows, its page color and delivery script) over a site's built-in themes.
//! - [`workspaces`]: saved tab/pane/deck layouts, bounded and well-formed (never evaluated).
//! - [`devmode`]: developer mode's debounced watcher of plugins, custom CSS and a theme file.
//! - `devloop` (debug builds with `app` only): page scripts read from disk and every tab rebuilt when
//! one changes; an app names its files with [`page_script!`].
//! - [`settings`]: the shell's settings (home page, where links open, session restore) and the page's
//! feature flags, as validated data and the scripts that hand them to site tabs.
//! - [`modes`]: the onboarding mode the owner started from (the presets are the app's feature table).
//! - [`zoom`]: page zoom per tab (a clamped factor the shell sets, remembered in `zoom.json`).
//! - [`safe`]: safe mode for one run (a flag in memory: site tabs get the tab bridge and nothing else).
//! - [`find`]: find in page (the request the shell sends, the fallback script; the query is kept nowhere).
//! - [`alerts`]: local keyword alerts (keywords, snippets, per-item/per-keyword rate limits).
//! - [`media`]: gallery folders (path containment) and the opt-in clipboard history (private copies skipped).
//! - [`gfonts`]: self-hosted Google Fonts: CSS parsing, URL/woff2 checks, the cache format, the page script;
//! [`fonts`] (`net` feature) downloads and caches them.
//! - [`store`]: installing themes and plugins from `waybetter://install` links, dropped files and the
//! signed multi-app wayBetter Store (allow-list, caps, catalog signatures, staging, install records);
//! downloads with the `net` feature.
//!
//! - [`commands`]: the commands the host serves for every app and who may call them (for `build.rs`);
//! [`testkit`]: capability and caller-check assertions for an app's security tests.
//!
//! With the `tauri` feature, [`host`] adds the Tauri glue that does not depend on app state; with
//! `tauri` and `net`, [`updater`] runs signed native updates and the hotfix channel. With `app`,
//! [`app`] is the whole app around a site: [`app::run_app`] makes the window, the shell and the tab
//! webviews, registers the plugins and serves the generic commands (tab layout and actions, profiles,
//! language, the plugins folder and custom CSS, page tweaks, the shell's settings); the app adds its own.
pub mod alerts;
pub mod backup;
pub mod bridge;
pub mod commands;
pub mod devmode;
pub mod diag;
pub mod find;
#[cfg(feature = "net")]
pub mod fonts;
pub mod gfonts;
pub mod hotfix;
pub mod i18n;
pub mod layout;
pub mod media;
pub mod modes;
pub mod plugins;
pub mod popover;
pub mod policy;
pub mod profiles;
pub mod safe;
pub mod settings;
pub mod site;
pub mod store;
pub mod sync;
pub mod tabs;
pub mod testkit;
pub mod theme;
pub mod update;
pub mod window_fit;
pub mod workspaces;
pub mod zoom;
#[cfg(feature = "app")]
pub mod app;
#[cfg(all(feature = "app", feature = "net"))]
mod store_commands;
#[cfg(feature = "app")]
pub mod media_commands;
#[cfg(feature = "app")]
mod sync_commands;
#[cfg(feature = "app")]
mod dev_commands;
#[cfg(feature = "app")]
pub mod theme_commands;
#[cfg(feature = "app")]
pub mod tab_commands;
#[cfg(feature = "app")]
pub mod alerts_commands;
#[cfg(feature = "app")]
mod backup_commands;
#[cfg(all(feature = "app", feature = "net"))]
mod fonts_commands;
#[cfg(all(feature = "app", feature = "net"))]
mod update_commands;
#[cfg(all(feature = "app", debug_assertions))]
pub mod devloop;
#[cfg(feature = "tauri")]
pub mod host;
#[cfg(feature = "tauri")]
pub mod popover_host;
#[cfg(all(feature = "tauri", feature = "net"))]
pub mod updater;
/// An app's page script for `SiteHooks::init_scripts`: `$embedded` (an `include_str!`) in every
/// build, except that a debug build reads `$file` (relative to the app crate's manifest directory)
/// from disk each time and falls back to the embedded copy when it is missing. The files named
/// this way are watched: a change rebuilds every tab (`devloop`). In a release build the disk arm
/// is not compiled at all: the expansion is `$embedded.to_owned()`, `$file` appears nowhere.
#[macro_export]
macro_rules! page_script {
($embedded:expr, $file:literal) => {{
#[cfg(debug_assertions)]
{
$crate::devloop::from_disk(concat!(env!("CARGO_MANIFEST_DIR"), "/", $file), $embedded)
}
#[cfg(not(debug_assertions))]
{
($embedded).to_owned()
}
}};
}
#[cfg(test)]
mod release_guard {
/// Release builds must not contain the dev loop: every mention of it in this crate is behind
/// `cfg(debug_assertions)` (checked on the source, so it also holds when only debug is built).
#[test]
fn every_devloop_mention_is_debug_only() {
for (file, source) in [("lib.rs", include_str!("lib.rs")), ("app.rs", include_str!("app.rs"))] {
// This test module itself talks about the dev loop; only the code before it counts.
let lines: Vec<&str> = source.split("mod release_guard").next().unwrap_or(source).lines().collect();
for (index, line) in lines.iter().enumerate() {
let code = line.trim_start();
if code.starts_with("//") || !(code.contains("devloop::") || code.starts_with("pub mod devloop")) || code.contains("$crate::devloop") {
continue;
}
let gated = (index.saturating_sub(2)..index).any(|before| lines[before].contains("debug_assertions"));
assert!(gated, "{file}:{}: devloop used outside cfg(debug_assertions)", index + 1);
}
}
}
/// `cargo test --release`: the macro is just the embedded text.
#[cfg(not(debug_assertions))]
#[test]
fn release_page_script_is_the_embedded_copy() {
assert_eq!(crate::page_script!("embedded", "no/such/file.js"), "embedded");
}
}