host: run_app — the window, shell, tab webviews and generic commands for every app
The `app` feature (App / run_app, AppConfig, SiteHooks): registers the Tauri plugins every app uses (single-instance, deep-link and updater opt-in, window-state, opener, process, notification, dialog), makes app data, loads profiles and the language, builds the window (restore, lost-geometry reset, helper-close routing, native resize follow) and the transparent shell, and serves tabs_sync, tab_navigate, tab_action, open_external_url, shell_focus, shell_raise, window_system_menu, profiles_get/set, i18n_get/set (shell-only) and tab_shortcut, tab_link_drag (tab-only). The tab builder is generic; the app fills in SiteHooks. `commands` lists them (manifest() for build.rs), `testkit` checks capability files and caller guards, i18n::page_script is origin-guarded. check:tauri now checks the app feature.
This commit is contained in:
parent
8c93fc470d
commit
d051762359
8 changed files with 1616 additions and 45 deletions
437
crates/waybetter-host/Cargo.lock
generated
437
crates/waybetter-host/Cargo.lock
generated
|
|
@ -82,7 +82,7 @@ dependencies = [
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.60.2",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -576,7 +576,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -624,6 +624,26 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
|
|
@ -1003,6 +1023,15 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||||
|
dependencies = [
|
||||||
|
"const-random",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dom_query"
|
name = "dom_query"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
|
|
@ -1490,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1692,6 +1721,12 @@ version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
|
|
@ -1829,7 +1864,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-registry",
|
"windows-registry 0.6.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1844,7 +1879,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2046,6 +2081,25 @@ version = "2.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
|
@ -2089,7 +2143,7 @@ dependencies = [
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
"portable-atomic-util",
|
"portable-atomic-util",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2159,7 +2213,7 @@ dependencies = [
|
||||||
"simd_cesu8",
|
"simd_cesu8",
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2754,6 +2808,17 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -2766,6 +2831,16 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2841,7 +2916,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3311,6 +3386,30 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||||
|
dependencies = [
|
||||||
|
"block2",
|
||||||
|
"dispatch2",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk-sys",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation",
|
||||||
|
"raw-window-handle",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
|
|
@ -3325,6 +3424,16 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
|
@ -4044,8 +4153,8 @@ dependencies = [
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
]
|
]
|
||||||
|
|
@ -4126,7 +4235,7 @@ dependencies = [
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4207,6 +4316,69 @@ dependencies = [
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-deep-link"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77cb298007f2f26977c9ff1799d1983a453945d646a47ba2ae3c58c663a2458f"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"plist",
|
||||||
|
"rust-ini",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"windows-registry 0.5.3",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-dialog"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7527c5fd1678032d116c1a82e5d609c67370e56f858ae6cfd376bbca37fe0490"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rfd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "466eb32466fe5021b6728b06862f9ff14a9f7e1af890047654e03a96d7c0e1bb"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"log",
|
||||||
|
"objc2-foundation",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"toml 1.1.6+spec-1.1.0",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-notification"
|
name = "tauri-plugin-notification"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -4226,6 +4398,55 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-opener"
|
||||||
|
version = "2.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60d60366174b745b4ef5824b8bbc1c457fd08f0ce101ff643c0a49181a9f4e91"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
|
"open",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"url",
|
||||||
|
"windows 0.61.3",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-process"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "74ae3d8549ae564c0397377373e262f340908eb23a5b9fbe03f2a00007dc0038"
|
||||||
|
dependencies = [
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-single-instance"
|
||||||
|
version = "2.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db817fe9295e19b7d8357e900af31edb93703dd9fb6de524b007b47b6afc63b0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
|
|
@ -4259,6 +4480,21 @@ dependencies = [
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-window-state"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
|
|
@ -4281,7 +4517,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4306,7 +4542,7 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4366,7 +4602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -4476,6 +4712,15 @@ dependencies = [
|
||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
|
@ -4981,13 +5226,19 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-process",
|
||||||
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tauri-plugin-window-state",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5083,8 +5334,8 @@ checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5105,8 +5356,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
|
checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror 2.0.21",
|
"thiserror 2.0.21",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5162,16 +5413,38 @@ dependencies = [
|
||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.61.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections 0.2.0",
|
||||||
|
"windows-core 0.61.2",
|
||||||
|
"windows-future 0.2.1",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-numerics 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.62.2"
|
version = "0.62.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-collections",
|
"windows-collections 0.3.2",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
"windows-future",
|
"windows-future 0.3.2",
|
||||||
"windows-numerics",
|
"windows-numerics 0.3.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5180,7 +5453,20 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
"windows-strings 0.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5191,9 +5477,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
"windows-result",
|
"windows-result 0.4.1",
|
||||||
"windows-strings",
|
"windows-strings 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.61.2",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-threading 0.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5202,9 +5499,9 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
"windows-threading",
|
"windows-threading 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5229,20 +5526,47 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-numerics"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.61.2",
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-numerics"
|
name = "windows-numerics"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-registry"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
"windows-strings 0.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5251,9 +5575,18 @@ version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
"windows-result",
|
"windows-result 0.4.1",
|
||||||
"windows-strings",
|
"windows-strings 0.5.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5262,7 +5595,16 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5271,7 +5613,7 @@ version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5316,7 +5658,7 @@ version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5356,7 +5698,7 @@ version = "0.53.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
"windows_aarch64_gnullvm 0.53.1",
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
"windows_aarch64_msvc 0.53.1",
|
"windows_aarch64_msvc 0.53.1",
|
||||||
"windows_i686_gnu 0.53.1",
|
"windows_i686_gnu 0.53.1",
|
||||||
|
|
@ -5367,13 +5709,22 @@ dependencies = [
|
||||||
"windows_x86_64_msvc 0.53.1",
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.1.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5382,7 +5733,7 @@ version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
|
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5601,8 +5952,8 @@ dependencies = [
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.62.2",
|
||||||
"windows-core",
|
"windows-core 0.62.2",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,12 @@ default = []
|
||||||
tauri = ["dep:tauri", "dep:tauri-plugin-notification", "dep:tauri-plugin-updater", "dep:tokio", "dep:arboard", "dep:clipboard-win", "dep:windows", "dep:webview2-com", "dep:gtk", "dep:objc2-app-kit"]
|
tauri = ["dep:tauri", "dep:tauri-plugin-notification", "dep:tauri-plugin-updater", "dep:tokio", "dep:arboard", "dep:clipboard-win", "dep:windows", "dep:webview2-com", "dep:gtk", "dep:objc2-app-kit"]
|
||||||
# Store downloads (`store::net`): allow-listed https, no redirects or cookies, size caps.
|
# Store downloads (`store::net`): allow-listed https, no redirects or cookies, size caps.
|
||||||
net = ["dep:reqwest", "dep:rustls"]
|
net = ["dep:reqwest", "dep:rustls"]
|
||||||
|
# The whole app (`app::run_app`): window, shell and tab webviews, the generic commands and the
|
||||||
|
# Tauri plugins every wayBetter app registers.
|
||||||
|
app = ["tauri", "dep:tauri-plugin-single-instance", "dep:tauri-plugin-deep-link", "dep:tauri-plugin-window-state", "dep:tauri-plugin-opener", "dep:tauri-plugin-process", "dep:tauri-plugin-dialog"]
|
||||||
|
# macOS: the shell webview is transparent (it can rise above the tabs for its overlays); needs
|
||||||
|
# `"macOSPrivateApi": true` in the app's tauri.conf.json. Other platforms always are.
|
||||||
|
macos-private-api = ["tauri?/macos-private-api"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
@ -46,6 +52,14 @@ tauri-plugin-updater = { version = "2", optional = true }
|
||||||
tokio = { version = "1", features = ["time"], optional = true }
|
tokio = { version = "1", features = ["time"], optional = true }
|
||||||
# Opt-in local clipboard history (media::clipboard), polled natively.
|
# Opt-in local clipboard history (media::clipboard), polled natively.
|
||||||
arboard = { version = "3", optional = true }
|
arboard = { version = "3", optional = true }
|
||||||
|
# `app`: single-instance (a second launch focuses the window and hands over its link), install
|
||||||
|
# links, window geometry, external links, relaunch after an update, and native dialogs.
|
||||||
|
tauri-plugin-single-instance = { version = "2", optional = true, features = ["deep-link"] }
|
||||||
|
tauri-plugin-deep-link = { version = "2", optional = true }
|
||||||
|
tauri-plugin-window-state = { version = "2", optional = true }
|
||||||
|
tauri-plugin-opener = { version = "2", optional = true }
|
||||||
|
tauri-plugin-process = { version = "2", optional = true }
|
||||||
|
tauri-plugin-dialog = { version = "2", optional = true }
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"], optional = true }
|
reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"], optional = true }
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring"], optional = true }
|
rustls = { version = "0.23", default-features = false, features = ["ring"], optional = true }
|
||||||
|
|
||||||
|
|
|
||||||
869
crates/waybetter-host/src/app.rs
Normal file
869
crates/waybetter-host/src/app.rs
Normal file
|
|
@ -0,0 +1,869 @@
|
||||||
|
//! The whole wayBetter app around one site: [`run_app`] (or [`App`]) makes the window, the trusted
|
||||||
|
//! local shell webview and one child webview per tab, registers the Tauri plugins every app uses,
|
||||||
|
//! and serves the generic commands ([`crate::commands`]): tab layout and actions, the shell's
|
||||||
|
//! focus and raise, external links, the window menu, account profiles and the language.
|
||||||
|
//!
|
||||||
|
//! The app hands it an [`AppConfig`] (its site, window title and size, which plugins it
|
||||||
|
//! configures) and a [`SiteHooks`] implementation for what only it knows (the page scripts of a
|
||||||
|
//! new tab, what happens when a site page loads, its own per-tab state), plus its own commands.
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! pub fn run() {
|
||||||
|
//! waybetter_host::app::App::new(CONFIG, MySite)
|
||||||
|
//! .setup(|app| { app.manage(MyState::load(app)); Ok(()) })
|
||||||
|
//! .run(tauri::generate_context!(), tauri::generate_handler![my_command]);
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
commands,
|
||||||
|
host::{self, emit_shell, emit_tab_state, read_json, shell_only, tab_only, tab_webview, write_json},
|
||||||
|
i18n::{self, NativeStrings},
|
||||||
|
layout, policy,
|
||||||
|
profiles::{self, AccountSeen, ProfileSettings},
|
||||||
|
site::SiteSpec,
|
||||||
|
tabs::{self, parse_web_url, OpenRequest, TabLayout, TabState},
|
||||||
|
};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
use tauri::webview::{DownloadEvent, NewWindowResponse, PageLoadEvent};
|
||||||
|
use tauri::{AppHandle, LogicalPosition, LogicalSize, Manager, Webview, WebviewBuilder, WebviewUrl, Wry};
|
||||||
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// The account profiles (app data), see [`profiles`].
|
||||||
|
pub const PROFILES_FILE: &str = "profiles.json";
|
||||||
|
/// The owner's language and the strings native code needs (app data), see [`i18n`].
|
||||||
|
pub const I18N_FILE: &str = "i18n.json";
|
||||||
|
|
||||||
|
/// What differs between wayBetter apps at the window level.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub site: &'static SiteSpec,
|
||||||
|
/// Window title (and the name in "failed to launch" messages).
|
||||||
|
pub title: &'static str,
|
||||||
|
/// A previous bundle identifier whose data folders are moved over once ([`host::migrate_identifier`]).
|
||||||
|
pub legacy_identifier: Option<&'static str>,
|
||||||
|
/// First-launch window size and the smallest size, in logical px.
|
||||||
|
pub window_size: (f64, f64),
|
||||||
|
pub min_window_size: (f64, f64),
|
||||||
|
/// The window's color before the shell paints (no white flash).
|
||||||
|
pub background: (u8, u8, u8),
|
||||||
|
/// The shell draws the titlebar (Windows/Linux: frameless; macOS: an overlay titlebar with the
|
||||||
|
/// traffic lights over the shell). Off: the OS frame, for a shell without one.
|
||||||
|
pub custom_titlebar: bool,
|
||||||
|
/// Register the deep-link plugin (`plugins.deep-link` in tauri.conf.json: install links).
|
||||||
|
pub deep_link: bool,
|
||||||
|
/// Register the updater plugin (`plugins.updater` in tauri.conf.json: endpoint and key).
|
||||||
|
pub updater: bool,
|
||||||
|
/// The language strings site tabs receive (key prefixes, e.g. `["x.", "kit.", "plugin."]`).
|
||||||
|
pub page_string_prefixes: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
/// The defaults: 1360x900 (at least 900x560), a near-black background, the shell's own
|
||||||
|
/// titlebar, no deep links or updater, the UI kit's and plugins' strings for site tabs.
|
||||||
|
pub const fn new(site: &'static SiteSpec, title: &'static str) -> Self {
|
||||||
|
Self {
|
||||||
|
site,
|
||||||
|
title,
|
||||||
|
legacy_identifier: None,
|
||||||
|
window_size: (1360.0, 900.0),
|
||||||
|
min_window_size: (900.0, 560.0),
|
||||||
|
background: (7, 8, 12),
|
||||||
|
custom_titlebar: true,
|
||||||
|
deep_link: false,
|
||||||
|
updater: false,
|
||||||
|
page_string_prefixes: &["kit.", "plugin."],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tab webview about to be created ([`SiteHooks::init_scripts`]).
|
||||||
|
pub struct NewTab<'a> {
|
||||||
|
pub id: &'a str,
|
||||||
|
/// `tab-<id>`
|
||||||
|
pub label: &'a str,
|
||||||
|
/// Its account profile (fixed for the webview's life).
|
||||||
|
pub profile: &'a str,
|
||||||
|
/// The shell's layout entry it is created for (role, navigation rail, ...).
|
||||||
|
pub item: &'a TabLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why a tab webview was closed ([`SiteHooks::tab_closed`]).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum TabClosed {
|
||||||
|
/// Its tab moved to another account profile: a fresh webview replaces it.
|
||||||
|
Replaced,
|
||||||
|
/// Its tab is gone.
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What only the app knows. Every method has a harmless default except the page scripts.
|
||||||
|
/// Called without any host lock held, so an implementation may use the host's helpers.
|
||||||
|
pub trait SiteHooks: Send + Sync + 'static {
|
||||||
|
/// The document-start scripts of a new tab webview, in order, the tab bridge included
|
||||||
|
/// (`waybetter_host::bridge`), then the app's page runtime.
|
||||||
|
fn init_scripts(&self, app: &AppHandle, tab: &NewTab) -> Vec<String>;
|
||||||
|
|
||||||
|
/// The new tab webview's background until the page paints.
|
||||||
|
fn tab_background(&self, _app: &AppHandle, _label: &str) -> Option<tauri::window::Color> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The URL a navigation to `url` should load instead (legacy domains), if any.
|
||||||
|
fn canonical_url(&self, _url: &Url) -> Option<Url> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pages that stay "on the site" for link routing besides its exact origin (login routes).
|
||||||
|
fn site_navigation(&self, _url: &Url) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A navigation starts in tab `label`. Returns whether links to other sites go to the
|
||||||
|
/// system browser (the owner's setting), or None when the app's state is unavailable (the
|
||||||
|
/// navigation is refused).
|
||||||
|
fn navigation_started(&self, _app: &AppHandle, _label: &str) -> Option<bool> {
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a new-window request (target=_blank, window.open) for `url` opens: "tab", "pane" or
|
||||||
|
/// "browser".
|
||||||
|
fn new_window_mode(&self, _app: &AppHandle, _url: &Url) -> String {
|
||||||
|
"tab".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new document started loading in `tab`.
|
||||||
|
fn page_started(&self, _app: &AppHandle, _tab: &Webview) {}
|
||||||
|
|
||||||
|
/// A document on the site finished loading in `tab` (evaluate settings, plugins, themes).
|
||||||
|
fn site_page_loaded(&self, _app: &AppHandle, _tab: &Webview, _url: &Url) {}
|
||||||
|
|
||||||
|
/// A tab webview was created.
|
||||||
|
fn tab_created(&self, _app: &AppHandle) {}
|
||||||
|
|
||||||
|
/// `tab` was laid out for `item` (existing or new), before it is placed.
|
||||||
|
fn tab_placed(&self, _app: &AppHandle, _tab: &Webview, _item: &TabLayout) {}
|
||||||
|
|
||||||
|
/// Every tab's placement after a layout: (id, rect, visible).
|
||||||
|
fn layout_changed(&self, _app: &AppHandle, _layout: &[(String, layout::Rect, bool)]) {}
|
||||||
|
|
||||||
|
/// A tab webview was closed: forget the app's per-tab state.
|
||||||
|
fn tab_closed(&self, _app: &AppHandle, _label: &str, _why: TabClosed) {}
|
||||||
|
|
||||||
|
/// The shell was raised above the tabs (`on`) or lowered again.
|
||||||
|
fn shell_raised(&self, _app: &AppHandle, _on: bool) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The account profiles and which profile each open tab webview belongs to.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ProfileState {
|
||||||
|
pub settings: ProfileSettings,
|
||||||
|
/// Tab webview label -> the profile its WebView data directory belongs to.
|
||||||
|
pub tab_profiles: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Profiles(Mutex<ProfileState>);
|
||||||
|
struct Strings(Mutex<NativeStrings>);
|
||||||
|
|
||||||
|
struct Host {
|
||||||
|
config: AppConfig,
|
||||||
|
hooks: Arc<dyn SiteHooks>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_state(app: &AppHandle) -> tauri::State<'_, Host> {
|
||||||
|
app.state::<Host>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app's config ([`run_app`] must be running).
|
||||||
|
pub fn config(app: &AppHandle) -> AppConfig {
|
||||||
|
host_state(app).config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The profiles lock. Never call an app hook while holding it.
|
||||||
|
pub fn profiles(app: &AppHandle) -> Result<MutexGuard<'_, ProfileState>, String> {
|
||||||
|
app.try_state::<Profiles>()
|
||||||
|
.ok_or("DATABASE: workspace state unavailable; restart safely")?
|
||||||
|
.inner()
|
||||||
|
.0
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| "DATABASE: workspace state unavailable; restart safely".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The profile tab webview `label` belongs to.
|
||||||
|
pub fn tab_profile(app: &AppHandle, label: &str) -> Option<String> {
|
||||||
|
profiles(app).ok()?.tab_profiles.get(label).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tab reported the account the site shows in it. Recorded only for that webview's own profile
|
||||||
|
/// (validated by the site's [`crate::site::AccountRules`]); the shell hears about a change. The
|
||||||
|
/// caller checks the webview is on the site right now.
|
||||||
|
pub fn account_seen(app: &AppHandle, label: &str, seen: AccountSeen) -> Result<(), String> {
|
||||||
|
let site = config(app).site;
|
||||||
|
let changed = {
|
||||||
|
let mut data = profiles(app)?;
|
||||||
|
let profile = data.tab_profiles.get(label).cloned();
|
||||||
|
match profile {
|
||||||
|
Some(profile) if data.settings.observe(&profile, seen, &site.accounts) => Some(data.settings.clone()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(settings) = changed {
|
||||||
|
write_json(app, PROFILES_FILE, &settings)?;
|
||||||
|
emit_shell(app, tabs::EVENT_PROFILES_CHANGED, settings);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens an http(s) or mailto link in the system browser / mail app; other schemes are dropped.
|
||||||
|
pub fn open_external(app: &AppHandle, url: &Url) {
|
||||||
|
if matches!(url.scheme(), "http" | "https" | "mailto") {
|
||||||
|
let _ = app.opener().open_url(url.as_str(), None::<&str>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a native OS notification under the app's name.
|
||||||
|
pub fn notify(app: &AppHandle, title: &str, body: &str) {
|
||||||
|
crate::alerts::notify(app, config(app).site.product, title, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_strings<T>(app: &AppHandle, f: impl FnOnce(&NativeStrings) -> T) -> Option<T> {
|
||||||
|
let state = app.try_state::<Strings>()?;
|
||||||
|
let strings = state.0.lock().ok()?;
|
||||||
|
Some(f(&strings))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The native string `key` in the owner's language, or the compiled-in English `fallback`.
|
||||||
|
pub fn tr(app: &AppHandle, key: &str, fallback: &str) -> String {
|
||||||
|
with_strings(app, |strings| strings.get(key, fallback).to_owned()).unwrap_or_else(|| fallback.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`tr`] with `{name}` placeholders filled in (values are plain text).
|
||||||
|
pub fn trf(app: &AppHandle, key: &str, fallback: &str, params: &[(&str, &str)]) -> String {
|
||||||
|
with_strings(app, |strings| strings.format(key, fallback, params))
|
||||||
|
.unwrap_or_else(|| NativeStrings::default().format(key, fallback, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The script handing a site tab its strings ([`AppConfig::page_string_prefixes`]), guarded to
|
||||||
|
/// the site's origin ([`i18n::page_script`]). Evaluate it at document start and on every load.
|
||||||
|
pub fn i18n_page_script(app: &AppHandle) -> String {
|
||||||
|
let config = config(app);
|
||||||
|
let payload = with_strings(app, |strings| strings.page_payload(config.page_string_prefixes))
|
||||||
|
.unwrap_or_else(|| serde_json::json!({ "locale": "en", "dir": "ltr", "messages": {} }));
|
||||||
|
i18n::page_script(config.site, &payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the shell webview sits above the tab webviews (one of its overlays is open).
|
||||||
|
static SHELL_RAISED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Whether the shell is raised above the tabs right now (`shell_raise`).
|
||||||
|
pub fn shell_is_raised() -> bool {
|
||||||
|
SHELL_RAISED.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_site(site: &SiteSpec, hooks: &dyn SiteHooks, url: &Url) -> bool {
|
||||||
|
site.is_origin(url) || hooks.site_navigation(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Commands (commands::SHELL, commands::TAB). Each checks its caller first (testkit::assert_host_guards).
|
||||||
|
|
||||||
|
/// Reconciles native tab webviews with the shell's panes: creates missing ones, positions and
|
||||||
|
/// shows/hides all of them, and closes webviews whose tab no longer exists.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn tabs_sync(webview: Webview, app: AppHandle, items: Vec<TabLayout>, viewport: Option<[f64; 2]>) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
let hooks = host_state(&app).hooks.clone();
|
||||||
|
let window = webview.window();
|
||||||
|
// Kept current by the window's resize events (host::follow_window_resizes): no blocking
|
||||||
|
// round trips to the event loop on every frame of a drag.
|
||||||
|
let metrics = host::window_metrics(&window).map_err(|error| format!("TAB: {error}"))?;
|
||||||
|
let win_size = LogicalSize::new(metrics.width, metrics.height);
|
||||||
|
let (maximized, frameless) = (metrics.maximized, metrics.frameless);
|
||||||
|
// Laid out for a window size that is already gone (the window was resized while this was in
|
||||||
|
// flight): re-based onto the current size like a native resize follow, never shown stale.
|
||||||
|
let rebase = host::layout_window(&metrics, viewport.map(|[width, height]| (width, height))).and_then(|old| {
|
||||||
|
let visible: Vec<_> = items.iter().filter(|item| item.visible).map(|item| (item.x, item.y, item.width, item.height)).collect();
|
||||||
|
layout::Frame::around(&visible, old).map(|frame| (old, frame))
|
||||||
|
});
|
||||||
|
let mut keep = HashSet::new();
|
||||||
|
let mut placed = Vec::new();
|
||||||
|
for item in items {
|
||||||
|
if !policy::valid_tab_id(&item.id) {
|
||||||
|
return Err("VALIDATION: invalid tab id".into());
|
||||||
|
}
|
||||||
|
let label = policy::tab_label(&item.id);
|
||||||
|
let current_profile = {
|
||||||
|
let data = profiles(&app)?;
|
||||||
|
if !profiles::valid_profile_id(&item.profile) || !data.settings.contains(&item.profile) {
|
||||||
|
return Err("VALIDATION: unknown profile".into());
|
||||||
|
}
|
||||||
|
data.tab_profiles.get(&label).cloned()
|
||||||
|
};
|
||||||
|
keep.insert(item.id.clone());
|
||||||
|
let (item_x, item_y, item_width, item_height) = match rebase {
|
||||||
|
Some((old, frame)) => layout::follow_rect((item.x, item.y, item.width, item.height), old, (metrics.width, metrics.height), frame),
|
||||||
|
None => (item.x, item.y, item.width, item.height),
|
||||||
|
};
|
||||||
|
let rect = policy::clamp_tab_rect(item_x, item_y, item_width, item_height, win_size.width, win_size.height, maximized, frameless)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
// Nothing left after clamping: keep (or create) the tab, but hidden.
|
||||||
|
let visible = item.visible && rect.is_some();
|
||||||
|
let (x, y, width, height) = rect.unwrap_or((0.0, policy::TITLEBAR_H, 1.0, 1.0));
|
||||||
|
let existing = tab_webview(&app, &item.id);
|
||||||
|
// A webview can never switch data directories: a profile change means a fresh webview,
|
||||||
|
// and the app's per-tab state goes with the old one.
|
||||||
|
let existing = match existing {
|
||||||
|
Some(tab) if current_profile.as_deref() != Some(item.profile.as_str()) => {
|
||||||
|
let _ = tab.close();
|
||||||
|
host::forget_tab(&label);
|
||||||
|
profiles(&app)?.tab_profiles.remove(&label);
|
||||||
|
hooks.tab_closed(&app, &label, TabClosed::Replaced);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
let tab = match existing {
|
||||||
|
Some(tab) => tab,
|
||||||
|
None => {
|
||||||
|
let url = parse_web_url(&item.url)?;
|
||||||
|
let data_dir = app.path().app_local_data_dir().ok().and_then(|local| profiles::data_dir(&local, &item.profile));
|
||||||
|
if item.profile != profiles::DEFAULT_PROFILE && data_dir.is_none() {
|
||||||
|
return Err("PROFILE: no local data directory".into());
|
||||||
|
}
|
||||||
|
// Reserve the profile before the webview exists, re-checked under the lock that
|
||||||
|
// profiles_set holds, so a concurrent removal cannot delete its folder under it.
|
||||||
|
{
|
||||||
|
let mut data = profiles(&app)?;
|
||||||
|
if !data.settings.contains(&item.profile) {
|
||||||
|
return Err("VALIDATION: unknown profile".into());
|
||||||
|
}
|
||||||
|
data.tab_profiles.insert(label.clone(), item.profile.clone());
|
||||||
|
}
|
||||||
|
if let Some(dir) = &data_dir {
|
||||||
|
if let Err(error) = std::fs::create_dir_all(dir) {
|
||||||
|
profiles(&app)?.tab_profiles.remove(&label);
|
||||||
|
return Err(format!("PROFILE: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_tab = NewTab { id: &item.id, label: &label, profile: &item.profile, item: &item };
|
||||||
|
let builder = tab_builder(&app, &hooks, &new_tab, url, data_dir);
|
||||||
|
// The site's page color until it paints (never the WebView's white).
|
||||||
|
let builder = match hooks.tab_background(&app, &label) {
|
||||||
|
Some(color) => builder.background_color(color),
|
||||||
|
None => builder,
|
||||||
|
};
|
||||||
|
match window.add_child(builder, LogicalPosition::new(x, y), LogicalSize::new(width, height)) {
|
||||||
|
Ok(tab) => {
|
||||||
|
hooks.tab_created(&app);
|
||||||
|
// A new child webview starts on top: keep a raised shell above it.
|
||||||
|
if shell_is_raised() {
|
||||||
|
host::raise_shell(&webview, true);
|
||||||
|
}
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
profiles(&app)?.tab_profiles.remove(&label);
|
||||||
|
return Err(format!("TAB: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hooks.tab_placed(&app, &tab, &item);
|
||||||
|
host::place_tab(&tab, (x, y, width, height), visible, item.parked || rect.is_none());
|
||||||
|
placed.push((item.id.clone(), (x, y, width, height), visible));
|
||||||
|
}
|
||||||
|
host::layout_placed(&metrics);
|
||||||
|
hooks.layout_changed(&app, &placed);
|
||||||
|
for existing in window.webviews() {
|
||||||
|
if let Some(id) = policy::tab_id(existing.label()) {
|
||||||
|
if !keep.contains(id) {
|
||||||
|
let label = existing.label().to_owned();
|
||||||
|
let _ = existing.close();
|
||||||
|
host::forget_tab(&label);
|
||||||
|
if let Ok(mut data) = profiles(&app) {
|
||||||
|
data.tab_profiles.remove(&label);
|
||||||
|
}
|
||||||
|
hooks.tab_closed(&app, &label, TabClosed::Removed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the native window system menu (Restore, Move, Size, Minimize, Maximize, Close) at a
|
||||||
|
/// screen position, for a right-click on the custom titlebar. No-op outside Windows.
|
||||||
|
#[tauri::command]
|
||||||
|
fn window_system_menu(webview: Webview, x: f64, y: f64) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
host::system_menu(&webview, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn tab_navigate(webview: Webview, app: AppHandle, id: String, url: String) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
host::navigate_tab(&app, &id, &url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// back | forward | reload | stop | focus
|
||||||
|
#[tauri::command]
|
||||||
|
async fn tab_action(webview: Webview, app: AppHandle, id: String, action: String) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
host::tab_action(&app, &id, &action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives keyboard focus back to the shell (e.g. Ctrl+L pressed inside a tab).
|
||||||
|
#[tauri::command]
|
||||||
|
async fn shell_focus(webview: Webview) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
webview.set_focus().map_err(|error| format!("FOCUS: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raises the shell above the tab webviews while one of its overlays (menu, dialog, palette,
|
||||||
|
/// drag) is open, so the panes stay on screen under it, or lowers it again. Returns whether this
|
||||||
|
/// platform can; when not, the shell hides the tabs for the overlay instead.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn shell_raise(webview: Webview, app: AppHandle, on: bool) -> Result<bool, String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
if on {
|
||||||
|
host_state(&app).hooks.shell_raised(&app, on);
|
||||||
|
}
|
||||||
|
SHELL_RAISED.store(on, Ordering::SeqCst);
|
||||||
|
Ok(host::raise_shell(&webview, on))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_external_url(webview: Webview, app: AppHandle, url: String) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
let url = parse_web_url(&url)?;
|
||||||
|
app.opener().open_url(url.as_str(), None::<&str>).map_err(|error| format!("OPEN: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn profiles_get(webview: Webview, app: AppHandle) -> Result<ProfileSettings, String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
Ok(profiles(&app)?.settings.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the profile list (add, rename, recolor, switch active, remove). A removed profile must
|
||||||
|
/// have no open tab; its WebView data directory (that account's login) is then deleted.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn profiles_set(webview: Webview, app: AppHandle, settings: ProfileSettings) -> Result<ProfileSettings, String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
let site = config(&app).site;
|
||||||
|
let settings = settings.validated(&site.accounts)?;
|
||||||
|
// One lock for check + save + swap: tabs_sync reserves a tab's profile under the same lock,
|
||||||
|
// so no tab can start in a profile between this check and its data folder being deleted.
|
||||||
|
let (settings, removed) = {
|
||||||
|
let mut data = profiles(&app)?;
|
||||||
|
// Accounts are what the tabs saw, never what the shell sends.
|
||||||
|
let settings = settings.keep_accounts_from(&data.settings);
|
||||||
|
let removed = data.settings.removal(&settings, data.tab_profiles.values())?;
|
||||||
|
write_json(&app, PROFILES_FILE, &settings)?;
|
||||||
|
data.settings = settings.clone();
|
||||||
|
(settings, removed)
|
||||||
|
};
|
||||||
|
host::remove_profile_dirs(&app, site, &removed);
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct LanguageInfo {
|
||||||
|
/// `site`, `system`, a locale tag, or empty before the shell ever chose.
|
||||||
|
preference: String,
|
||||||
|
locale: String,
|
||||||
|
/// The OS language preferences, most preferred first.
|
||||||
|
system: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The saved language choice and the OS languages.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn i18n_get(webview: Webview, app: AppHandle) -> Result<LanguageInfo, String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
let (preference, locale) = with_strings(&app, |strings| (strings.preference.clone(), strings.locale.clone())).unwrap_or_default();
|
||||||
|
Ok(LanguageInfo { preference, locale, system: i18n::system_locales() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shell only: the owner's language and the strings native code and site tabs show in it
|
||||||
|
/// (validated plain text; site tabs get only [`AppConfig::page_string_prefixes`], as data).
|
||||||
|
#[tauri::command]
|
||||||
|
async fn i18n_set(webview: Webview, app: AppHandle, strings: NativeStrings) -> Result<(), String> {
|
||||||
|
shell_only(&webview)?;
|
||||||
|
let config = config(&app);
|
||||||
|
let strings = strings.validated()?;
|
||||||
|
write_json(&app, I18N_FILE, &strings)?;
|
||||||
|
let script = i18n::page_script(config.site, &strings.page_payload(config.page_string_prefixes));
|
||||||
|
{
|
||||||
|
let state = app.state::<Strings>();
|
||||||
|
*state.0.lock().map_err(|_| "I18N: language state unavailable")? = strings;
|
||||||
|
}
|
||||||
|
host::eval_in_site_tabs(&app, config.site, &script);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browser shortcuts pressed while a tab has keyboard focus, forwarded to the shell.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn tab_shortcut(webview: Webview, app: AppHandle, shortcut: String) -> Result<(), String> {
|
||||||
|
let id = tab_only(&webview)?;
|
||||||
|
host::forward_shortcut(&app, id, shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A link drag started in this tab (`start`, with the dragged http(s) link), an external link drag
|
||||||
|
/// entered it (`enter`), or this tab's drag ended (`end`). The shell then hides the tab webviews
|
||||||
|
/// and shows its drop zones; the link only ever opens as a new tab, like `window.open`.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn tab_link_drag(webview: Webview, app: AppHandle, phase: String, url: Option<String>) -> Result<(), String> {
|
||||||
|
let id = tab_only(&webview)?;
|
||||||
|
host::forward_link_drag(&app, id, phase, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// Tab webviews
|
||||||
|
|
||||||
|
/// Linux: the page-level part of a tab's `on_navigation`, run when the main frame starts loading
|
||||||
|
/// `url` (WebKitGTK's navigation callback cannot tell iframes from the page). Returns false when
|
||||||
|
/// the load was redirected (a legacy domain) or stopped and handed to the browser.
|
||||||
|
fn main_frame_started(app: &AppHandle, site: &SiteSpec, hooks: &dyn SiteHooks, tab: &Webview, url: &Url) -> bool {
|
||||||
|
static ON_SITE: std::sync::OnceLock<Mutex<HashMap<String, bool>>> = std::sync::OnceLock::new();
|
||||||
|
if let Some(site_url) = hooks.canonical_url(url) {
|
||||||
|
let tab = tab.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = tab.navigate(site_url);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(external_browser) = hooks.navigation_started(app, tab.label()) else { return false };
|
||||||
|
let mut on_site = ON_SITE.get_or_init(Default::default).lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||||
|
let was_on_site = on_site.get(tab.label()).copied().unwrap_or(false);
|
||||||
|
if external_browser && was_on_site && matches!(url.scheme(), "http" | "https") && !in_site(site, hooks, url) {
|
||||||
|
open_external(app, url);
|
||||||
|
let _ = tab.eval("window.stop()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
on_site.insert(tab.label().to_owned(), site.is_origin(url));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new tab webview: its profile's data directory, the app's document-start scripts, and the
|
||||||
|
/// navigation, pop-up, load, title and download handling every wayBetter tab gets.
|
||||||
|
fn tab_builder(app: &AppHandle, hooks: &Arc<dyn SiteHooks>, tab: &NewTab, url: Url, data_dir: Option<PathBuf>) -> WebviewBuilder<Wry> {
|
||||||
|
let site = config(app).site;
|
||||||
|
let tab_id = tab.id.to_owned();
|
||||||
|
let (nav_app, nav_id, nav_hooks) = (app.clone(), tab_id.clone(), hooks.clone());
|
||||||
|
let (new_app, new_id, new_hooks) = (app.clone(), tab_id.clone(), hooks.clone());
|
||||||
|
let (load_app, load_id, load_hooks) = (app.clone(), tab_id.clone(), hooks.clone());
|
||||||
|
let (title_app, title_id) = (app.clone(), tab_id);
|
||||||
|
let mut builder = WebviewBuilder::new(tab.label, WebviewUrl::External(url));
|
||||||
|
// Non-default profiles get their own cookie/storage jar; the default keeps the existing one.
|
||||||
|
if let Some(dir) = data_dir {
|
||||||
|
builder = builder.data_directory(dir);
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
builder = builder.data_store_identifier(profiles::store_identifier(tab.profile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for script in hooks.init_scripts(app, tab) {
|
||||||
|
builder = builder.initialization_script(script);
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
// HTML5 drag and drop in the page: link drags out of it (split/tab drop zones in the shell)
|
||||||
|
// and the site's own file drops, instead of wry's OS file-drop handler (which refuses links).
|
||||||
|
.disable_drag_drop_handler()
|
||||||
|
.on_navigation(move |url| {
|
||||||
|
let app = &nav_app;
|
||||||
|
if !matches!(url.scheme(), "http" | "https") {
|
||||||
|
// about:blank and similar internal documents are fine; other schemes go to the OS.
|
||||||
|
if url.scheme() == "about" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// WebKitGTK: possibly an iframe's own document (see below).
|
||||||
|
if cfg!(target_os = "linux") && matches!(url.scheme(), "blob" | "data") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
open_external(app, url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// WebKitGTK asks this for iframe navigations too, without saying which frame (WebView2
|
||||||
|
// asks only for the page): a signed-out page embedding a sign-in iframe would get that
|
||||||
|
// iframe's URL as the tab's address. On Linux the page-level work below runs from
|
||||||
|
// on_page_load (Started), which is the main frame only.
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if let Some(site_url) = nav_hooks.canonical_url(url) {
|
||||||
|
if let Some(tab) = tab_webview(app, &nav_id) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = tab.navigate(site_url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(external_browser) = nav_hooks.navigation_started(app, &policy::tab_label(&nav_id)) else { return false };
|
||||||
|
let on_site = tab_webview(app, &nav_id).and_then(|tab| tab.url().ok()).is_some_and(|current| site.is_origin(¤t));
|
||||||
|
if external_browser && on_site && !in_site(site, &*nav_hooks, url) {
|
||||||
|
open_external(app, url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
emit_tab_state(app, TabState { id: nav_id.clone(), url: Some(url.to_string()), loading: Some(true), ..Default::default() });
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.on_new_window(move |url, _| {
|
||||||
|
let app = &new_app;
|
||||||
|
if !matches!(url.scheme(), "http" | "https") {
|
||||||
|
open_external(app, &url);
|
||||||
|
return NewWindowResponse::Deny;
|
||||||
|
}
|
||||||
|
let url = new_hooks.canonical_url(&url).unwrap_or(url);
|
||||||
|
let mode = new_hooks.new_window_mode(app, &url);
|
||||||
|
if mode == "browser" {
|
||||||
|
open_external(app, &url);
|
||||||
|
} else {
|
||||||
|
emit_shell(
|
||||||
|
app,
|
||||||
|
tabs::EVENT_TAB_OPEN,
|
||||||
|
OpenRequest { url: url.to_string(), from: Some(new_id.clone()), background: false, beside: mode == "pane" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
NewWindowResponse::Deny
|
||||||
|
})
|
||||||
|
.on_page_load(move |tab, payload| {
|
||||||
|
let app = &load_app;
|
||||||
|
if cfg!(target_os = "linux")
|
||||||
|
&& matches!(payload.event(), PageLoadEvent::Started)
|
||||||
|
&& !main_frame_started(app, site, &*load_hooks, &tab, payload.url())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let finished = matches!(payload.event(), PageLoadEvent::Finished);
|
||||||
|
if !finished {
|
||||||
|
load_hooks.page_started(app, &tab);
|
||||||
|
}
|
||||||
|
emit_tab_state(app, TabState { id: load_id.clone(), url: Some(payload.url().to_string()), loading: Some(!finished), ..Default::default() });
|
||||||
|
if finished && site.is_origin(payload.url()) {
|
||||||
|
load_hooks.site_page_loaded(app, &tab, payload.url());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_document_title_changed(move |_, title| {
|
||||||
|
emit_tab_state(&title_app, TabState { id: title_id.clone(), title: Some(title), ..Default::default() });
|
||||||
|
})
|
||||||
|
.on_download(|_, event| match event {
|
||||||
|
// Ordinary browser downloads land in the user's Downloads folder.
|
||||||
|
DownloadEvent::Requested { .. } => true,
|
||||||
|
_ => true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
// The app
|
||||||
|
|
||||||
|
/// What the window-state plugin saves and restores: never DECORATIONS (a state file from a framed
|
||||||
|
/// build would re-enable the OS frame) or VISIBLE (the window is shown once it is placed).
|
||||||
|
pub fn window_state_flags() -> tauri_plugin_window_state::StateFlags {
|
||||||
|
use tauri_plugin_window_state::StateFlags;
|
||||||
|
StateFlags::all() - StateFlags::DECORATIONS - StateFlags::VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A window state saved while the window was minimized (Windows parks it at -32000,-32000 with a
|
||||||
|
/// caption-sized rectangle) or on a monitor that is gone comes back as a tiny, off-screen window
|
||||||
|
/// in which nothing works. Such geometry is replaced by `size`, centered.
|
||||||
|
pub fn reset_lost_geometry(window: &tauri::Window, size: (f64, f64), min: (f64, f64)) {
|
||||||
|
let Ok(scale) = window.scale_factor() else { return };
|
||||||
|
let too_small = window
|
||||||
|
.inner_size()
|
||||||
|
.map(|size| size.to_logical::<f64>(scale))
|
||||||
|
.is_ok_and(|size| size.width < min.0 || size.height < min.1);
|
||||||
|
let off_screen = match (window.outer_position(), window.outer_size(), window.available_monitors()) {
|
||||||
|
(Ok(position), Ok(size), Ok(monitors)) => !monitors.iter().any(|monitor| {
|
||||||
|
let (mp, ms) = (monitor.position(), monitor.size());
|
||||||
|
// At least a 120 px square of the window must be on some monitor.
|
||||||
|
let overlap_x = (position.x + size.width as i32).min(mp.x + ms.width as i32) - position.x.max(mp.x);
|
||||||
|
let overlap_y = (position.y + size.height as i32).min(mp.y + ms.height as i32) - position.y.max(mp.y);
|
||||||
|
overlap_x >= 120 && overlap_y >= 120
|
||||||
|
}),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if too_small || off_screen {
|
||||||
|
let _ = window.set_size(LogicalSize::new(size.0, size.1));
|
||||||
|
let _ = window.center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host's commands ([`commands::SHELL`], [`commands::TAB`]).
|
||||||
|
fn host_handler() -> impl Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static {
|
||||||
|
tauri::generate_handler![
|
||||||
|
tabs_sync,
|
||||||
|
tab_navigate,
|
||||||
|
tab_action,
|
||||||
|
open_external_url,
|
||||||
|
shell_focus,
|
||||||
|
shell_raise,
|
||||||
|
window_system_menu,
|
||||||
|
profiles_get,
|
||||||
|
profiles_set,
|
||||||
|
i18n_get,
|
||||||
|
i18n_set,
|
||||||
|
tab_shortcut,
|
||||||
|
tab_link_drag
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Setup = Box<dyn FnOnce(&AppHandle) -> Result<(), Box<dyn std::error::Error>> + Send>;
|
||||||
|
|
||||||
|
/// The app builder: config, site hooks, the app's own setup, then [`App::run`].
|
||||||
|
pub struct App {
|
||||||
|
config: AppConfig,
|
||||||
|
hooks: Arc<dyn SiteHooks>,
|
||||||
|
setup: Option<Setup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new(config: AppConfig, hooks: impl SiteHooks) -> Self {
|
||||||
|
Self { config, hooks: Arc::new(hooks), setup: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app's own setup (open its database, `manage` its state), run after the host's (app
|
||||||
|
/// data folder, profiles, language) and before the window exists.
|
||||||
|
pub fn setup(mut self, setup: impl FnOnce(&AppHandle) -> Result<(), Box<dyn std::error::Error>> + Send + 'static) -> Self {
|
||||||
|
self.setup = Some(Box::new(setup));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the app: `context` is `tauri::generate_context!()` and `handler` the app's own
|
||||||
|
/// `tauri::generate_handler![...]` (the host serves [`commands::SHELL`] and [`commands::TAB`]).
|
||||||
|
pub fn run<H>(self, context: tauri::Context<Wry>, handler: H)
|
||||||
|
where
|
||||||
|
H: Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let App { config, hooks, setup } = self;
|
||||||
|
let host_handler = host_handler();
|
||||||
|
let builder = tauri::Builder::<Wry>::default()
|
||||||
|
// Must be first: a second launch focuses the running window instead of fighting over
|
||||||
|
// the app's data and WebView profile (and hands over its link).
|
||||||
|
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||||
|
if let Some(window) = app.get_window(policy::MAIN_LABEL) {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
// Install links (after single-instance, which forwards a second launch's link).
|
||||||
|
let builder = if config.deep_link { builder.plugin(tauri_plugin_deep_link::init()) } else { builder };
|
||||||
|
let builder = builder
|
||||||
|
.plugin(tauri_plugin_window_state::Builder::default().with_state_flags(window_state_flags()).build())
|
||||||
|
.plugin(tauri_plugin_opener::init());
|
||||||
|
// No capability grants the updater/process JS APIs; only Rust drives them.
|
||||||
|
let builder = if config.updater { builder.plugin(tauri_plugin_updater::Builder::new().build()) } else { builder };
|
||||||
|
builder
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.setup(move |tauri_app| {
|
||||||
|
let handle = tauri_app.handle().clone();
|
||||||
|
let site = config.site;
|
||||||
|
if let Some(legacy) = config.legacy_identifier {
|
||||||
|
host::migrate_identifier(&handle, site, legacy);
|
||||||
|
}
|
||||||
|
let directory = tauri_app.path().app_data_dir()?;
|
||||||
|
std::fs::create_dir_all(&directory)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700))?;
|
||||||
|
}
|
||||||
|
let settings = read_json::<ProfileSettings>(&handle, PROFILES_FILE)
|
||||||
|
.and_then(|settings| settings.validated(&site.accounts).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
host::prune_profile_dirs(&handle, &settings);
|
||||||
|
tauri_app.manage(Profiles(Mutex::new(ProfileState { settings, tab_profiles: HashMap::new() })));
|
||||||
|
let strings = read_json::<NativeStrings>(&handle, I18N_FILE).and_then(|strings| strings.validated().ok()).unwrap_or_default();
|
||||||
|
tauri_app.manage(Strings(Mutex::new(strings)));
|
||||||
|
tauri_app.manage(Host { config, hooks });
|
||||||
|
if let Some(setup) = setup {
|
||||||
|
setup(&handle)?;
|
||||||
|
}
|
||||||
|
open_window(tauri_app, &config)
|
||||||
|
})
|
||||||
|
.invoke_handler(move |invoke| {
|
||||||
|
if commands::is_host(invoke.message.command()) {
|
||||||
|
host_handler(invoke)
|
||||||
|
} else {
|
||||||
|
handler(invoke)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run(context)
|
||||||
|
.unwrap_or_else(|error| panic!("Failed to launch {}: {error}", config.title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`App::new`] + [`App::setup`] + [`App::run`] in one call.
|
||||||
|
pub fn run_app<H>(
|
||||||
|
config: AppConfig,
|
||||||
|
hooks: impl SiteHooks,
|
||||||
|
setup: impl FnOnce(&AppHandle) -> Result<(), Box<dyn std::error::Error>> + Send + 'static,
|
||||||
|
context: tauri::Context<Wry>,
|
||||||
|
handler: H,
|
||||||
|
) where
|
||||||
|
H: Fn(tauri::ipc::Invoke<Wry>) -> bool + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
App::new(config, hooks).setup(setup).run(context, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main window (frameless on Windows/Linux, overlay titlebar on macOS) with the shell
|
||||||
|
/// webview filling it, shown once its saved geometry is restored.
|
||||||
|
fn open_window(app: &mut tauri::App<Wry>, config: &AppConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let (r, g, b) = config.background;
|
||||||
|
let builder = tauri::window::WindowBuilder::new(app, policy::MAIN_LABEL)
|
||||||
|
.title(config.title)
|
||||||
|
.inner_size(config.window_size.0, config.window_size.1)
|
||||||
|
// The titlebar needs room for the logo, a few tabs and the caption buttons.
|
||||||
|
.min_inner_size(config.min_window_size.0, config.min_window_size.1)
|
||||||
|
// The shell draws its own titlebar (Windows 11: DWM shadow and rounded corners).
|
||||||
|
.decorations(!config.custom_titlebar)
|
||||||
|
.shadow(true)
|
||||||
|
// No white flash before the shell paints.
|
||||||
|
.background_color(tauri::window::Color(r, g, b, 255))
|
||||||
|
.resizable(true)
|
||||||
|
// Shown after the saved geometry is restored.
|
||||||
|
.visible(false);
|
||||||
|
// macOS keeps the native frame with an overlay titlebar and traffic lights.
|
||||||
|
// (`traffic_light_position` is not on the multiwebview WindowBuilder in tauri 2.12.)
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let builder = if config.custom_titlebar {
|
||||||
|
builder.decorations(true).title_bar_style(tauri::TitleBarStyle::Overlay).hidden_title(true)
|
||||||
|
} else {
|
||||||
|
builder
|
||||||
|
};
|
||||||
|
let window = builder.build()?;
|
||||||
|
{
|
||||||
|
use tauri_plugin_window_state::WindowExt;
|
||||||
|
// Never DECORATIONS: a state file from a framed build would re-enable the OS frame.
|
||||||
|
let _ = window.restore_state(window_state_flags());
|
||||||
|
}
|
||||||
|
reset_lost_geometry(&window, config.window_size, config.min_window_size);
|
||||||
|
// `taskkill` (and anything else closing "the first visible window") must close the app
|
||||||
|
// even when a minimized window sits below the runtime's helper windows.
|
||||||
|
host::route_helper_closes(&window);
|
||||||
|
// Tab webviews follow window resizes natively, before the shell lays out again.
|
||||||
|
host::follow_window_resizes(&window);
|
||||||
|
let size = window.inner_size()?.to_logical::<f64>(window.scale_factor()?);
|
||||||
|
// The shell fills the window; tab webviews are layered over its pane rectangles.
|
||||||
|
let shell = WebviewBuilder::new(policy::SHELL_LABEL, WebviewUrl::App("index.html".into()))
|
||||||
|
.auto_resize()
|
||||||
|
// HTML5 drag and drop (tab dragging, split drop zones) instead of OS file drops.
|
||||||
|
.disable_drag_drop_handler()
|
||||||
|
.on_navigation(|url| policy::local_navigation_allowed(url, cfg!(debug_assertions)))
|
||||||
|
.on_new_window(|_, _| NewWindowResponse::Deny);
|
||||||
|
// Raised over the tabs for an overlay (`shell_raise`), the page is see-through where the
|
||||||
|
// panes are; otherwise its own background paints as before. (macOS: `macos-private-api`.)
|
||||||
|
#[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))]
|
||||||
|
let shell = shell.transparent(true);
|
||||||
|
let shell = window.add_child(shell, LogicalPosition::new(0.0, 0.0), LogicalSize::new(size.width, size.height))?;
|
||||||
|
host::attach_shell(&shell);
|
||||||
|
window.show()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
73
crates/waybetter-host/src/commands.rs
Normal file
73
crates/waybetter-host/src/commands.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
//! The Tauri commands [`crate::app::run_app`] registers for every app, and who may call them.
|
||||||
|
//!
|
||||||
|
//! An app's `build.rs` lists its own commands plus these in the app-command manifest
|
||||||
|
//! ([`manifest`]), and its capability files grant [`SHELL`] to the local `shell` webview only and
|
||||||
|
//! [`TAB`] to tab webviews only; [`crate::testkit`] checks both. Every command re-checks its caller
|
||||||
|
//! natively (`host::shell_only` / `host::tab_only`) before anything else.
|
||||||
|
|
||||||
|
/// Shell-only: the local `shell` webview (debug builds also accept the dev server).
|
||||||
|
pub const SHELL: &[&str] = &[
|
||||||
|
"tabs_sync",
|
||||||
|
"tab_navigate",
|
||||||
|
"tab_action",
|
||||||
|
"open_external_url",
|
||||||
|
"shell_focus",
|
||||||
|
"shell_raise",
|
||||||
|
"window_system_menu",
|
||||||
|
"profiles_get",
|
||||||
|
"profiles_set",
|
||||||
|
"i18n_get",
|
||||||
|
"i18n_set",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Tab-only: any tab webview, speaking only about itself.
|
||||||
|
pub const TAB: &[&str] = &["tab_shortcut", "tab_link_drag"];
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The capability permission tauri-build generates for `command` (`tabs_sync` → `allow-tabs-sync`).
|
||||||
|
pub fn permission(command: &str) -> String {
|
||||||
|
format!("allow-{}", command.replace('_', "-"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app-command manifest for `build.rs`: the app's own commands followed by the host's (each
|
||||||
|
/// once). Leaked on purpose: tauri-build wants `&'static` and a build script runs once.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// 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());
|
||||||
|
for command in app.iter().chain(SHELL).chain(TAB) {
|
||||||
|
if !all.contains(command) {
|
||||||
|
all.push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box::leak(all.into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn manifest_adds_the_host_commands_once() {
|
||||||
|
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) {
|
||||||
|
assert!(all.contains(command), "{command}");
|
||||||
|
assert!(is_host(command));
|
||||||
|
}
|
||||||
|
assert!(!is_host("draft_list"));
|
||||||
|
assert_eq!(permission("tab_link_drag"), "allow-tab-link-drag");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_and_tab_commands_do_not_overlap() {
|
||||||
|
assert!(!SHELL.iter().any(|command| TAB.contains(command)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -154,6 +154,14 @@ impl NativeStrings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The script that hands a site tab its strings (`payload`, see [`NativeStrings::page_payload`]):
|
||||||
|
/// delivered through the page runtime (queued in `__wbI18n` until it exists), and only on the
|
||||||
|
/// site's primary origin: other sites' tabs run the same document-start scripts and must not get them.
|
||||||
|
pub fn page_script(site: &crate::site::SiteSpec, payload: &serde_json::Value) -> String {
|
||||||
|
let script = crate::tabs::deliver_script(site.runtime_global, "i18n", "set", "__wbI18n", &payload.to_string());
|
||||||
|
format!("if(location.origin==='{}'){script}", site.primary_origin())
|
||||||
|
}
|
||||||
|
|
||||||
/// The OS language preferences, most preferred first (`tr-TR`, `en-US`), as valid tags.
|
/// The OS language preferences, most preferred first (`tr-TR`, `en-US`), as valid tags.
|
||||||
pub fn system_locales() -> Vec<String> {
|
pub fn system_locales() -> Vec<String> {
|
||||||
sys_locale::get_locales()
|
sys_locale::get_locales()
|
||||||
|
|
@ -169,6 +177,14 @@ pub fn system_locales() -> Vec<String> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn page_script_is_guarded_to_the_site_origin() {
|
||||||
|
let payload = strings(&[("x.fab.title", "wayBetter"), ("native.notify.title", "Hi")]).page_payload(&["x."]);
|
||||||
|
let script = page_script(&crate::site::testing::SITE, &payload);
|
||||||
|
assert!(script.starts_with("if(location.origin==='https://x.com')("), "{script}");
|
||||||
|
assert!(script.contains("x.fab.title") && !script.contains("native.notify.title"), "{script}");
|
||||||
|
}
|
||||||
|
|
||||||
fn strings(messages: &[(&str, &str)]) -> NativeStrings {
|
fn strings(messages: &[(&str, &str)]) -> NativeStrings {
|
||||||
NativeStrings {
|
NativeStrings {
|
||||||
locale: "tr".into(),
|
locale: "tr".into(),
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,18 @@
|
||||||
//! signed multi-app wayBetter Store (allow-list, caps, catalog signatures, staging, install records);
|
//! signed multi-app wayBetter Store (allow-list, caps, catalog signatures, staging, install records);
|
||||||
//! downloads with the `net` feature.
|
//! 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
|
//! 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.
|
//! `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; the app adds its own.
|
||||||
|
|
||||||
pub mod alerts;
|
pub mod alerts;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod bridge;
|
pub mod bridge;
|
||||||
|
pub mod commands;
|
||||||
pub mod devmode;
|
pub mod devmode;
|
||||||
#[cfg(feature = "net")]
|
#[cfg(feature = "net")]
|
||||||
pub mod fonts;
|
pub mod fonts;
|
||||||
|
|
@ -50,9 +56,12 @@ pub mod site;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod tabs;
|
pub mod tabs;
|
||||||
|
pub mod testkit;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
|
#[cfg(feature = "app")]
|
||||||
|
pub mod app;
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
pub mod host;
|
pub mod host;
|
||||||
#[cfg(all(feature = "tauri", feature = "net"))]
|
#[cfg(all(feature = "tauri", feature = "net"))]
|
||||||
|
|
|
||||||
239
crates/waybetter-host/src/testkit.rs
Normal file
239
crates/waybetter-host/src/testkit.rs
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
//! Security assertions for an app's own tests (`tests/security.rs`): who may call which command,
|
||||||
|
//! read from the app's capability files, and whether a command checks its caller first.
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! use waybetter_host::testkit::Capabilities;
|
||||||
|
//! let caps = Capabilities::from_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/capabilities"));
|
||||||
|
//! caps.assert_host_commands(); // the host's shell-only / tab-only commands
|
||||||
|
//! caps.assert_shell_only("draft_list"); // the app's own
|
||||||
|
//! caps.assert_tab_only("tab_report");
|
||||||
|
//! waybetter_host::testkit::assert_guarded(include_str!("../src/desktop.rs"), "draft_list", "shell_only(&webview)?");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! A capability counts as the shell's when its webviews are exactly `["shell"]`, it is `local` and
|
||||||
|
//! has no `remote`; as a tab's when its webviews are exactly `["tab-*"]` and it is not `local`.
|
||||||
|
//! Anything else granting a command fails both checks.
|
||||||
|
|
||||||
|
use crate::commands;
|
||||||
|
use crate::policy::SHELL_LABEL;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// One capability file: its name (for messages) and its JSON.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Capability {
|
||||||
|
pub file: String,
|
||||||
|
pub json: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Capability {
|
||||||
|
pub fn identifier(&self) -> &str {
|
||||||
|
self.json["identifier"].as_str().unwrap_or(&self.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The permissions it grants, as strings (`allow-tabs-sync`, `core:event:default`, ...).
|
||||||
|
pub fn permissions(&self) -> Vec<String> {
|
||||||
|
self.json["permissions"]
|
||||||
|
.as_array()
|
||||||
|
.map(|list| list.iter().filter_map(|p| p.as_str().map(str::to_owned)).collect())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn webviews(&self) -> Vec<&str> {
|
||||||
|
self.json["webviews"].as_array().map(|list| list.iter().filter_map(Value::as_str).collect()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local `shell` webview and nothing else.
|
||||||
|
pub fn is_shell(&self) -> bool {
|
||||||
|
self.webviews() == [SHELL_LABEL] && self.json["local"] == Value::Bool(true) && self.json.get("remote").is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tab webviews (`tab-*`) and nothing else, never local pages.
|
||||||
|
pub fn is_tab(&self) -> bool {
|
||||||
|
self.webviews() == ["tab-*"] && self.json["local"] == Value::Bool(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An app's capability files.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Capabilities(pub Vec<Capability>);
|
||||||
|
|
||||||
|
impl Capabilities {
|
||||||
|
/// Every `*.json` in `dir` (the app's `capabilities` folder), so a new file is checked too.
|
||||||
|
pub fn from_dir(dir: impl AsRef<Path>) -> Self {
|
||||||
|
let dir = dir.as_ref();
|
||||||
|
let mut files: Vec<_> = std::fs::read_dir(dir)
|
||||||
|
.unwrap_or_else(|error| panic!("capabilities folder {}: {error}", dir.display()))
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
|
||||||
|
.collect();
|
||||||
|
files.sort();
|
||||||
|
assert!(!files.is_empty(), "no capability files in {}", dir.display());
|
||||||
|
Self(
|
||||||
|
files
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let text = std::fs::read_to_string(&path).unwrap_or_else(|error| panic!("{}: {error}", path.display()));
|
||||||
|
let file = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
|
||||||
|
Self::parse(file, &text)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capability files given as `(name, json)` (e.g. from `include_str!`).
|
||||||
|
pub fn from_sources(sources: &[(&str, &str)]) -> Self {
|
||||||
|
Self(sources.iter().map(|(file, text)| Self::parse((*file).to_owned(), text)).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(file: String, text: &str) -> Capability {
|
||||||
|
let json = serde_json::from_str(text).unwrap_or_else(|error| panic!("{file}: invalid JSON: {error}"));
|
||||||
|
Capability { file, json }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The capability with this identifier.
|
||||||
|
pub fn get(&self, identifier: &str) -> &Capability {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find(|cap| cap.identifier() == identifier)
|
||||||
|
.unwrap_or_else(|| panic!("no capability {identifier:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The capabilities granting `permission`.
|
||||||
|
pub fn granting(&self, permission: &str) -> Vec<&Capability> {
|
||||||
|
self.0.iter().filter(|cap| cap.permissions().iter().any(|p| p == permission)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `command` is granted, and only to the local shell webview.
|
||||||
|
pub fn assert_shell_only(&self, command: &str) {
|
||||||
|
let permission = commands::permission(command);
|
||||||
|
let granting = self.granting(&permission);
|
||||||
|
assert!(granting.iter().any(|cap| cap.is_shell()), "the shell lacks {permission}");
|
||||||
|
for cap in granting {
|
||||||
|
assert!(cap.is_shell(), "{permission} is shell-only, but capability {:?} ({}) grants it", cap.identifier(), cap.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `command` is granted, and only to tab webviews (never the shell or another local page).
|
||||||
|
pub fn assert_tab_only(&self, command: &str) {
|
||||||
|
let permission = commands::permission(command);
|
||||||
|
let granting = self.granting(&permission);
|
||||||
|
assert!(!granting.is_empty(), "no tab capability grants {permission}");
|
||||||
|
for cap in granting {
|
||||||
|
assert!(cap.is_tab(), "{permission} is tab-only, but capability {:?} ({}) grants it", cap.identifier(), cap.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `command` is granted to no capability at all.
|
||||||
|
pub fn assert_not_granted(&self, command: &str) {
|
||||||
|
let permission = commands::permission(command);
|
||||||
|
let granting: Vec<_> = self.granting(&permission).iter().map(|cap| cap.identifier().to_owned()).collect();
|
||||||
|
assert!(granting.is_empty(), "{permission} is granted by {granting:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host's commands ([`commands::SHELL`], [`commands::TAB`]) keep their callers.
|
||||||
|
pub fn assert_host_commands(&self) {
|
||||||
|
for command in commands::SHELL {
|
||||||
|
self.assert_shell_only(command);
|
||||||
|
}
|
||||||
|
for command in commands::TAB {
|
||||||
|
self.assert_tab_only(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No capability exposes these Tauri plugins' JS APIs (`updater`, `process`, ...): only Rust
|
||||||
|
/// drives them.
|
||||||
|
pub fn assert_no_plugin_apis(&self, plugins: &[&str]) {
|
||||||
|
for cap in &self.0 {
|
||||||
|
for permission in cap.permissions() {
|
||||||
|
assert!(
|
||||||
|
!plugins.iter().any(|plugin| permission.starts_with(&format!("{plugin}:"))),
|
||||||
|
"capability {:?} exposes a plugin API: {permission}",
|
||||||
|
cap.identifier()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first statement of function `function` in `source` contains `guard` (e.g.
|
||||||
|
/// `shell_only(&webview)?`): the command checks its caller before it does anything else.
|
||||||
|
pub fn assert_guarded(source: &str, function: &str, guard: &str) {
|
||||||
|
let at = source
|
||||||
|
.find(&format!("fn {function}("))
|
||||||
|
.unwrap_or_else(|| panic!("no function {function} in the given source"));
|
||||||
|
let body = &source[at..];
|
||||||
|
let body = &body[body.find('{').unwrap_or_else(|| panic!("{function} has no body")) + 1..];
|
||||||
|
let first = &body[..body.find(';').unwrap_or(body.len())];
|
||||||
|
assert!(first.contains(guard), "{function} must check `{guard}` first, starts with: {}", first.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host's own command wrappers check their caller first (shell-only: `shell_only`, tab-only:
|
||||||
|
/// `tab_only`). Runs against the host's source, so an app's tests re-check the version it builds with.
|
||||||
|
pub fn assert_host_guards() {
|
||||||
|
let source = include_str!("app.rs");
|
||||||
|
for command in commands::SHELL {
|
||||||
|
assert_guarded(source, command, "shell_only(&webview)?");
|
||||||
|
}
|
||||||
|
for command in commands::TAB {
|
||||||
|
assert_guarded(source, command, "tab_only(&webview)?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn caps(shell_extra: &str, tab_extra: &str) -> Capabilities {
|
||||||
|
let host_shell: Vec<String> = commands::SHELL.iter().map(|c| format!("\"{}\"", commands::permission(c))).collect();
|
||||||
|
let host_tab: Vec<String> = commands::TAB.iter().map(|c| format!("\"{}\"", commands::permission(c))).collect();
|
||||||
|
let shell = format!(
|
||||||
|
r#"{{"identifier":"shell","webviews":["shell"],"local":true,"permissions":["core:event:default",{}{shell_extra}]}}"#,
|
||||||
|
host_shell.join(",")
|
||||||
|
);
|
||||||
|
let tab = format!(
|
||||||
|
r#"{{"identifier":"tab-chrome","webviews":["tab-*"],"local":false,"remote":{{"urls":["https://*/*"]}},"permissions":[{}{tab_extra}]}}"#,
|
||||||
|
host_tab.join(",")
|
||||||
|
);
|
||||||
|
Capabilities::from_sources(&[("shell.json", &shell), ("tab-chrome.json", &tab)])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_commands_pass_when_granted_to_their_callers() {
|
||||||
|
let caps = caps("", "");
|
||||||
|
caps.assert_host_commands();
|
||||||
|
caps.assert_not_granted("draft_list");
|
||||||
|
caps.assert_no_plugin_apis(&["updater", "process"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "shell-only")]
|
||||||
|
fn a_shell_command_granted_to_tabs_fails() {
|
||||||
|
caps("", r#","allow-tabs-sync""#).assert_host_commands();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "tab-only")]
|
||||||
|
fn a_tab_command_granted_to_the_shell_fails() {
|
||||||
|
caps(r#","allow-tab-shortcut""#, "").assert_host_commands();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "plugin API")]
|
||||||
|
fn plugin_apis_are_refused() {
|
||||||
|
caps(r#","updater:default""#, "").assert_no_plugin_apis(&["updater"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guards_are_read_from_the_first_statement() {
|
||||||
|
let source = "async fn a(webview: Webview) -> Result<(), String> {\n shell_only(&webview)?;\n Ok(())\n}\nfn b(webview: Webview) { let x = 1; shell_only(&webview)?; }";
|
||||||
|
assert_guarded(source, "a", "shell_only(&webview)?");
|
||||||
|
assert!(std::panic::catch_unwind(|| assert_guarded(source, "b", "shell_only(&webview)?")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_host_commands_check_their_callers_first() {
|
||||||
|
assert_host_guards();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"test:create": "node --test packages/create-waybetter/tests/*.test.mjs",
|
"test:create": "node --test packages/create-waybetter/tests/*.test.mjs",
|
||||||
"test:cli": "node --test packages/waybetter/tests/*.test.mjs",
|
"test:cli": "node --test packages/waybetter/tests/*.test.mjs",
|
||||||
"test:rust": "cargo test --manifest-path crates/waybetter-host/Cargo.toml --locked --features net",
|
"test:rust": "cargo test --manifest-path crates/waybetter-host/Cargo.toml --locked --features net",
|
||||||
"check:tauri": "cargo check --manifest-path crates/waybetter-host/Cargo.toml --features tauri,net",
|
"check:tauri": "cargo check --manifest-path crates/waybetter-host/Cargo.toml --features app,net",
|
||||||
"build": "bun run typecheck && cargo build --manifest-path crates/waybetter-host/Cargo.toml --locked",
|
"build": "bun run typecheck && cargo build --manifest-path crates/waybetter-host/Cargo.toml --locked",
|
||||||
"build:packages": "cd packages/waybetter && npm run build",
|
"build:packages": "cd packages/waybetter && npm run build",
|
||||||
"pack:check": "bun run build:packages && npm pack ./packages/waybetter --dry-run && npm pack ./packages/create-waybetter --dry-run",
|
"pack:check": "bun run build:packages && npm pack ./packages/waybetter --dry-run && npm pack ./packages/create-waybetter --dry-run",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue