ruby_everywhere 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +99 -3
  3. data/bridge/README.md +70 -1
  4. data/bridge/everywhere/bridge.js +151 -0
  5. data/bridge/everywhere/native.css +61 -0
  6. data/lib/everywhere/agents_guide.rb +57 -0
  7. data/lib/everywhere/auth_handoff.rb +8 -2
  8. data/lib/everywhere/builders/desktop.rb +326 -0
  9. data/lib/everywhere/child_processes.rb +74 -0
  10. data/lib/everywhere/commands/build.rb +44 -5
  11. data/lib/everywhere/commands/dev.rb +426 -59
  12. data/lib/everywhere/commands/install.rb +93 -1
  13. data/lib/everywhere/commands/release.rb +2 -2
  14. data/lib/everywhere/config.rb +298 -4
  15. data/lib/everywhere/console.rb +117 -0
  16. data/lib/everywhere/desktop_assets.rb +150 -0
  17. data/lib/everywhere/dock/footer.rb +150 -0
  18. data/lib/everywhere/dock/screen.rb +114 -0
  19. data/lib/everywhere/dock/state.rb +59 -0
  20. data/lib/everywhere/dock.rb +238 -0
  21. data/lib/everywhere/emulator.rb +2 -2
  22. data/lib/everywhere/fatal.rb +20 -0
  23. data/lib/everywhere/line_pump.rb +89 -0
  24. data/lib/everywhere/log_filter.rb +37 -0
  25. data/lib/everywhere/native_helper.rb +38 -5
  26. data/lib/everywhere/paths.rb +62 -11
  27. data/lib/everywhere/relay.rb +77 -0
  28. data/lib/everywhere/shellout.rb +90 -15
  29. data/lib/everywhere/task_pool.rb +123 -0
  30. data/lib/everywhere/ui.rb +54 -11
  31. data/lib/everywhere/version.rb +1 -1
  32. data/support/agents/AGENTS.md +90 -0
  33. data/support/agents/frameworks/hanami-views.md +26 -0
  34. data/support/agents/frameworks/hanami.md +10 -0
  35. data/support/agents/frameworks/rails-views.md +98 -0
  36. data/support/agents/frameworks/rails.md +16 -0
  37. data/support/agents/frameworks/sinatra-views.md +24 -0
  38. data/support/agents/frameworks/sinatra.md +9 -0
  39. data/support/agents/modes/local.md +12 -0
  40. data/support/agents/modes/remote.md +14 -0
  41. data/support/desktop/README.md +121 -0
  42. data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
  43. data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
  44. data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
  45. data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
  46. data/support/desktop/src-tauri/src/extension_host.rs +53 -0
  47. data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
  48. data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
  49. data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
  50. data/support/{macos → release/macos}/notarize.sh +2 -2
  51. metadata +55 -22
  52. data/support/github/build.yml +0 -85
  53. data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
  54. /data/support/{shell → desktop}/splash/index.html +0 -0
  55. /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
  56. /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
  57. /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
  58. /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
  59. /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
  60. /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
  61. /data/support/{macos → release/macos}/entitlements.plist +0 -0
