ruby_everywhere 0.4.0 → 0.5.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -2
  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/auth_handoff.rb +8 -2
  7. data/lib/everywhere/builders/desktop.rb +326 -0
  8. data/lib/everywhere/child_processes.rb +74 -0
  9. data/lib/everywhere/commands/build.rb +44 -5
  10. data/lib/everywhere/commands/dev.rb +426 -59
  11. data/lib/everywhere/commands/install.rb +20 -0
  12. data/lib/everywhere/commands/release.rb +2 -2
  13. data/lib/everywhere/config.rb +298 -4
  14. data/lib/everywhere/console.rb +117 -0
  15. data/lib/everywhere/desktop_assets.rb +150 -0
  16. data/lib/everywhere/dock/footer.rb +150 -0
  17. data/lib/everywhere/dock/screen.rb +114 -0
  18. data/lib/everywhere/dock/state.rb +59 -0
  19. data/lib/everywhere/dock.rb +238 -0
  20. data/lib/everywhere/emulator.rb +2 -2
  21. data/lib/everywhere/fatal.rb +20 -0
  22. data/lib/everywhere/line_pump.rb +89 -0
  23. data/lib/everywhere/log_filter.rb +37 -0
  24. data/lib/everywhere/native_helper.rb +38 -5
  25. data/lib/everywhere/paths.rb +62 -11
  26. data/lib/everywhere/relay.rb +77 -0
  27. data/lib/everywhere/shellout.rb +90 -15
  28. data/lib/everywhere/task_pool.rb +123 -0
  29. data/lib/everywhere/ui.rb +54 -11
  30. data/lib/everywhere/version.rb +1 -1
  31. data/support/desktop/README.md +121 -0
  32. data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
  33. data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
  34. data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
  35. data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
  36. data/support/desktop/src-tauri/src/extension_host.rs +53 -0
  37. data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
  38. data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
  39. data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
  40. data/support/{macos → release/macos}/notarize.sh +2 -2
  41. metadata +31 -17
  42. data/support/github/build.yml +0 -85
  43. data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
  44. /data/support/{shell → desktop}/splash/index.html +0 -0
  45. /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
  46. /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
  47. /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
  48. /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
  49. /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
  50. /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
  51. /data/support/{macos → release/macos}/entitlements.plist +0 -0
data/lib/everywhere/ui.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "fatal"
4
+ require_relative "console"
5
+
3
6
  module Everywhere
4
7
  # Terminal output helpers. Colors follow the informal standard:
5
8
  # on when stdout is a TTY or CLICOLOR_FORCE=1, always off when NO_COLOR is set.
@@ -69,14 +72,18 @@ module Everywhere
69
72
  def warn_line(msg) = "#{yellow("!")} #{msg}"
70
73
  def success_line(msg) = "#{green("✓")} #{bold(msg)}"
71
74
 
72
- def phase(msg) = puts(phase_line(msg))
73
- def step(msg) = puts(step_line(msg))
74
- def substep(msg) = puts(substep_line(msg))
75
- def detail(msg) = puts(detail_line(msg))
76
- def ok(msg) = puts(ok_line(msg))
77
- def bad(msg) = puts(bad_line(msg))
78
- def warn(msg) = puts(warn_line(msg))
79
- def success(msg) = puts(success_line(msg))
75
+ # Everything prints through Console, which serializes concurrent writers and
76
+ # gives the `every dev` dock a chance to repaint around each line. With no
77
+ # dock installed it is a plain $stdout.write, resolved per call so
78
+ # capture_io still works.
79
+ def phase(msg) = Console.puts(phase_line(msg))
80
+ def step(msg) = Console.puts(step_line(msg))
81
+ def substep(msg) = Console.puts(substep_line(msg))
82
+ def detail(msg) = Console.puts(detail_line(msg))
83
+ def ok(msg) = Console.puts(ok_line(msg))
84
+ def bad(msg) = Console.puts(bad_line(msg))
85
+ def warn(msg) = Console.puts(warn_line(msg))
86
+ def success(msg) = Console.puts(success_line(msg))
80
87
 
