564 lines
39 KiB
Markdown
564 lines
39 KiB
Markdown
# Architecture reference
|
|
|
|
Every module of the host, the runtime and the shell, and what an app supplies. The overview is in
|
|
the [README](../README.md); this is the long form.
|
|
|
|
A wayBetter app is one native window (Tauri 2, multiwebview) with:
|
|
|
|
- a trusted local **shell** webview: tab strip, split panes (dockview), address bar, titlebar and
|
|
window controls, command palette, account switcher, the app's own settings;
|
|
- one native child webview per browser **tab**, positioned over the pane rectangles the shell
|
|
reports (`tabs_sync`), each in an account **profile** with its own WebView data directory;
|
|
- a **page runtime** injected into tabs that are on the app's site: shared DOM/request hubs,
|
|
plugins, themes, split-view rail hiding, link routing and account reporting.
|
|
|
|
This repository holds the parts that do not depend on which site the app extends. An app is mostly
|
|
a **site adapter** plus its own features.
|
|
|
|
## One system for every app
|
|
|
|
Every wayBetter app is the same system: the Tauri window above (shell + page runtime) plus one
|
|
small adapter. That holds for websites like x.com and just as much for services that ship their
|
|
own Electron desktop client, such as WhatsApp or Discord: wayBetter runs their web app in its own
|
|
light Tauri window instead of the heavy client, and gives it the same tabs, split panes, real
|
|
accounts side by side, themes, plugins and signed updates. There is no second kind of host.
|
|
|
|
License: GPL-3.0-or-later.
|
|
|
|
## Projects using wayBetter
|
|
|
|
| Project | Target | What it is |
|
|
| --- | --- | --- |
|
|
| [wayBetter for X](https://waybetter.fyi/x/) | x.com (website) | Desktop app around the live X: tabs, split panes, real accounts side by side, themes, plugins, drafts and fixes for X's own bugs. The first wayBetter app. |
|
|
|
|
Built something on wayBetter? Add it to this list with a pull request: name and link, what it extends, one line on what it does.
|
|
|
|
## Build your own
|
|
|
|
```sh
|
|
npm create waybetter plugin # a *.plugin.js (or --ts: a TypeScript project) on the real plugin API
|
|
npm create waybetter theme # a theme JSON with "$schema", a derived palette and font roles
|
|
npm create waybetter app -- --site kick.com # a whole app: Tauri 2 + waybetter-host + runtime + shell
|
|
```
|
|
|
|
`create-waybetter` (in `packages/create-waybetter`) asks for what it needs in a terminal, and
|
|
takes every answer as a flag (`--help`). The app skeleton is this README in code: the native
|
|
`SiteSpec`, the page `SiteAdapter`, the app on `waybetter_host::app` (its config, site hooks and
|
|
its own commands), the three capability files and a `tests/security.rs` on `waybetter_host::testkit`,
|
|
the shell (the framework's shell frame through `createShell`, with the site's pages and a plugins
|
|
panel), the plugin store, one site-only command, the
|
|
runtime bundled with `waybetter/build` and a `waybetter.release.json` for `waybetter release`, with presets for
|
|
youtube.com, kick.com and twitch.tv and every site selector marked as a placeholder. Its tests
|
|
generate each kind into a temp folder and check it against the real contracts: plugin headers with
|
|
`plugins::parse_meta`, plugin code against `createCorePluginApi`'s members, themes against
|
|
`theme.schema.json`, the app's TypeScript with `tsc` against the built package and its Rust with
|
|
`cargo check`/`cargo test` against this crate.
|
|
|
|
## Layout
|
|
|
|
```
|
|
crates/waybetter-host/ Rust: security policy, profiles, plugin store, hotfix, updates, tab payloads,
|
|
and the generic native features every app gets (below)
|
|
tab-bridge.js the tab bridge every tab gets (served by src/bridge.rs)
|
|
src/site.rs SiteSpec + AccountRules (the native half of the adapter)
|
|
src/policy.rs webview labels, origin checks, tab rect clamp
|
|
src/profiles.rs profiles, per-profile data dirs, native-owned accounts
|
|
src/plugins.rs *.plugin.js store, custom CSS, bundled-plugin upgrades
|
|
src/plugins/commands.rs palette commands site tabs report, and the script that runs one
|
|
src/settings.rs the shell's settings (home page, where links open, session restore) and the
|
|
page's feature flags, as validated data and the scripts that deliver them
|
|
src/hotfix.rs signed data-only hotfix channel (minisign)
|
|
src/update.rs silent-update scheduling and crash handshake
|
|
src/tabs.rs TabLayout/TabState/OpenRequest, link modes, shortcut and drag validation,
|
|
deck ColumnConfig bounds and unread caps
|
|
src/bridge.rs the tab bridge script (reports; shortcut/link-drag forwarding opt-in)
|
|
src/backup.rs settings backup envelope, secret scrubbing, plugin/account sections
|
|
src/sync.rs encrypted folder sync (Argon2id + XChaCha20-Poly1305 snapshots)
|
|
src/workspaces.rs saved tab/pane/deck layouts, bounded and well-formed
|
|
src/devmode.rs developer mode: debounced file watcher, reload order, error positions
|
|
src/alerts.rs keyword alerts: keywords, snippets, rate limits; native notifications
|
|
src/media.rs gallery folders (path containment), clipboard history, PNG encoding
|
|
src/media/clipboard.rs (feature "tauri") opt-in clipboard polling, private copies skipped
|
|
src/gfonts.rs self-hosted Google Fonts: CSS parsing, URL/woff2 checks, cache format
|
|
src/fonts.rs (feature "net") font downloads and the font cache
|
|
src/updater.rs (features "tauri" + "net") signed native updates and the hotfix channel
|
|
src/store/ install links, download allow-list, signed multi-app store catalog, staging
|
|
and installs; downloads with feature "net" (docs/STORE.md)
|
|
src/host.rs (feature "tauri") webview helpers, shell events, system menu
|
|
src/app.rs (feature "app") the whole app around a site: run_app / App, AppConfig,
|
|
SiteHooks, the window, shell and tab webviews, the generic commands
|
|
(tabs, profiles, language, plugins and custom CSS, feature flags, settings)
|
|
src/commands.rs the commands the host serves and who may call them; manifest() for build.rs
|
|
src/testkit.rs capability/caller-check assertions for an app's tests/security.rs
|
|
packages/runtime/src/ page runtime core (TypeScript, no dependencies)
|
|
site.ts SiteAdapter (the page half of the adapter) + configureRuntime
|
|
domHub.ts netHub.ts one MutationObserver/frame, one fetch/XHR hook per page
|
|
pluginApi.ts core of window.<Runtime>.api for plugins (incl. ui and theme)
|
|
ui.ts plugin UI kit: toasts, alert/confirm/prompt, dialogs (shadow DOM, themed)
|
|
themeApi.ts the active theme as values + --wb-* CSS variables (api.theme)
|
|
themeTokens.ts OKLab color model: maps the site's palette onto a theme's axis
|
|
paneNav.ts split view: park the site's navigation rail in redundant panes
|
|
column.ts deck columns: hide site chrome, fill the width, filters, unread count
|
|
linkRouting.ts divert in-page clicks to a pane/tab/browser per setting
|
|
accountReport.ts report the signed-in account of this tab's profile
|
|
siteRuntime.ts the runtime's composition: the IPC bridge, flag-controlled parts with a deck
|
|
column's overrides, the runtime object under its window names, native queues
|
|
pageStrings.ts the page's language: English built in, native deliveries checked as data
|
|
stringPacks.ts string packs (docs/STRING-PACKS.md): pack and inventory formats, validator,
|
|
ordered engine, coverage, drift report
|
|
stringPackHook.ts the per-site catalog hook adapter, its self-check, a webpack chunk array hook
|
|
textNet.ts DOM safety net for labels no catalog hook reaches
|
|
panelButton.ts the way into the panel from the page (navigation entry or floating button)
|
|
commandReport.ts the palette command report and developer-mode start errors
|
|
packages/shell/src/ shell core (TypeScript) and the shell frame (React)
|
|
addressModel.ts address bar chips (site pages named by the adapter)
|
|
paneModel.ts beside-pane reuse, redundant rails, navRedundant for tabs_sync
|
|
deckModel.ts decks of site columns: bounded storage model, paging layout, focus
|
|
linkDrop.ts dropping links into the dock
|
|
profileModel.ts profiles and account labels
|
|
palette.ts command palette scoring/ranking
|
|
updateModel.ts titlebar update chip
|
|
windowControls.ts caption buttons, resize grips, system menu (Tauri API injected)
|
|
chrome.ts titlebar/toolbar/resize-border sizes (match policy.rs)
|
|
storeModel.ts storeMessages.ts Store wire types, item states, permission words; wb.store/install/permission.*
|
|
installController.ts the install screen's state machine (links, drops, store items, confirm)
|
|
InstallDialog.tsx StoreView.tsx store.css the install screen and the Store view (React peer, --wb-* tokens)
|
|
frame/ the shell frame: createShell(site) (see "The shell frame" below)
|
|
panel/ the panel: createPanel(site), the app's own tab (see "The panel" below)
|
|
installerMessages.ts the Windows installer's words (wb.installer.*)
|
|
packages/waybetter/ the published npm package (runtime + shell as ESM + .d.ts, theme.schema.json,
|
|
store-catalog.schema.json, the `waybetter store build|sign|verify`,
|
|
`waybetter release windows|macos`, `waybetter i18n check|extract`,
|
|
`waybetter privacy-scan` and `waybetter build-env` commands, `waybetter/build`, locales/ (51 catalogs and translator notes),
|
|
windows/ (the installer template and its language files))
|
|
dev-tools/ development harnesses for a running or built app (perf probes, close and
|
|
stress checks); not published (dev-tools/README.md)
|
|
docs/STORE.md the Store: shared catalog format, signing and keys, tooling, trust model, how to add it
|
|
packages/create-waybetter/ `npm create waybetter`: plugin, theme and app scaffolds (no dependencies)
|
|
```
|
|
|
|
Both TypeScript packages are plain source (`.ts`, ESM, `type: module`) with no runtime
|
|
dependencies, so an app can bundle them with esbuild/Vite and run their tests with Node's type
|
|
stripping. Anything that needs the Tauri API (window controls, the shell frame) takes it as a
|
|
parameter. The React parts (the Store screens, the shell frame) have React, and the frame dockview,
|
|
as optional peers: the app's copies are used.
|
|
|
|
## The shell frame
|
|
|
|
`packages/shell/src/frame` (`waybetter/shell/frame`) is the whole shell of a wayBetter app. An app
|
|
describes its site and gets tabs in the titlebar row, split panes and link drags into the dock, the
|
|
smart address bar, the new-tab page, the command palette, the account switcher, decks of site
|
|
columns, workspaces, the developer-mode and sync chips, the gallery/clipboard panel, pane themes,
|
|
its own panel tab, keyboard shortcuts, overlays that raise the shell above the tab webviews, the
|
|
one-layout-in-flight `tabs_sync` and session restore:
|
|
|
|
```tsx
|
|
import { invoke } from '@tauri-apps/api/core'; // or window.__TAURI__ (withGlobalTauri)
|
|
import { listen } from '@tauri-apps/api/event';
|
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
import { createShell, type ShellSite } from 'waybetter/shell/frame';
|
|
|
|
const site: ShellSite = { name: 'Kick', product: 'wayBetter for Kick', homeUrl, loginUrl, logoutUrl,
|
|
isSiteUrl, accounts, searchUrl, address, panel: { id, title, component }, newTab, palette,
|
|
deck, media, themes, overlays, features, start };
|
|
const Shell = createShell(site, { native: { invoke, listen, currentWindow: getCurrentWindow }, i18n });
|
|
createRoot(root).render(<Shell/>);
|
|
```
|
|
|
|
What the site supplies (`frame/site.ts`, every field documented there):
|
|
|
|
- names and pages: `name` (in messages as `{site}`), `product`, `logo`, `homeUrl`, `loginUrl`,
|
|
`logoutUrl`, `isSiteUrl`, the account rules, `searchUrl` for typed text;
|
|
- the address bar: `address(url)` (chips, usually `parseAddress` with an `AddressSite`) and an
|
|
optional `addressCompleter` (inline completions, e.g. search operators);
|
|
- slots: `panel` (the app's own tab and its sections), `newTab` (search label, quick links),
|
|
`palette` (placeholder, the literal search, completions, the panel's sections, the site's pages),
|
|
`deck` (column kinds with labels and icons, the page each loads, titles, value parsing and
|
|
completions, column flags and presets, an optional compose dock), `media` (which pages have a
|
|
composer), `themes` (a `createThemeStore(adapter)`: the app's theme documents, chrome colors via
|
|
`chromeVars`, pane themes), `overlays` (the app's own dialogs), `start()` (read site settings,
|
|
call `siteChanged()` when site text changes);
|
|
- `features`: the native features the app wires up (workspaces, sync, developer mode, plugin
|
|
palette commands); an app without them shows nothing of them.
|
|
|
|
The frame's words are `wb.*` messages in the framework catalogs (51 languages; English source in
|
|
`frame/frameMessages.ts`): the app passes its i18n instance with the framework catalogs merged in.
|
|
Styles come with the components (dockview's, then `shell.css`, `deck.css`, `workspaces.css`,
|
|
`accountSwitcher.css`, `paneTheme.css`); colors are `--wb-c-*` tokens with Night Ink fallbacks,
|
|
set by the theme store (the chrome and each pane group) and cached for the first frame:
|
|
`bootScript()` (`frame/boot.ts`) is the `boot.js` an app serves before its modules (a Vite plugin or
|
|
a build step writes it). `installPreviewMocks(handlers)` answers native commands from memory, so
|
|
the real shell renders in a plain browser for development previews and screenshots.
|
|
|
|
Native commands the frame calls: the host's (`tabs_sync`, `tab_navigate`, `tab_action`,
|
|
`shell_focus`, `shell_raise`, `open_external_url`, `window_system_menu`, `profiles_*`), and for the
|
|
features an app turns on `settings_get`, `column_configure`, `workspaces_*`, `sync_*`, `dev_*`,
|
|
`plugins_list`, `tab_run_command`, `media_get`/`gallery_*`/`clip_*`/`tab_attach_image`/
|
|
`tab_insert_text`, `theme_scopes_*` and `gfonts_*`, `app_ready`/`update_status`/`apply_update`. It
|
|
listens to `tab-state`, `tab-open`, `tab-shortcut`, `link-drag`, `open-panel`, `profiles-changed`,
|
|
`tab-commands`, `dev-event`, `update-status`, `tab-still` and `clipboard-changed`.
|
|
|
|
## The panel
|
|
|
|
`packages/shell/src/panel` (`waybetter/shell/panel`) is an app's own tab in the shell: a home page,
|
|
Appearance (the theme gallery, customization, account and pane themes, downloaded fonts, theme
|
|
import/export) and the Theme Builder (undo/redo, a palette from three colors, a contrast report, a
|
|
font per role with the Google Fonts browser), plugins and custom CSS, the Store, Make (example
|
|
plugins, the plugin UI kit, the theme API, bundled docs), accounts, Power User (deck settings),
|
|
settings (language, links and session, keyword alerts, the media panel), backup and sync, developer
|
|
mode, updates, and a search across every setting. It lives in the shell package, next to the frame
|
|
it renders in: it uses the frame's stores (profiles, decks, themes, sync, developer mode), its
|
|
native bridge and its language, so `createShell` comes first.
|
|
|
|
```tsx
|
|
import { createPanel, type PanelSite } from 'waybetter/shell/panel';
|
|
|
|
const panel: PanelSite = { brand: { name: 'wayBetter', logo }, home, themes, sections, order, make,
|
|
store, languages, settings, power, backup: true, updates: true };
|
|
const Panel = createPanel(panel);
|
|
const Shell = createShell({ ...site, panel: { id: 'app', title: 'wayBetter', component: Panel } }, options);
|
|
```
|
|
|
|
What the app supplies (`panel/site.ts`, every field documented there):
|
|
|
|
- `brand` and `home`: the header and the home page's words (kicker, title, lede, chips, actions,
|
|
highlight cards, the About block), `version()` for its chip;
|
|
- `sections` and `order`: its own sections (a label, an icon path, a component that gets `notify`
|
|
and `openSection`; `searchable` ones also show in the settings search), placed among the built-in
|
|
ones. X adds Tweaks and Drafts;
|
|
- `themes`: its theme store and catalog (`createThemeStore`, `createThemeCatalog`), accent swatches,
|
|
the **preview** component (a miniature of the site drawn from a theme document, used by the gallery
|
|
and the builder), the site font's name, the palette a draft starts from, and for Make's theme view
|
|
`resolved(choice, mode)` with the site's own display modes;
|
|
- `make`: example plugins, an example theme file and bundled Markdown docs;
|
|
- `store`: the install controller (the Store section, and removing a plugin from the list);
|
|
- `languages`: the language list and state (`createLanguage`, `frame/language.ts`);
|
|
- `settings`, `power`, `cssExamples`: keyword alerts, extra settings rows, which column flags and
|
|
which memory measure Power User lists, placeholder CSS with the site's selectors.
|
|
|
|
A built-in section shows only when the app serves what it needs: Appearance, the Theme Builder and
|
|
the Store with `themes` (and `store`), Make with `make`, Power User with `site.deck`, backup with
|
|
`backup` (its sync half with the frame's `sync` feature), Developer with the frame's `developer`
|
|
feature, Updates with `updates`. An app with nothing but plugins gets Home, Plugins, Accounts and
|
|
Settings (this is what `npm create waybetter app` generates).
|
|
|
|
Building blocks for the app's own sections: the panel's controls (`Section`, `Row`, `Toggle`,
|
|
`Segmented`, `Select`, `Slider`, `ColorField`, `Button`, `Badge`, ...; `panel/controls`), the
|
|
feature toggles (`useFeatures` + `FeatureGroups` over `features_get/set`: the app names its flags
|
|
and groups, its catalog has `features.<flag>.label` and `.description`), and `compareLines`.
|
|
|
|
The panel's words are `wb.*` messages too (English source in `panel/panelMessages.ts`, translator
|
|
notes in `locales/en.meta.json`). Three families of keys stay the app's, by convention, because
|
|
they name its own things: `plugin.<file>.name|description` (its bundled plugins),
|
|
`theme.<id>.description` (its built-in themes) and `features.*` (its tweaks).
|
|
|
|
Native commands the panel calls: the host's `plugins_list`, `plugin_set_enabled`,
|
|
`plugins_open_folder`, `custom_css_get/set`, `features_get/set`, `settings_get/set`,
|
|
`profiles_*` and `open_external_url`, and for what the app turns on `alerts_get/set`,
|
|
`media_get/set`/`gallery_add_folder`/`clip_remove`, `backup_*`, `sync_*`, `dev_*`, the theme
|
|
store's commands and `gfonts_*`, `install_records`/`install_forget`/`plugin_remove`/`store_catalog`,
|
|
`app_ready`/`update_status`/`apply_update`.
|
|
|
|
The Google Fonts list the font browser shows is an offline snapshot
|
|
(`panel/fonts/googleFontsCatalog.json`; `packages/shell/scripts/google-fonts-catalog.mjs`
|
|
regenerates it). The app never fetches the list, only the font files the owner picks (natively).
|
|
|
|
## The site adapter
|
|
|
|
A site adapter is three small, explicit things.
|
|
|
|
### 1. Native: `SiteSpec` (Rust)
|
|
|
|
```rust
|
|
use waybetter_host::site::{AccountRules, SiteSpec};
|
|
|
|
fn kick_handle(value: &str) -> bool {
|
|
(3..=25).contains(&value.len()) && value.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
|
|
}
|
|
|
|
pub static SITE: SiteSpec = SiteSpec {
|
|
product: "wayBetter for Kick",
|
|
hosts: &["kick.com"], // exact https origin(s) of site tabs
|
|
runtime_global: "WayBetterKick", // window.<global> of the page runtime
|
|
accounts: AccountRules {
|
|
valid_handle: kick_handle,
|
|
avatar_prefixes: &["https://files.kick.com/"],
|
|
max_name_chars: 50,
|
|
max_avatar_len: 512,
|
|
},
|
|
};
|
|
```
|
|
|
|
`SiteSpec::is_origin` is the only definition of "a site tab". Site-only commands check
|
|
`policy::require_site_tab(&SITE, label, url)` (or `host::site_tab_only`), which also requires a
|
|
`tab-*` label.
|
|
|
|
### 2. Page: `SiteAdapter` (TypeScript)
|
|
|
|
```ts
|
|
import { configureRuntime, type SiteAdapter } from 'waybetter/packages/runtime/src/site.ts';
|
|
|
|
export const KICK: SiteAdapter = {
|
|
product: 'wayBetter for Kick',
|
|
hosts: ['kick.com'],
|
|
runtimeGlobal: 'WayBetterKick',
|
|
heavyPath: null, // pages where offOnHeavy DOM features pause
|
|
apiMarks: ['/api/'], // request URLs the net hub stages may see
|
|
xhrMarks: [],
|
|
plugins: { name: 'WayBetterKick', product: 'wayBetter for Kick', storagePrefix: 'wbk-plugin:',
|
|
styleDataKey: 'wbkPlugin', toastId: 'wbk-toast', toastBackground: '#53fc18',
|
|
itemSelector: '[data-chat-entry]', chordKeys: [] },
|
|
rail: { prefix: 'wbk', rail: '#sidebar-wrapper', anchor: 'a[href="/"]', main: 'main',
|
|
modalOpen: '[role="dialog"]', menuOpen: '[role="menu"]',
|
|
paint: { width: 64, background: '#0b0e0f', line: 'rgba(255,255,255,.12)', accent: '#53fc18', rtl: false } },
|
|
account: { read: readKickAccount, signedOut: root => !!root.querySelector('[data-test="login"]') },
|
|
links: { configKey: 'site', clickedUrl: kickClickedUrl },
|
|
themeSources: { dark: KICK_DARK_AXIS },
|
|
};
|
|
configureRuntime(KICK);
|
|
```
|
|
|
|
Each runtime module takes only its piece (`installPaneNav(window, KICK.rail!)`,
|
|
`installLinkRouting(window, KICK.links!)`, `installAccountReport(window, KICK.account!, invoke)`,
|
|
`createCorePluginApi(window, KICK.plugins)`), so a site without a rail simply does not install it.
|
|
|
|
What every runtime entry does the same way is composed by `runtime/siteRuntime`:
|
|
`pageInvoke(window, product)` (the IPC bridge, read at call time), `createFeatureFlags({ defaults,
|
|
installers, apply })` (parts that install on enable and dispose on disable; a deck column's flag
|
|
overrides sit on top of the owner's) and `installSiteRuntime({ win, site, version, plugins, strings,
|
|
features, theme, fonts, pane, links, column, extra, queues })`, which builds the runtime object
|
|
(`features.apply`, `i18n.set`, `links.configure`, `pane.set`, `fonts.provide`, `column.set`,
|
|
`open()`, `api`, `withPlugin`, `devError`), publishes it as `window.wayBetter` and the app's own
|
|
names, takes over what native code queued before it existed and starts the palette command report.
|
|
`createPageStrings` (`runtime/pageStrings`) is the page's language and `installPanelButton`
|
|
(`runtime/panelButton`) keeps the way into the panel on the page: the adapter's `mountNav` puts
|
|
an entry into the site's own navigation, and where the page has none a floating button stands in.
|
|
|
|
### 3. Capabilities (Tauri JSON, keyed by webview label)
|
|
|
|
The security model is the same for every app and must stay keyed by webview label:
|
|
|
|
| webview | may call |
|
|
| --- | --- |
|
|
| `shell` (local assets only) | the app's own commands and the host's `commands::SHELL` (`tabs_sync`, `tab_navigate`, `tab_action`, `shell_raise`, `profiles_*`, `i18n_*`, `plugins_*`, `custom_css_*`, `features_*`, `settings_*`, ...) |
|
|
| `tab-*` (any site) | only tab chrome about itself: `tab_report`, `tab_shortcut`, `tab_link_drag` |
|
|
| `tab-*` on the exact site origin | the app's short, reviewed list of site-only commands, and the host's `commands::SITE` (`open_panel`) |
|
|
| other local webviews (`popover-*`, ...) | their own narrow capability |
|
|
|
|
```json
|
|
{ "identifier": "tab-chrome", "webviews": ["tab-*"], "local": false,
|
|
"remote": { "urls": ["https://*/*", "http://*/*"] },
|
|
"permissions": ["allow-tab-report", "allow-tab-shortcut", "allow-tab-link-drag"] }
|
|
```
|
|
|
|
```json
|
|
{ "identifier": "site-tab", "webviews": ["tab-*"], "local": false,
|
|
"remote": { "urls": ["https://kick.com/*"] },
|
|
"permissions": ["allow-open-panel"] }
|
|
```
|
|
|
|
Native code re-checks every one of these (the capability file alone is not the boundary): the
|
|
caller's label, the webview's own URL (never the page's claim), payload bounds. A tab can speak only
|
|
about itself; the account it reports is recorded only for its own profile, only while it is on the
|
|
site, and a shell save can never change it (`ProfileSettings::keep_accounts_from`).
|
|
|
|
`waybetter_host::testkit` checks this in an app's own tests: `Capabilities::from_dir` reads every
|
|
capability file, `assert_host_commands` keeps the host's commands shell-only / tab-only,
|
|
`assert_shell_only` / `assert_tab_only` do the same for the app's, `assert_site_only(command, &SITE)`
|
|
checks that a site command is granted only on the site's own URLs, `assert_no_plugin_apis` refuses
|
|
Tauri plugin JS APIs, and `assert_guarded` / `assert_host_guards` check that each command's first
|
|
statement checks its caller.
|
|
|
|
## What the host gives an app
|
|
|
|
- `app` (feature `app`): the whole app. `App::new(AppConfig, hooks).setup(..).run(context,
|
|
handler)` (or `run_app`) registers single-instance, deep-link (opt-in), window-state, opener,
|
|
updater (opt-in), process, notification and dialog; makes the app-data folder (0700 on Unix),
|
|
loads the profiles and the language, runs the app's setup, then builds the window (frameless
|
|
with the shell's titlebar, or the OS frame), restores its geometry (lost geometry reset),
|
|
routes helper-window closes (Windows), follows window resizes natively and adds the shell
|
|
webview (transparent: it can rise above the tabs; macOS needs the `macos-private-api` feature).
|
|
It serves `commands::SHELL` (`tabs_sync`, `tab_navigate`, `tab_action`, `open_external_url`,
|
|
`shell_focus`, `shell_ready`, `tab_still_ready`, `shell_raise`, `window_system_menu`, `profiles_get`/`profiles_set`,
|
|
`i18n_get`/`i18n_set`, `plugins_list`, `plugin_set_enabled`, `plugins_open_folder`,
|
|
`custom_css_get`/`custom_css_set`, `features_get`/`features_set`, `settings_get`/`settings_set`),
|
|
`commands::TAB` (`tab_shortcut`, `tab_link_drag`) and `commands::SITE` (`open_panel`), each
|
|
checking its caller first, and dispatches every other command to the app's handler. It keeps
|
|
the page state those commands read and save (`PageState`: the plugin store in app data
|
|
`plugins`, made from the `PluginHost` in `AppConfig::new(site, title, plugins)`; the feature flags
|
|
in `features.json`; `ShellSettings` in `settings.json`): the app reads it with `page(app)`, gets a
|
|
loaded document's scripts from `page_scripts(app)` (flags, links, custom CSS, plugins) and writes
|
|
it with `set_features`, `set_settings` and `set_custom_css` (a backup import). Tab webviews get their
|
|
profile's data folder, the app's document-start scripts, navigation/pop-up/load/title/download
|
|
handling, stills and memory targets; `SiteHooks` is where the app says what only it knows (page
|
|
scripts, legacy domains, link routing, per-tab state, what a finished site page gets). Helpers:
|
|
`profiles`, `tab_profile`, `account_seen`, `tr`/`trf`, `i18n_page_script`, `notify`,
|
|
`open_external`.
|
|
To show the first window only after restored panes are placed, opt in on both sides:
|
|
`AppConfig::defer_window_show = true` and `ShellSite.deferWindowShow = true`, and grant the
|
|
shell `allow-shell-ready`. The default still shows the window immediately. An opted-in app
|
|
shows it after initial `tabs_sync`, with a three-second fallback if shell startup fails.
|
|
- `commands`: the host's command lists (`SHELL`, `TAB`, `SITE`) and `manifest(&[..])` for the app's `build.rs`.
|
|
- `settings`: `ShellSettings` (home page, where links to other sites and to the site's own pages
|
|
open, session restore; missing fields take the defaults), `links_script`, and the feature flags'
|
|
validation (a small map of booleans) and `features_script`.
|
|
- `testkit`: see the capabilities section.
|
|
- `policy`: `SHELL_LABEL`, `tab_id`, `require_local`, `require_site_tab`, `clamp_tab_rect` (tabs never
|
|
cover the titlebar or, while restored, the resize band).
|
|
- `profiles`: `ProfileSettings` (validated, default profile required, profiles with open tabs cannot be
|
|
removed), `data_dir` (ids cannot escape `profiles/`), `orphan_dirs`, `store_identifier` (macOS).
|
|
- `plugins`: `PluginStore` with a `PluginHost` (runtime global, origin, product, bundled plugins).
|
|
Bundled plugins are written disabled and upgraded only while untouched.
|
|
- `hotfix`: minisign-verified, schema-checked, monotonic, expiring data; delivered frozen as data.
|
|
- `update`: check schedule with jitter, crash handshake (automatic apply off after two failed boots).
|
|
- `tabs`: shell payloads and validation; `deliver_script` hands JSON to a runtime member or parks it.
|
|
- `bridge`: the tab bridge (`SOURCE`, or `script(BridgeOptions)` to leave shortcut and link-drag
|
|
forwarding off when the shell does not handle them).
|
|
- `backup` + `sync`: the `.waybetter` settings backup (the app registers its sections and files;
|
|
secret-looking keys are scrubbed both ways) and encrypted snapshots in a folder the owner syncs.
|
|
- `workspaces`: validation of saved layouts (slug ids, known tab kinds, http(s) URLs, size caps).
|
|
- `devmode`: the live-reload watcher (plugins, `custom.css`, one theme file; the app turns the theme
|
|
document into its own state through a callback).
|
|
- `alerts`: keyword settings, snippets, `Throttle`, `AlertGate` over any `KeywordMatch`;
|
|
`alerts::notify` (feature `tauri`) shows a native notification.
|
|
- `media`: gallery folders and the opt-in clipboard history; `media::clipboard::watch` (feature
|
|
`tauri`) polls the OS clipboard and never reads a copy a password manager marked private.
|
|
- `gfonts` + `fonts` (feature `net`): Google Fonts downloaded natively from the two Google hosts
|
|
only, cached with a sha256 manifest and handed to site pages as bytes (`page_script(&SITE, ..)`).
|
|
- `updater` (features `tauri` + `net`): the update loop, the signed hotfix channel and
|
|
`status`/`apply`/`ready` for the app's commands, configured by an `UpdaterConfig`.
|
|
- `plugins::commands`: palette commands a site tab reports, and `run_script(&SITE, id)`.
|
|
- `store`: the wayBetter Store for any app from a `StoreConfig` (app id, link scheme, store URL,
|
|
public keys with rotation, download hosts): `waybetter://install` link parsing, the allow-list,
|
|
size caps, catalog verification (Ed25519, strict schema, each app sees only its items), `Store`
|
|
(pending links, staged installs, commit of exactly the staged bytes, records, offline catalog),
|
|
and with feature `net` no-redirect/no-cookie downloads. The shell half is `waybetter/shell`'s
|
|
`InstallHost`, `StoreView` and install controller; the tooling is `waybetter store`
|
|
(docs/STORE.md).
|
|
- `host` (feature `tauri`): `shell_only`, `tab_only`, `site_tab_only`, `emit_shell`, `tab_webview`,
|
|
`eval_in_site_tabs`, `navigate_tab`, `tab_action`, `forward_shortcut`, `forward_link_drag`,
|
|
`read_json`/`write_json`, `remove_profile_dirs`, `prune_profile_dirs`, `migrate_identifier`,
|
|
`system_menu`.
|
|
|
|
Besides the commands above, the host serves optional groups of shell-only commands that an app turns
|
|
on with `AppConfig::with_*` (see "Optional command groups" in the README): `commands::STORE`,
|
|
`MEDIA`, `GFONTS`, `UPDATE`, `SYNC`, `DEV`, `WORKSPACES` and `BACKUP`, with the `SiteHooks` methods
|
|
that carry what only the app knows (`fonts_changed`, `backup_sections`, `backup_section_names`,
|
|
`backup_drafts`, `backup_apply`, `dev_theme_apply`, `dev_theme_end`). The app keeps its own
|
|
`#[tauri::command]` wrappers for everything site-specific, and its capability files; its `build.rs`
|
|
lists its commands through `commands::manifest`, which adds the host's.
|
|
|
|
The npm package adds the app-side tooling:
|
|
|
|
- `waybetter/build`: `buildRuntime({ entry, outfile, catalog, prefixes, ... })` bundles a page
|
|
runtime into one IIFE with the app's esbuild (`?inline` CSS as text, other CSS dropped, only the
|
|
runtime's own catalog prefixes compiled in).
|
|
- `waybetter release windows|macos`: signed native releases and the update manifest from the
|
|
app's `waybetter.release.json` (product and file names, key folder, CDN and API URLs, an optional
|
|
manifest validator). Windows: the signed NSIS build, the installer and `.sig` in
|
|
`<outDir>/upload`, `latest.json` (other platforms of the version kept); macOS: signs a universal
|
|
build's updater bundle and adds both darwin platforms. It never uploads: it prints the steps.
|
|
The key is read only into the build's environment or handed to the Tauri signer by path.
|
|
- `waybetter build-env -- <command>` and `waybetter privacy-scan`: nothing an app publishes may
|
|
name the person who built it. A Rust binary embeds the absolute paths of its sources (the Cargo
|
|
registry, the rustup toolchain, the workspace: about 1,400 strings holding the account name in
|
|
one app), so `build-env` runs a build with those roots remapped to `/home`, `/src`, `/cargo`,
|
|
`/rustup` and `/target` (`--remap-path-prefix` appended to the flags already set,
|
|
`/PDBALTPATH:%_PDB%` on MSVC, `-ffile-prefix-map` for C sources on gcc and clang). MSVC has no
|
|
prefix map, so there the C and C++ a build script compiles (a CMake library, the cc crate) lose
|
|
the same roots through `/d1trimfile:<root>` in the `CL` variable, and any PDB they link through
|
|
`/PDBALTPATH` in `LINK`; cl.exe and link.exe read both themselves, whatever runs them:
|
|
`waybetter build-env -- tauri build`, on every platform. `privacy-scan` is the proof: it searches
|
|
files, binaries (UTF-8, UTF-16), archives (npm tarballs, zip, tar, .deb, DMG; NSIS installers and
|
|
AppImages through 7-Zip), git-tracked files and, with `--history`, every blob, path, commit
|
|
message and identity of a repository for the terms in a file kept outside every repository
|
|
(`"forbiddenTerms"` in `waybetter.release.json`, or `WAYBETTER_FORBIDDEN_TERMS`) plus the
|
|
machine's account name, and prints hits masked (`m***`). `waybetter release` builds with
|
|
`build-env` and refuses to release an artifact with a hit or one it could not open. The `.pdb`
|
|
next to a Windows binary still holds every path: never publish it.
|
|
- `waybetter strings check <pack>`: a string pack (data that replaces the wrapped site's own UI
|
|
strings) against the site's string inventory: only inventory keys, parameter names, plural and
|
|
select kinds, no markup; prints the coverage. See docs/STRING-PACKS.md.
|
|
- `waybetter i18n check|extract`: an app's locale catalogs against the framework's rules (file
|
|
shape, keys English lacks, ICU syntax, placeholder parity, plural forms, no markup), the
|
|
framework's catalogs in the same languages, the generated `languages.json` and native
|
|
localization files (macOS `Info.plist` and `InfoPlist.strings`, the Linux `.desktop` template),
|
|
bundled plugins' inline English, and the keys the code looks up; configured by the app's
|
|
`waybetter.i18n.json`.
|
|
- `waybetter/windows/languages/<Language>.nsh`: the installer's messages for the 17 languages
|
|
Tauri does not ship itself, generated from the framework catalogs (`wb.installer.*`;
|
|
`node packages/waybetter/scripts/installer-languages.mjs --write`). An app lists them under
|
|
`bundle.windows.nsis.customLanguageFiles`.
|
|
- `waybetter/windows/installer.nsi`: the Windows installer, a template for Tauri's NSIS bundler
|
|
(`bundle.windows.nsis.template`). No wizard and no UAC: it installs for the current user, makes
|
|
the Desktop and Start menu shortcuts and opens the app. Run again by hand, it only installs when
|
|
it is newer than what is installed (an upgrade in place, app data untouched); otherwise it opens
|
|
the installed app and exits 0, so an old setup left in Downloads never replaces or downgrades an
|
|
install the updater has moved on. `/FORCE` reinstalls the same version (repair), `/NORUN` does
|
|
not open the app, `/UPDATE` is the updater's path and always installs. An app renamed after its
|
|
first release keeps its registry keys by defining `WAYBETTER_LEGACY_NAME` in its installer hooks
|
|
(`bundle.windows.nsis.installerHooks`). An install made under the old name (its `DisplayName` is
|
|
still that name) counts as older than any setup of the renamed app, whatever its version number:
|
|
the renamed app may start its numbering again and still upgrades such an install in place.
|
|
|
|
## Consuming it
|
|
|
|
wayBetter for X adds this repository as a git submodule at `waybetter/` and consumes it from source:
|
|
|
|
- Rust: a path dependency, `waybetter-host = { path = "../../../waybetter/crates/waybetter-host" }`,
|
|
with `features = ["app", "net", "macos-private-api"]` enabled by the app's desktop feature (and
|
|
as a build-dependency for `commands::manifest`).
|
|
- TypeScript: relative imports of `waybetter/packages/*/src/*.ts`: the X site for the shell frame
|
|
(`src/shell/site.tsx`), the page runtime's adapter and a few X modules. Its Vite config dedupes
|
|
React, dockview and the Tauri API (the frame's imports use the app's copies) and serves
|
|
`bootScript()` as `/boot.js`. The
|
|
packages are not Bun workspace members of the app on purpose: the app's server CI clones without
|
|
submodules and runs `bun i`, which fails when a declared workspace package is missing; and Node's
|
|
type stripping refuses `.ts` files under `node_modules`, while sources outside it work with Bun's
|
|
isolated linker on Windows.
|
|
|
|
## Dev loop
|
|
|
|
`waybetter dev [--stable] --watch "<build watcher>"...` starts the app's watchers, waits for their
|
|
first build and runs `tauri dev` (`--stable` adds `--no-watch`: a Rust file saved by someone else
|
|
does not restart the app; restart it yourself after Rust changes). Debug builds only:
|
|
|
|
- The app names its page scripts with `waybetter_host::page_script!(include_str!("../x.js"), "x.js")`
|
|
in `SiteHooks::init_scripts`. A debug build reads the file from disk (path relative to the app
|
|
crate, falling back to the embedded copy) each time a tab's init scripts are built and watches it
|
|
(`devloop`, using `devmode`'s debounced signature watcher). When it changes, every tab webview is
|
|
rebuilt on its current URL through `tabs_sync`, because a webview's document-start scripts are
|
|
fixed at creation. Route is kept, scroll is not. About 1 s from save to visible.
|
|
- `buildRuntime({ watch: watchRequested(), ... })` (`waybetter/build`) keeps esbuild rebuilding the
|
|
file; a failed build leaves the last good one.
|
|
- The shell reloads through the frontend dev server (`beforeDevCommand`), instantly.
|
|
- Restart needed: Rust, `tauri.conf.json`, capabilities, bundled plugin sources (`include_str!` in the
|
|
plugin table; installed plugin files reload with developer mode as before).
|
|
- Release builds contain none of it: `devloop` is `cfg(debug_assertions)`, the macro's release arm is
|
|
`$embedded.to_owned()` and the path literal is never expanded. `release_guard` (lib.rs) checks that
|
|
every mention is gated; `cargo test --release` also runs `release_page_script_is_the_embedded_copy`.
|
|
- `.taurignore` the watched output files (`observer.js`, `x-runtime.js`), or plain `tauri dev` restarts
|
|
on every rebuild. `create-waybetter` apps get `npm run dev` and `npm run dev:stable`.
|
|
|
|
## Scripts (no CI)
|
|
|
|
```sh
|
|
bun install # typescript + @types/node for the checks
|
|
bun run typecheck # tsc over both packages and their tests, and create-waybetter (checkJs)
|
|
bun run test # node --test (runtime + shell), cargo test (headless host), create-waybetter
|
|
bun run test:ts
|
|
bun run test:rust
|
|
bun run test:create # scaffolds into temp dirs; its cargo check of an app is ~1 min cold
|
|
# (WAYBETTER_SKIP_CARGO_CHECK=1 skips it; needs the local crate cache)
|
|
bun run check # typecheck + build:packages + test
|
|
bun run check:tauri # cargo check with the app feature (slow: builds Tauri and its plugins)
|
|
bun run build # typecheck + cargo build
|
|
bun run pack:check # npm pack --dry-run of waybetter and create-waybetter
|
|
node packages/waybetter/scripts/installer-languages.mjs [--write] # windows/languages/*.nsh from the catalogs
|
|
node packages/shell/scripts/google-fonts-catalog.mjs # the font browser's offline list
|
|
```
|
|
|
|
`dev-tools/` has the harnesses for a running or built app (perf probes, the graceful-close check,
|
|
tab and webview stress); see its README.
|
|
|
|
Requires Bun 1.4.2+, Node 24+ and Rust 1.88+.
|