host: site themes are the host's (AppConfig::with_themes, commands::THEME)

theme_commands serves theme_get / theme_set / theme_preview / theme_scopes_get / theme_scopes_set
(shell-only, each checks its caller first; testkit::assert_theme_guards) over the theme state the
host now keeps: the owner's theme, account and pane themes, the Theme Builder preview. The app
only names its built-in themes and the two files (ThemeConfig). apply() themes every site tab
(script, cached Google font bytes, native background); page_scripts()/tab_background() serve a new
document; set_theme/set_scopes/set_preview are the app's own writers (backup import, dev mode).
A font install or removal re-themes the tabs in the host itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CRTkafa 2026-10-01 14:10:43 +07:00
parent 260646bbc0
commit 82382293a2
6 changed files with 276 additions and 7 deletions

View file

@ -118,9 +118,14 @@ pub const DEV: &[&str] = &["dev_get", "dev_set", "dev_pick_theme", "dev_clear_th
/// (`SiteHooks::backup_sections`, `backup_drafts`, `backup_apply`).
pub const BACKUP: &[&str] = &["backup_collect", "backup_export_file", "backup_open_file", "backup_apply"];
/// Shell-only, optional: site themes (the owner's theme, account and pane themes, the Theme
/// Builder's live preview), served when the app sets `AppConfig::themes`. An app that uses them
/// grants these to the shell (`testkit::Capabilities::assert_shell_only_group`).
pub const THEME: &[&str] = &["theme_get", "theme_set", "theme_preview", "theme_scopes_get", "theme_scopes_set"];
/// Whether `command` is one of the host's (dispatched to the host, never to the app's handler).
pub fn is_host(command: &str) -> bool {
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command) || DEV.contains(&command) || BACKUP.contains(&command)
SHELL.contains(&command) || TAB.contains(&command) || SITE.contains(&command) || STORE.contains(&command) || MEDIA.contains(&command) || GFONTS.contains(&command) || UPDATE.contains(&command) || WORKSPACES.contains(&command) || SYNC.contains(&command) || DEV.contains(&command) || BACKUP.contains(&command) || THEME.contains(&command)
}
/// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`).
@ -135,8 +140,8 @@ pub fn permission(command: &str) -> String {
/// tauri_build::AppManifest::new().commands(waybetter_host::commands::manifest(&["draft_list"]))
/// ```
pub fn manifest(app: &[&'static str]) -> &'static [&'static str] {
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len() + DEV.len() + BACKUP.len());
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP) {
let mut all: Vec<&'static str> = Vec::with_capacity(app.len() + SHELL.len() + TAB.len() + SITE.len() + STORE.len() + MEDIA.len() + GFONTS.len() + UPDATE.len() + WORKSPACES.len() + SYNC.len() + DEV.len() + BACKUP.len() + THEME.len());
for command in app.iter().chain(SHELL).chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME) {
if !all.contains(command) {
all.push(command);
}
@ -153,7 +158,7 @@ mod tests {
let all = manifest(&["draft_list", "tabs_sync"]);
assert_eq!(all[0], "draft_list");
assert_eq!(all.iter().filter(|c| **c == "tabs_sync").count(), 1);
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP) {
for command in SHELL.iter().chain(TAB).chain(SITE).chain(STORE).chain(MEDIA).chain(GFONTS).chain(UPDATE).chain(WORKSPACES).chain(SYNC).chain(DEV).chain(BACKUP).chain(THEME) {
assert!(all.contains(command), "{command}");
assert!(is_host(command));
}
@ -163,7 +168,7 @@ mod tests {
#[test]
fn shell_and_tab_commands_do_not_overlap() {
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command) || UPDATE.contains(command) || WORKSPACES.contains(command) || SYNC.contains(command) || DEV.contains(command) || BACKUP.contains(command)));
assert!(!SHELL.iter().any(|command| TAB.contains(command) || SITE.contains(command) || STORE.contains(command) || MEDIA.contains(command) || GFONTS.contains(command) || UPDATE.contains(command) || WORKSPACES.contains(command) || SYNC.contains(command) || DEV.contains(command) || BACKUP.contains(command) || THEME.contains(command)));
assert!(!TAB.iter().any(|command| SITE.contains(command)));
}
}