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>
160 lines
7.6 KiB
Rust
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");
|
|
}
|
|
}
|