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.
- checksums.yaml +4 -4
- data/README.md +86 -2
- data/bridge/README.md +70 -1
- data/bridge/everywhere/bridge.js +151 -0
- data/bridge/everywhere/native.css +61 -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 +20 -0
- 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/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 +31 -17
- 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
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def
|
|
77
|
-
def
|
|
78
|
-
def
|
|
79
|
-
def
|
|
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
|
-
|
|
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 =
|
|
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,
|
data/lib/everywhere/version.rb
CHANGED
|
@@ -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
|
+
}
|