@@ -0,0 +1,24 @@
1
+ ### Native chrome from your views
2
+
3
+ The ERB helpers (`native_app?`, `everywhere_nav_button`, `everywhere_fab`, …) ship with the
4
+ Rails engine, so they aren't available here. The markup contract behind them is public,
5
+ though, and the shell watches for it on every Turbo visit — write it directly:
6
+
7
+ - `data-everywhere-nav-button` (+ `-nav-title`, `-nav-icon`, `-nav-icon-ios`,
8
+ `-nav-icon-android`, `-nav-style`, `side`) — lifts a link or submit button into the top
9
+ navigation bar. Tapping the native control clicks your element.
10
+ - `data-everywhere-nav-menu` with `data-everywhere-menu-item` children (+ `-menu-title`,
11
+ `-menu-style`) — a native pull-down menu.
12
+ - `data-everywhere-menu` + `-menu-trigger` + `-menu-items` — an in-content action sheet.
13
+ - `data-everywhere-haptic` — a tap haptic.
14
+ - `data-everywhere-biometric-lock` (+ `-content`, `-locked`, `-unlock`, `-reason`,
15
+ `-passcode`) — the Face ID / Touch ID gate.
16
+ - `<meta name="everywhere:badge">` and `<meta name="everywhere:tab-badge">` (JSON
17
+ `{path, count}`) — app-icon and tab badges, CSP-safe and correct on first paint.
18
+
19
+ Detect the shell server-side from the User-Agent: the shells prepend
20
+ `RubyEverywhere/<version> (<os>)` to Hotwire Native's own marker. Client-side, use
21
+ `Everywhere.platform` / `Everywhere.native`.
22
+
23
+ `public/native.css` carries the styling helpers, including `.everywhere-titlebar-inset` for
24
+ `window.title_bar: overlay`.
@@ -0,0 +1,9 @@
1
+ - `native_boot.rb` — the packaged-app entry point. Keep it thin and keep it at the app root.
2
+ - `public/bridge.js` and `public/native.css` — the bridge, vendored and refreshed on each
3
+ `every install`. Load it with `<script type="module" src="/bridge.js"></script>`.
4
+
5
+ Rack apps vary too much for the CLI to auto-edit their boot files, so the rest is by hand and
6
+ `every install` prints it rather than guessing: point the production SQLite path at
7
+ `ENV.fetch("NATIVE_STORAGE_DIR", "storage")`, and call
8
+ `Everywhere::Database.prepare!(__dir__)` from `config.ru` after loading the app. Both only
9
+ matter for on-device packaging.
@@ -0,0 +1,12 @@
1
+ The app itself is compiled into the shipped binary and runs on the user's machine, rather than
2
+ the shells pointing at a deployed site. Shipping a change means cutting a new build and
3
+ release.
4
+
5
+ That trade brings constraints the docs cover properly — a read-only application filesystem
6
+ with a separate writable data directory, SQLite only, desktop only — so read
7
+ <https://rubyeverywhere.com/docs> before changing anything that writes to disk, boots the app,
8
+ or touches `native_boot.rb`. The short version: anything written at runtime belongs under
9
+ `ENV.fetch("NATIVE_STORAGE_DIR", "storage")`, and `ENV["NATIVE_PACKAGED"]` is the guard for
10
+ packaged-only behavior.
11
+
12
+ Everything below applies the same way in either mode.
@@ -0,0 +1,14 @@
1
+ The native shells are a thin wrapper around the deployed site at `remote.url` in
2
+ `config/everywhere.yml`. Nothing about the app is compiled into them.
3
+
4
+ **So you are working on an ordinary web app.** Ship a change by deploying as you always have,
5
+ and users see it on their next launch — no rebuild, no app-store review. Normal databases,
6
+ background jobs, and hosting all apply, unchanged.
7
+
8
+ Only three things are native-specific, and they're all covered below: what
9
+ `config/everywhere.yml` declares, the view markup that becomes native chrome, and the
10
+ `Everywhere` JavaScript API. Of those, tab and path-rule changes deploy with the app; the rest
11
+ of `config/everywhere.yml` needs a new build of the shells.
12
+
13
+ Remote is also the only mode mobile supports — iOS and Android are always a shell around a
14
+ deployed app.
@@ -0,0 +1,121 @@
1
+ # The RubyEverywhere desktop shell
2
+
3
+ A [Tauri](https://tauri.app) v2 app that boots a packaged Rails binary as a
4
+ sidecar and shows it in a webview — or, in remote mode, wraps an already-deployed
5
+ app. This directory is the canonical copy; the CLI resolves it with
6
+ `Everywhere::Paths.shell_dir` and external consumers with `every shell-dir`.
7
+
8
+ ```
9
+ src-tauri/
10
+ src/main.rs boot, window, menu, tray, config, assets
11
+ src/updater.rs signed self-updates
12
+ src/extension_host.rs the event bridge extension commands answer on
13
+ src/extensions/mod.rs GENERATED — the app's Rust registry
14
+ Cargo.toml carries the __EVERYWHERE_APP_CRATES__ marker
15
+ tauri.conf.json near-empty on purpose (see below)
16
+ capabilities/default.json
17
+ splash/index.html frontendDist — shown while the sidecar boots
18
+ ```
19
+
20
+ ## One binary, every app
21
+
22
+ Unlike the iOS and Android templates, this shell is **not** stamped per app by
23
+ default. One crate serves every RubyEverywhere app, and everything per-app
24
+ arrives at runtime as JSON — `NATIVE_CONFIG` under `every dev`,
25
+ `Contents/Resources/everywhere.json` inside a packaged `.app`.
26
+
27
+ That's why `tauri.conf.json` declares `"windows": []` and `"bundle": {"active":
28
+ false}`: window properties are set at runtime in `build_window`, and the `.app`
29
+ is assembled by hand in `Commands::Build#bundle_app` rather than by `tauri-cli`.
30
+ `productName` and `identifier` in that file never reach a built app: an app's
31
+ name comes from `CFBundleName` in the hand-written plist (and, under `every dev`,
32
+ from the throwaway `.app` wrapper `Commands::Dev#stage_dev_bundle` assembles),
33
+ and its bundle id from `everywhere.yml`.
34
+
35
+ They do still name things on disk, so they aren't free to change. `identifier`
36
+ keys Tauri's own app-config directory, which is where `tauri-plugin-window-state`
37
+ writes each app's saved geometry. Everything an app would actually miss is keyed
38
+ by its own `bundle_id` under the plain OS data dir instead — app data (see the
39
+ comment above the `app_data` binding in `main.rs`), the updater's channel choice,
40
+ and the Windows/Linux webview profile — so changing `identifier` costs one
41
+ forgotten window size per app and nothing else.
42
+
43
+ The payoff is that `cargo` compiles this once per machine, against the shared
44
+ `~/.rubyeverywhere/shell-target`, and stays warm across every project.
45
+
46
+ ## The freeze contract
47
+
48
+ An app that declares `native.desktop` in `config/everywhere.yml` **does** get a
49
+ stamped copy, under `~/.rubyeverywhere/desktop/<bundle_id>/`, with its own
50
+ target dir. `Builders::Desktop` writes exactly these files there and nothing
51
+ else:
52
+
53
+ 1. `src-tauri/Cargo.toml` — the template's, with `native.desktop.crates`
54
+ substituted into the `# __EVERYWHERE_APP_CRATES__` marker. Cargo has no
55
+ include mechanism, so unlike Android's `native-packages.gradle.kts` the crate
56
+ list cannot live in its own file.
57
+ 2. `src-tauri/src/extensions/**` — the app's `native/desktop/**/*.rs`, copied
58
+ preserving relative paths, plus a generated `mod.rs` in every directory.
59
+
60
+ Everything else is the frozen template, copied as-is. In particular
61
+ `src/main.rs`, `src/extension_host.rs`, `capabilities/default.json` and
62
+ `tauri.conf.json` are **never** rewritten — if a feature seems to need editing
63
+ one of them per app, it belongs in the runtime config instead.
64
+
65
+ `mod.rs` is reserved at every level: the builder refuses to run if the app ships
66
+ one, rather than silently overwriting it.
67
+
68
+ The stamped dir is merged into, not wiped, so cargo's incremental state
69
+ survives — except on a gem version change, where the marker `.everywhere-template`
70
+ mismatches and the whole thing is rebuilt.
71
+
72
+ ### The three seams in main.rs
73
+
74
+ Adding an extension point means touching `main.rs`, which the contract above
75
+ forbids per app. These are the ones that exist, and they should stay the only
76
+ ones:
77
+
78
+ - `mod extensions;` / `mod extension_host;`
79
+ - `.invoke_handler(extensions::handler())`
80
+ - `extensions::setup(app)?` in the Tauri setup closure
81
+
82
+ The checked-in `src/extensions/mod.rs` registers nothing, so the bare template
83
+ compiles green on its own.
84
+
85
+ ## Why extensions use events, not `invoke`
86
+
87
+ Tauri v2 refuses IPC `invoke` from any `http` origin — and a RubyEverywhere page
88
+ is *always* http, whether that's `127.0.0.1` in local mode or your own domain in
89
+ remote mode. Granting the origin in `capabilities/default.json` doesn't change
90
+ it; a registered command still fails with `not allowed. Plugin not found`.
91
+
92
+ So extension commands are plain functions with a uniform signature:
93
+
94
+ ```rust
95
+ pub fn scan_ports(app: &tauri::AppHandle, payload: serde_json::Value)
96
+ -> Result<serde_json::Value, String>
97
+ ```
98
+
99
+ reached over an event pair (`everywhere:command` →
100
+ `everywhere:command-result`), which is the same path `Everywhere.notify` has
101
+ always used. `extension_host.rs` owns that protocol; the generated
102
+ `extensions/mod.rs` only supplies the `match` over command names.
103
+
104
+ ## Working on the shell
105
+
106
+ ```sh
107
+ # run it against any URL, no CLI needed
108
+ NATIVE_DEV_URL=http://localhost:3000 \
109
+ NATIVE_CONFIG='{"name":"Test","bundle_id":"com.example.test"}' \
110
+ CARGO_TARGET_DIR=~/.rubyeverywhere/shell-target cargo run
111
+
112
+ # or point the CLI at a modified checkout
113
+ every dev --shell-dir /path/to/src-tauri
114
+ ```
115
+
116
+ Other env vars `main.rs` reads: `NATIVE_ASSETS_DIR` (staged
117
+ `native/desktop/assets`), `NATIVE_NAVIGATE_URL` (splash→navigate flow without a
118
+ sidecar), `NATIVE_SIDECAR_PATH`.
119
+
120
+ `--shell-dir` points at the `src-tauri` directory; the builder stages its parent,
121
+ because `frontendDist` is `../splash`.
@@ -995,6 +995,7 @@ version = "0.2.0"
995
995
  dependencies = [
996
996
  "minisign-verify",
997
997
  "objc2",
998
+ "objc2-app-kit",
998
999
  "objc2-foundation",
999
1000
  "objc2-web-kit",
1000
1001
  "semver",
@@ -2258,10 +2259,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
2258
2259
  dependencies = [
2259
2260
  "bitflags 2.13.0",
2260
2261
  "block2",
2262
+ "libc",
2261
2263
  "objc2",
2264
+ "objc2-cloud-kit",
2265
+ "objc2-core-data",
2262
2266
  "objc2-core-foundation",
2263
2267
  "objc2-core-graphics",
2268
+ "objc2-core-image",
2269
+ "objc2-core-text",
2270
+ "objc2-core-video",
2264
2271
  "objc2-foundation",
2272
+ "objc2-quartz-core",
2265
2273
  ]
2266
2274
 
2267
2275
  [[package]]
@@ -2281,6 +2289,7 @@ version = "0.3.2"
2281
2289
  source = "registry+https://github.com/rust-lang/crates.io-index"
2282
2290
  checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
2283
2291
  dependencies = [
2292
+ "bitflags 2.13.0",
2284
2293
  "objc2",
2285
2294
  "objc2-foundation",
2286
2295
  ]
@@ -2341,6 +2350,19 @@ dependencies = [
2341
2350
  "objc2-core-graphics",
2342
2351
  ]
2343
2352
 
2353
+ [[package]]
2354
+ name = "objc2-core-video"
2355
+ version = "0.3.2"
2356
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2357
+ checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
2358
+ dependencies = [
2359
+ "bitflags 2.13.0",
2360
+ "objc2",
2361
+ "objc2-core-foundation",
2362
+ "objc2-core-graphics",
2363
+ "objc2-io-surface",
2364
+ ]
2365
+
2344
2366
  [[package]]
2345
2367
  name = "objc2-encode"
2346
2368
  version = "4.1.0"
@@ -3624,6 +3646,7 @@ dependencies = [
3624
3646
  "gtk",
3625
3647
  "heck 0.5.0",
3626
3648
  "http",
3649
+ "image",
3627
3650
  "jni",
3628
3651
  "libc",
3629
3652
  "log",
@@ -8,7 +8,13 @@ edition = "2021"
8
8
  tauri-build = { version = "2", features = [] }
9
9
 
10
10
  [dependencies]
11
- tauri = { version = "2", features = ["devtools", "webview-data-url", "tray-icon"] }
11
+ # image-png / image-ico back Image::from_path, which loads the app's `tray`
12
+ # asset (native/desktop/assets) for the menu bar.
13
+ #
14
+ # Deliberately NOT here: macos-private-api. It's what a transparent macOS window
15
+ # needs, and Apple has rejected Mac App Store submissions over it — not a tax
16
+ # worth charging every app for a look none of them have asked for yet.
17
+ tauri = { version = "2", features = ["devtools", "webview-data-url", "tray-icon", "image-png", "image-ico"] }
12
18
  serde_json = "1"
13
19
  tauri-plugin-notification = "2"
14
20
  tauri-plugin-dialog = "2"
@@ -21,6 +27,14 @@ semver = "1"
21
27
  sha2 = "0.10"
22
28
  minisign-verify = "0.2"
23
29
 
30
+ # Crates the app declares under `native.desktop.crates` are substituted in here
31
+ # when the shell is stamped per app. Cargo has no include/apply-from mechanism —
32
+ # unlike Android's native-packages.gradle.kts, the list can't be its own file —
33
+ # so the CLI rewrites this marker in the STAMPED copy under ~/.rubyeverywhere.
34
+ # The gem's own Cargo.toml is never edited. Keep this line last in [dependencies]
35
+ # (the macOS target table below has to stay a separate section).
36
+ # __EVERYWHERE_APP_CRATES__
37
+
24
38
  # macOS: set the WebView's `applicationNameForUserAgent`, which WKWebView
25
39
  # natively APPENDS to its real default UA — so we get the true system UA plus
26
40
  # our marker without ever reading or hardcoding the base string. Versions are
@@ -29,6 +43,10 @@ minisign-verify = "0.2"
29
43
  objc2 = "0.6"
30
44
  objc2-foundation = { version = "0.3", features = ["NSString", "NSUUID", "NSProcessInfo"] }
31
45
  objc2-web-kit = { version = "0.3", features = ["WKWebViewConfiguration", "WKWebsiteDataStore"] }
46
+ # Dock icon + menu-bar app name under `every dev`, where there's no .app bundle
47
+ # for macOS to read either from. Already in the tree via tao/muda; pinned to the
48
+ # same 0.3 line so no duplicate objc2 crates appear.
49
+ objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
32
50
 
33
51
  [profile.release]
34
52
  strip = true
@@ -14,6 +14,13 @@
14
14
  "permissions": [
15
15
  "core:default",
16
16
  "core:window:allow-set-title",
17
+ "core:window:allow-minimize",
18
+ "core:window:allow-maximize",
19
+ "core:window:allow-unmaximize",
20
+ "core:window:allow-toggle-maximize",
21
+ "core:window:allow-close",
22
+ "core:window:allow-is-maximized",
23
+ "core:window:allow-start-dragging",
17
24
  "notification:default",
18
25
  "dialog:default",
19
26
  "clipboard-manager:allow-read-text",
@@ -0,0 +1 @@
1
+ {"default":{"identifier":"default","description":"Main window IPC: local app origins (the packaged Rails app serves on 127.0.0.1)","remote":{"urls":["http://127.0.0.1:*","http://localhost:*"]},"local":true,"windows":["main"],"permissions":["core:default","core:window:allow-set-title","core:window:allow-minimize","core:window:allow-maximize","core:window:allow-unmaximize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-is-maximized","core:window:allow-start-dragging","notification:default","dialog:default","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
@@ -0,0 +1,53 @@
1
+ // The machinery behind native/desktop/ extensions. Frozen shell code — unlike
2
+ // src/extensions/mod.rs next door, the CLI never rewrites this file.
3
+ //
4
+ // Pages reach extension Rust over EVENTS, not Tauri's invoke. That isn't a
5
+ // remote-mode workaround, it's the only thing that works: a RubyEverywhere page
6
+ // is always served over http — 127.0.0.1 in local mode, your own domain in
7
+ // remote mode — and Tauri v2 refuses invoke from any http origin, including the
8
+ // packaged app's own localhost server. (Verified: invoking a registered command
9
+ // from http://127.0.0.1 fails with "not allowed. Plugin not found" even with
10
+ // the origin granted in capabilities/default.json.) Events are the sanctioned
11
+ // path for non-tauri:// content, and they're what Everywhere.notify has always
12
+ // used.
13
+ //
14
+ // Protocol, mirroring the bridge's Everywhere.desktop.invoke():
15
+ //
16
+ // page -> shell everywhere:command { id, name, payload }
17
+ // shell -> page everywhere:command-result { id, ok: true, value }
18
+ // { id, ok: false, error }
19
+ //
20
+ // The id round-trips so concurrent calls can't take each other's answers.
21
+
22
+ use tauri::{Emitter, Listener};
23
+
24
+ // What a generated dispatcher looks like. One uniform signature rather than
25
+ // Tauri's typed-argument commands, because typed commands only ever arrive
26
+ // through invoke — which, per above, our pages can't use.
27
+ pub type Dispatch = fn(&tauri::AppHandle, &str, serde_json::Value) -> Result<serde_json::Value, String>;
28
+
29
+ pub fn install_command_bridge(app: &tauri::App, dispatch: Dispatch) {
30
+ let handle = app.handle().clone();
31
+ app.listen_any("everywhere:command", move |event| {
32
+ let request: serde_json::Value =
33
+ serde_json::from_str(event.payload()).unwrap_or(serde_json::Value::Null);
34
+ let id = request["id"].as_str().unwrap_or_default().to_string();
35
+ let name = request["name"].as_str().unwrap_or_default().to_string();
36
+ let payload = request["payload"].clone();
37
+ let handle = handle.clone();
38
+
39
+ // Off the event thread: an extension command is app code that may well
40
+ // block on a port, a file, or an HTTP call, and running it inline would
41
+ // stall every other event behind it. The id in the reply means answers
42
+ // coming back out of order costs nothing.
43
+ std::thread::spawn(move || {
44
+ let reply = match dispatch(&handle, &name, payload) {
45
+ Ok(value) => serde_json::json!({ "id": id, "ok": true, "value": value }),
46
+ Err(message) => serde_json::json!({ "id": id, "ok": false, "error": message }),
47
+ };
48
+ if let Err(e) = handle.emit("everywhere:command-result", reply) {
49
+ eprintln!("[extensions] couldn't answer command {name}: {e}");
50
+ }
51
+ });
52
+ });
53
+ }
@@ -0,0 +1,39 @@
1
+ // GENERATED by `every build` / `every dev` from `native.desktop` in the app's
2
+ // config/everywhere.yml. THIS COPY REGISTERS NOTHING — it exists so the bare
3
+ // template still compiles and so the seams in main.rs have something to call.
4
+ //
5
+ // An app that declares Rust extensions gets a stamped copy of this shell under
6
+ // ~/.rubyeverywhere/desktop/<bundle_id>, where this file is rewritten to name
7
+ // the app's own modules and commands, and the app's .rs files sit beside it.
8
+ // Don't edit it by hand: it is overwritten on every build. App code belongs in
9
+ // native/desktop/ in the app repo.
10
+ //
11
+ // See support/desktop/README.md for the freeze contract.
12
+
13
+ use crate::commands;
14
+
15
+ // Every command the shell exposes over Tauri's IPC. Extracted into a function
16
+ // so the generator owns one file instead of editing main.rs: generate_handler!
17
+ // has to name every command in a single list.
18
+ //
19
+ // This is the shell's OWN commands only. App extensions are reached over events
20
+ // instead — see extension_host.rs for why invoke isn't available to them.
21
+ pub fn handler() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
22
+ tauri::generate_handler![commands::notify_command]
23
+ }
24
+
25
+ // Runs once at boot, before any window exists.
26
+ pub fn setup(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
27
+ crate::extension_host::install_command_bridge(app, dispatch);
28
+ Ok(())
29
+ }
30
+
31
+ // Routes an Everywhere.desktop.invoke(name, payload) call to the app's Rust.
32
+ // The generated version matches on every name in `native.desktop.commands`.
33
+ fn dispatch(
34
+ _app: &tauri::AppHandle,
35
+ name: &str,
36
+ _payload: serde_json::Value,
37
+ ) -> Result<serde_json::Value, String> {
38
+ Err(format!("no desktop command named {name:?} — declare it under native.desktop.commands"))
39
+ }