81
88
  # An indented, dim outcome under a substep — e.g. "accepted", "valid on disk".
82
89
  def note_line(msg, marker: "·", color: :gray)
@@ -84,15 +91,28 @@ module Everywhere
84
91
  " #{tint.call(marker)} #{tint.call(msg)}"
85
92
  end
86
93
 
87
- def note(msg, marker: "·", color: :gray) = puts(note_line(msg, marker: marker, color: color))
94
+ def note(msg, marker: "·", color: :gray) = Console.puts(note_line(msg, marker: marker, color: color))
88
95
 
96
+ # Ends the current unit of work. At the top level Ruby exits 1 and prints
97
+ # nothing extra, so this behaves exactly like the Kernel#abort it replaced —
98
+ # but inside `every dev` the worker thread running the build catches the
99
+ # Fatal, marks that target failed, and hands you back the key menu with the
100
+ # dev server still serving. The message is the same string abort received,
101
+ # so every assert_raises(SystemExit) reading err.message is unaffected.
89
102
  def die!(msg)
90
- abort "#{red("✗")} #{red(msg)}"
103
+ line = "#{red("✗")} #{red(msg)}"
104
+ Console.error(line)
105
+ raise Everywhere::Fatal.new(1, line)
91
106
  end
92
107
 
93
108
  # A yes/no fact, colored by truthiness (for signing receipts).
94
109
  def yn(bool) = bool ? green("yes") : red("no")
95
110
 
111
+ # Braille spinner frames, shared by the transient Status line and the
112
+ # `every dev` dock. Ten frames at ~200ms is one revolution every two
113
+ # seconds — legible as motion without being distracting.
114
+ FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
115
+
96
116
  # --- transient status line -------------------------------------------------
97
117
 
98
118
  # A single self-rewriting line for waits that produce no output — chiefly
@@ -104,8 +124,12 @@ module Everywhere
104
124
  # line every HEADLESS_INTERVAL seconds. Callers MUST `clear` before writing
105
125
  # anything else — `update` tracks whether a line is currently on screen so
106
126
  # `clear` is cheap and idempotent.
127
+ #
128
+ # NOTE: this writes to its `io:` directly, NOT through Console, so it is not
129
+ # serialized against other writers. That's fine for its one caller
130
+ # (`every platform build`, single-threaded) — do not reuse it concurrently.
107
131
  class Status
108
- FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
132
+ FRAMES = UI::FRAMES
109
133
  HEADLESS_INTERVAL = 30 # seconds between plain lines when not a TTY
110
134
 
111
135
  def initialize(io: $stdout)
@@ -157,6 +181,25 @@ module Everywhere
157
181
  "#{secs / 3600}h #{(secs % 3600) / 60}m"
158
182
  end
159
183
 
184
+ # --- ANSI ------------------------------------------------------------------
185
+
186
+ # Escape sequences a tool writes when it thinks it's on a terminal — which,
187
+ # since `every dev` relays its children over a pty, they all do. Two forms
188
+ # matter in practice: SGR color (CSI), and OSC-8 hyperlinks, which cargo
189
+ # wraps around the profile name in its own status lines.
190
+ ANSI = /
191
+ \e\][^\a\e]*(?:\a|\e\\) # OSC ... terminated by BEL or ST
192
+ |
193
+ \e\[[0-9;?]*[ -\/]*[@-~] # CSI ... final byte
194
+ |
195
+ \e[@-Z\\-_] # two-character escapes
196
+ /x
197
+
198
+ # For MATCHING, never for display: a filter that pattern-matches colored
199
+ # output has to look past the codes, but the colors are still what the
200
+ # developer wants to read.
201
+ def strip_ansi(str) = str.to_s.gsub(ANSI, "")
202
+
160
203
  # --- path shortening ------------------------------------------------------
161
204
 
162
205
  # Collapse the noisy machine paths that external tools echo (codesign,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Everywhere
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
 
6
6
  # Version of the @rubyeverywhere/bridge JS this gem ships. bridge/ in the
7
7
  # gem IS the npm package (served to Rails apps by Everywhere::Engine,
@@ -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
+ }