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.
- checksums.yaml +4 -4
- data/README.md +99 -3
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +151 -0
- data/bridge/everywhere/native.css +61 -0
- data/lib/everywhere/agents_guide.rb +57 -0
- data/lib/everywhere/auth_handoff.rb +8 -2
- data/lib/everywhere/builders/desktop.rb +326 -0
- data/lib/everywhere/child_processes.rb +74 -0
- data/lib/everywhere/commands/build.rb +44 -5
- data/lib/everywhere/commands/dev.rb +426 -59
- data/lib/everywhere/commands/install.rb +93 -1
- data/lib/everywhere/commands/release.rb +2 -2
- data/lib/everywhere/config.rb +298 -4
- data/lib/everywhere/console.rb +117 -0
- data/lib/everywhere/desktop_assets.rb +150 -0
- data/lib/everywhere/dock/footer.rb +150 -0
- data/lib/everywhere/dock/screen.rb +114 -0
- data/lib/everywhere/dock/state.rb +59 -0
- data/lib/everywhere/dock.rb +238 -0
- data/lib/everywhere/emulator.rb +2 -2
- data/lib/everywhere/fatal.rb +20 -0
- data/lib/everywhere/line_pump.rb +89 -0
- data/lib/everywhere/log_filter.rb +37 -0
- data/lib/everywhere/native_helper.rb +38 -5
- data/lib/everywhere/paths.rb +62 -11
- data/lib/everywhere/relay.rb +77 -0
- data/lib/everywhere/shellout.rb +90 -15
- data/lib/everywhere/task_pool.rb +123 -0
- data/lib/everywhere/ui.rb +54 -11
- data/lib/everywhere/version.rb +1 -1
- data/support/agents/AGENTS.md +90 -0
- data/support/agents/frameworks/hanami-views.md +26 -0
- data/support/agents/frameworks/hanami.md +10 -0
- data/support/agents/frameworks/rails-views.md +98 -0
- data/support/agents/frameworks/rails.md +16 -0
- data/support/agents/frameworks/sinatra-views.md +24 -0
- data/support/agents/frameworks/sinatra.md +9 -0
- data/support/agents/modes/local.md +12 -0
- data/support/agents/modes/remote.md +14 -0
- data/support/desktop/README.md +121 -0
- data/support/{shell → desktop}/src-tauri/Cargo.lock +23 -0
- data/support/{shell → desktop}/src-tauri/Cargo.toml +19 -1
- data/support/{shell → desktop}/src-tauri/capabilities/default.json +7 -0
- data/support/desktop/src-tauri/gen/schemas/capabilities.json +1 -0
- data/support/desktop/src-tauri/src/extension_host.rs +53 -0
- data/support/desktop/src-tauri/src/extensions/mod.rs +39 -0
- data/support/{shell → desktop}/src-tauri/src/main.rs +355 -10
- data/support/{shell → desktop}/src-tauri/tauri.conf.json +2 -2
- data/support/{macos → release/macos}/notarize.sh +2 -2
- metadata +55 -22
- data/support/github/build.yml +0 -85
- data/support/shell/src-tauri/gen/schemas/capabilities.json +0 -1
- /data/support/{shell → desktop}/splash/index.html +0 -0
- /data/support/{shell → desktop}/src-tauri/build.rs +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/acl-manifests.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/desktop-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/gen/schemas/macOS-schema.json +0 -0
- /data/support/{shell → desktop}/src-tauri/icons/icon.png +0 -0
- /data/support/{shell → desktop}/src-tauri/src/updater.rs +0 -0
- /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
|
-
|
|
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
|
+
}
|