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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ffb921df597efe047c25240e7d8b92aaad49c00c90d546020e053eac618739a
4
- data.tar.gz: 17315b4c5d91eced13b44d7147942d7d6357ee791b8622425ab655eb97b1ab27
3
+ metadata.gz: b4e222fd8e74b0c921b4cdab9d681b0f06bad2d94aae8dbe999415824508a1f5
4
+ data.tar.gz: e3a49bb93e2e49924c70cb622943e02b9bb302ddb828d29fc4c2ee5064c01e0e
5
5
  SHA512:
6
- metadata.gz: 71d9ba1ea63abe51a7262f02f069ef9f068d3192bb20800bdd0fba8bcd50f36f2daa83bb3f758e7273576b69b88be5541428b41f93021579cc1b5054532099ad
7
- data.tar.gz: 59632bacae03ade28233832d0193d2500e4e3f85c07b7e61a39f1e086cc398b40f66d48091ffc9e336a11e2f91994b666ea60fc7ab2cbef2b76252d6fb0ada60
6
+ metadata.gz: 5756a97bf1012daf2069fdb186de16f22a1bdbed9972ec42e384dc8b9b24a84b03b004568af26eeb6990b53336ceb3bfd6c4f3177ec9b0b43e2682db15b382a1
7
+ data.tar.gz: aba1e8c877b1ecd3b9b975a434c89831a9a0b302cbc2bea567472d650f28a442666735cfb9171a20aa615894c7e9bd5de52d2f9e334b7b9af00f750489fac14f
data/README.md CHANGED
@@ -63,6 +63,7 @@ every doctor # check the toolchain before your first build
63
63
  every dev # dev server + native shells on demand, live reload
64
64
  every dev --ios # ...straight into the iOS Simulator
65
65
  every dev --android # ...straight into the Android emulator
66
+ every dev --mobile # ...both at once, built in parallel
66
67
 
67
68
  every build # package the macOS .app
68
69
  every build --ios # build the iOS shell
@@ -78,6 +79,28 @@ boot files.
78
79
  `every dev` never packages anything: it runs your ordinary dev server and points
79
80
  the native shells at it, so the edit-refresh loop is untouched Rails.
80
81
 
82
+ It keeps a dock pinned to the bottom of the terminal showing what each target is
83
+ doing, while the logs scroll normally above it — ordinary scrollback, so you can
84
+ still scroll up and search a backtrace:
85
+
86
+ ```
87
+ web │ Started GET "/notes" for 127.0.0.1
88
+ ios │ ▸ compiling Swift sources
89
+ ────────────────────────────────────────────────────────────────
90
+ ● web :3000 ○ desktop ⠙ iOS 42s ✗ Android see dist/android-build.log
91
+ ⌨ d desktop · i iOS · a Android · b browser · l logs · r restart · ? help · q quit
92
+ ```
93
+
94
+ Every target runs on its own thread, so builds happen in parallel and the keys
95
+ keep working while one is going. A build that fails marks that target red and
96
+ leaves everything else — the dev server included — running; fix the error and
97
+ press the key again. `r` restarts just the dev server, `?` expands the full key
98
+ list, and each line is tagged with the source that printed it once more than one
99
+ thing is talking.
100
+
101
+ Set `EVERY_NO_DOCK=1` (or pipe the output anywhere) to get plain, unadorned
102
+ lines instead.
103
+
81
104
  ## Configuration
82
105
 
83
106
  Everything per-app lives in `config/everywhere.yml`. The native shell templates
@@ -101,6 +124,13 @@ appearance:
101
124
  remote:
102
125
  url: https://app.example.com # remote mode: the deployed app to wrap
103
126
 
127
+ window: # desktop window chrome
128
+ title_bar: overlay # decorated (default) | overlay | frameless
129
+ title: false # draw the title text
130
+ size: [1100, 750]
131
+ min_size: [600, 400]
132
+ drag_height: 28 # top strip that drags the window; 0 to opt out
133
+
104
134
  tabs: # the native mobile tab bar
105
135
  - name: Builds
106
136
  path: /builds
@@ -117,6 +147,12 @@ rules: # Hotwire Native path configuration
117
147
  properties:
118
148
  context: modal
119
149
 
150
+ native: # code compiled into the native shells
151
+ desktop:
152
+ commands: [scan_ports] # Rust in native/desktop/, callable from the page
153
+ crates:
154
+ serialport: "4.3"
155
+
120
156
  build:
121
157
  ruby: "4.0.6"
122
158
  targets: [macos-arm64, ios-arm64, android-arm64]
@@ -134,6 +170,53 @@ Also supported: `auth:` (native OAuth sign-in with a callback scheme),
134
170
  `native/ios/` that `every build` compiles into the shell, including SwiftUI
135
171
  screens, bridge components, a custom splash, and pinned SPM packages.
136
172
 
173
+ Desktop has the same seam in `native/desktop/`, shaped for what a desktop app
174
+ actually wants: not native screens (the whole UI is a webview) but the machine
175
+ underneath it. Drop in Rust, pin the crates you need, and call it from the page:
176
+
177
+ ```yaml
178
+ native:
179
+ desktop:
180
+ commands: [scan_ports]
181
+ setup: true # run native/desktop/setup.rs at boot
182
+ crates:
183
+ serialport: "4.3"
184
+ ```
185
+
186
+ ```rust
187
+ // native/desktop/serial.rs
188
+ pub fn scan_ports(_app: &tauri::AppHandle, payload: serde_json::Value)
189
+ -> Result<serde_json::Value, String> { … }
190
+ ```
191
+
192
+ ```js
193
+ const ports = await Everywhere.desktop.invoke("scan_ports", { baud: 9600 })
194
+ ```
195
+
196
+ Declaring anything under `native.desktop` has a real cost the mobile sections
197
+ don't: the desktop shell stops being one prebuilt binary shared by every app and
198
+ gets compiled per app, so the first build takes minutes instead of seconds. Apps
199
+ that declare nothing are untouched.
200
+
201
+ Images the desktop shell itself draws live in `native/desktop/assets/`, using the
202
+ same `<name>[@2x|@3x][~dark].png` filenames the mobile assets folders use.
203
+ Name one `tray` (or `tray-template` for a macOS template image) and it becomes
204
+ the menu-bar icon; a custom `splash:` can reference the rest as
205
+ `assets/logo.png`. These are for the shell, not your pages — images your HTML
206
+ references should keep coming from the asset pipeline.
207
+
208
+ `window:` shapes the desktop frame. `title_bar: overlay` is the macOS look where
209
+ the traffic lights float over your page and there's no title text — pair it with
210
+ the `.everywhere-titlebar-inset` class from `everywhere/native.css`, which
211
+ reserves exactly the space they sit in. `frameless` removes the system chrome
212
+ altogether; the page then draws its own bar and drives the buttons through
213
+ `Everywhere.window`. Windows and Linux have no overlay style, so `overlay`
214
+ behaves as `frameless` there.
215
+
216
+ Either way the window stays draggable with no markup: the shell makes the top
217
+ 28px of the page drag it (and double-click zoom it), while buttons and links up
218
+ there keep their clicks. `window.drag_height: 0` hands that back to you.
219
+
137
220
  Tabs and path rules ship two ways from that one source: baked into the app
138
221
  bundle for offline/first launch, and served live from
139
222
  `/everywhere/{ios,android}_v1.json`, so changing them deploys with your web app.
@@ -159,6 +242,7 @@ Everywhere.badge.setTab("/inbox", 3)
159
242
  Everywhere.permissions.request("camera")
160
243
  Everywhere.storage.set("theme", "dark")
161
244
  Everywhere.updates.check()
245
+ Everywhere.window.close() // desktop, for a page-drawn title bar
162
246
  ```
163
247
 
164
248
  Everything degrades gracefully — the same page works in a plain browser tab and
@@ -187,7 +271,7 @@ Plus `native_app?`, `native_platform`, `everywhere_badge`,
187
271
  | --- | --- |
188
272
  | `every install` | Prepare an app (boot stub, `config/everywhere.yml`, config tweaks) |
189
273
  | `every doctor` | Check the toolchain — Tebako, Rust, brew deps, Xcode, Android SDK/JDK |
190
- | `every dev` | Dev server with native shells on demand (`--desktop --ios --android --browser`) |
274
+ | `every dev` | Dev server with native shells on demand (`--desktop --ios --android --mobile --browser`) |
191
275
  | `every logs` | Stream shell logs from the Simulator or Android device |
192
276
  | `every build` | Package natively — desktop, `--ios`, or `--android` |
193
277
  | `every icon` | Generate `.icns` / `.ico` / Linux icons from a source PNG |
@@ -237,7 +321,7 @@ and complete on its own.
237
321
  lib/everywhere/ the CLI, config, builders, and Rails engine
238
322
  exe/every, exe/rbe the executables
239
323
  bridge/ @rubyeverywhere/bridge — the JS half
240
- support/shell/ the Tauri desktop shell template
324
+ support/desktop/ the Tauri desktop shell template
241
325
  support/mobile/ios/ the Hotwire Native iOS shell template
242
326
  support/mobile/android/ the Hotwire Native Android shell template
243
327
  ```
data/bridge/README.md CHANGED
@@ -135,6 +135,75 @@ tap, and `<meta name="everywhere:badge">` / `<meta name="everywhere:tab-badge">`
135
135
  tags (the `everywhere_badge` / `everywhere_tab_badge` Rails helpers) are
136
136
  applied on every Turbo visit.
137
137
 
138
+ ### Desktop extensions
139
+
140
+ Rust the app keeps in `native/desktop/` and declares under
141
+ `native.desktop.commands` in `everywhere.yml`:
142
+
143
+ ```js
144
+ Everywhere.desktop.supported // true in the desktop shell
145
+
146
+ const ports = await Everywhere.desktop.invoke("scan_ports", { baud: 9600 })
147
+ ```
148
+
149
+ The payload arrives in Rust as a `serde_json::Value` and whatever the command
150
+ returns comes back as the resolved value; a command that returns `Err` rejects
151
+ with its own message.
152
+
153
+ Unlike the rest of the bridge this does **not** degrade — there's no web
154
+ equivalent for "talk to a serial port", so off the desktop it rejects rather
155
+ than resolving something untrue. Gate on `Everywhere.desktop.supported`.
156
+
157
+ Under the hood this is an event round-trip, not Tauri's `invoke`: a
158
+ RubyEverywhere page is always served over http, and Tauri v2 refuses `invoke`
159
+ from any http origin — including the packaged app's own localhost server.
160
+
161
+ ### Window controls
162
+
163
+ Desktop apps that draw their own title bar — `window.title_bar: overlay` or
164
+ `frameless` in `everywhere.yml` — drive the window from the page:
165
+
166
+ ```js
167
+ Everywhere.window.supported // true in the desktop shell
168
+
169
+ Everywhere.window.minimize()
170
+ Everywhere.window.maximize()
171
+ Everywhere.window.unmaximize()
172
+ Everywhere.window.toggleMaximize() // what a title-bar double-click does
173
+ Everywhere.window.close()
174
+ await Everywhere.window.isMaximized() // Promise<boolean>
175
+ Everywhere.window.startDragging() // from a pointerdown on a custom bar
176
+ ```
177
+
178
+ `overlay` keeps the macOS traffic lights and only hides the title, so the page
179
+ just needs to stay clear of them; `frameless` removes every control, so the page
180
+ supplies its own.
181
+
182
+ **Dragging works out of the box.** With no system title bar left to grab, the
183
+ shell makes the top 28px of the page drag the window, and double-click there
184
+ zooms it — no markup required. Buttons, links and inputs in that strip keep
185
+ their clicks:
186
+
187
+ ```html
188
+ <header class="everywhere-titlebar">
189
+ <span>My App</span>
190
+ <button onclick="Everywhere.window.close()">×</button>
191
+ </header>
192
+ ```
193
+
194
+ To make something *outside* the strip draggable, mark it
195
+ `data-tauri-drag-region`. To own dragging yourself, set `window.drag_height: 0`
196
+ in `everywhere.yml` and call `startDragging()` from your own handlers.
197
+
198
+ `everywhere/native.css` ships `.everywhere-titlebar` (a drag strip that clears
199
+ the traffic lights in overlay mode) plus `.everywhere-titlebar-inset` and
200
+ `.everywhere-titlebar-controls-inset` padding utilities. They key off an
201
+ attribute the bridge stamps on `<html>`, so they're inert in a browser tab, on
202
+ mobile, and in a normal decorated window.
203
+
204
+ Everything here resolves to `null` off the desktop, so the same markup is safe
205
+ everywhere.
206
+
138
207
  ### Auto-updates
139
208
 
140
209
  Inside the desktop shell, apps with an update feed configured can check,
@@ -164,7 +233,7 @@ In a plain browser tab the updates API is inert: `check()` resolves
164
233
  ## Platforms
165
234
 
166
235
  - **desktop** — the Tauri-based RubyEverywhere shell. Notifications, dialogs,
167
- clipboard, shell events, and auto-updates are native.
236
+ clipboard, shell events, window controls, and auto-updates are native.
168
237
  - **browser** — honest web fallbacks: the Notification API, `window.confirm`,
169
238
  `navigator.clipboard`, DOM CustomEvents.
170
239
  - **mobile** — Hotwire Native (iOS shell). `notify()` delivers a native local
@@ -178,6 +178,19 @@ function detectPlatform() {
178
178
 
179
179
  // --- desktop: the Tauri-based RubyEverywhere shell -------------------------
180
180
 
181
+ // Distinguishes concurrent desktop command calls. Only has to be unique within
182
+ // one page, so a counter is enough; the random suffix just keeps two rapid
183
+ // calls from colliding if the page is restored from bfcache mid-flight.
184
+ let commandSequence = 0
185
+ const COMMAND_TIMEOUT = 30000
186
+
187
+ // The shell's own window handle. Named so it can't shadow the global `window`
188
+ // inside the adapter below, where every other method still needs the real one.
189
+ function tauriWindow() {
190
+ const api = window.__TAURI__ && window.__TAURI__.window
191
+ return api && api.getCurrentWindow ? api.getCurrentWindow() : null
192
+ }
193
+
181
194
  const desktop = {
182
195
  notify({ title, body }) {
183
196
  return window.__TAURI__.event.emit("everywhere:notify", { title, body })
@@ -210,6 +223,56 @@ const desktop = {
210
223
  return window.__TAURI__.clipboardManager.readText()
211
224
  },
212
225
 
226
+ // Window controls, for apps that draw their own title bar (`window.title_bar:
227
+ // frameless`, and `overlay` on Windows/Linux where there's no native overlay
228
+ // style). Without a system title bar there's nothing to drag or click, so the
229
+ // page supplies both and calls these. One entry point rather than six methods
230
+ // because the Tauri window API is already uniform: every one of these is a
231
+ // no-arg call returning a promise.
232
+ //
233
+ // These are ACL-gated. The matching core:window:allow-* permissions have to be
234
+ // in BOTH capability sets (capabilities/default.json for local origins, and
235
+ // the mirrored list in authorize_remote_origin for remote mode) or they reject
236
+ // at runtime.
237
+ windowControl(action) {
238
+ const win = tauriWindow()
239
+ return win ? win[action]() : Promise.resolve(null)
240
+ },
241
+
242
+ // Call a Rust function the app keeps in native/desktop/. Events, not invoke:
243
+ // a RubyEverywhere page is always served over http (127.0.0.1 in local mode,
244
+ // your domain in remote mode) and Tauri v2 refuses invoke from any http
245
+ // origin — even the packaged app's own localhost server. See the comment on
246
+ // install_command_bridge in the shell's extension_host.rs.
247
+ //
248
+ // The id round-trips so concurrent calls can't take each other's answers,
249
+ // which a bare request/response event pair would otherwise allow.
250
+ desktopInvoke(name, payload) {
251
+ return new Promise((resolve, reject) => {
252
+ const id = `${commandSequence++}-${Math.random().toString(36).slice(2)}`
253
+ let off = null
254
+ const settle = (fn, value) => {
255
+ clearTimeout(timer)
256
+ if (off) off()
257
+ fn(value)
258
+ }
259
+ const timer = setTimeout(
260
+ () => settle(reject, new Error(`desktop command ${name} timed out`)),
261
+ COMMAND_TIMEOUT
262
+ )
263
+ off = this.on("command-result", (result) => {
264
+ if (!result || result.id !== id) return
265
+ if (result.ok) settle(resolve, result.value)
266
+ else settle(reject, new Error(result.error || `desktop command ${name} failed`))
267
+ })
268
+ window.__TAURI__.event.emit("everywhere:command", {
269
+ id,
270
+ name,
271
+ payload: payload === undefined ? null : payload
272
+ })
273
+ })
274
+ },
275
+
213
276
  // Ask the shell to check its update feed; resolve off whichever update
214
277
  // event answers first. The shell always replies to an explicit check
215
278
  // (available / none / error), the timeout is just a safety net.
@@ -295,6 +358,22 @@ const browser = {
295
358
  return null
296
359
  },
297
360
 
361
+ // A browser tab has no window of its own to drive — the one thing close()
362
+ // could map to (window.close) only works for script-opened windows, which
363
+ // this never is. Resolving null keeps a page that draws a custom title bar
364
+ // from throwing when it's opened in a plain browser.
365
+ windowControl() {
366
+ return Promise.resolve(null)
367
+ },
368
+
369
+ // There is no Rust here. Rejecting (rather than resolving null) is the honest
370
+ // answer: unlike the rest of the bridge, a desktop command has no web
371
+ // equivalent to degrade to, and silently resolving would let a page act on a
372
+ // result it never got. Gate on Everywhere.desktop.supported.
373
+ desktopInvoke(name) {
374
+ return Promise.reject(new Error(`desktop command ${name} is unavailable outside the desktop shell`))
375
+ },
376
+
298
377
  // Honest approximation: the Vibration API where it exists (Android
299
378
  // browsers); silently nothing elsewhere (iOS Safari has no equivalent).
300
379
  haptic(kind, value) {
@@ -764,6 +843,17 @@ const adapter = adapters[platform]
764
843
  // registered by the time app code first calls notify().
765
844
  if (platform === "mobile") installWebBridge()
766
845
 
846
+ // Publish the window chrome mode to CSS. The titlebar utilities in native.css
847
+ // key off this attribute, which is the only way they can know a mode that lives
848
+ // in everywhere.yml — and it keeps them inert everywhere else, since nothing
849
+ // stamps it in a browser tab, on mobile, or in a normally decorated window.
850
+ if (platform === "desktop" && typeof document !== "undefined") {
851
+ const titleBar = config.window && config.window.title_bar
852
+ if (titleBar && titleBar !== "decorated") {
853
+ document.documentElement.dataset.everywhereTitlebar = titleBar
854
+ }
855
+ }
856
+
767
857
  // Mutable so setChannel can keep updates.channel truthful without a reload.
768
858
  let updatesChannel = (config.updates && config.updates.channel) || null
769
859
 
@@ -1034,6 +1124,67 @@ export const Everywhere = {
1034
1124
  }
1035
1125
  },
1036
1126
 
1127
+ // Rust the app keeps in native/desktop/ and declares under
1128
+ // `native.desktop.commands` in everywhere.yml:
1129
+ //
1130
+ // const ports = await Everywhere.desktop.invoke("scan_ports", { baud: 9600 })
1131
+ //
1132
+ // The payload arrives as serde_json::Value and whatever the command returns
1133
+ // comes back as the resolved value. Errors reject with the command's own
1134
+ // message. Rejects immediately off the desktop — there's no web fallback for
1135
+ // "talk to a serial port", so check `supported` first.
1136
+ desktop: {
1137
+ get supported() {
1138
+ return platform === "desktop"
1139
+ },
1140
+ invoke(name, payload) {
1141
+ return adapter.desktopInvoke(name, payload)
1142
+ }
1143
+ },
1144
+
1145
+ // Desktop window controls, for apps that draw their own title bar. Set
1146
+ // `window.title_bar: overlay` (macOS traffic lights float over your content)
1147
+ // or `frameless` (no system chrome at all) in everywhere.yml; frameless is
1148
+ // the one that needs these, since the page then owns dragging and the
1149
+ // minimize/maximize/close buttons:
1150
+ //
1151
+ // <header data-tauri-drag-region>
1152
+ // <button onclick="Everywhere.window.close()">×</button>
1153
+ // </header>
1154
+ //
1155
+ // Everything resolves to null in a browser tab and on mobile, where the OS
1156
+ // owns the frame — so the same markup is safe everywhere.
1157
+ window: {
1158
+ get supported() {
1159
+ return platform === "desktop"
1160
+ },
1161
+ minimize() {
1162
+ return adapter.windowControl("minimize")
1163
+ },
1164
+ maximize() {
1165
+ return adapter.windowControl("maximize")
1166
+ },
1167
+ unmaximize() {
1168
+ return adapter.windowControl("unmaximize")
1169
+ },
1170
+ // What a double-click on a title bar normally does.
1171
+ toggleMaximize() {
1172
+ return adapter.windowControl("toggleMaximize")
1173
+ },
1174
+ close() {
1175
+ return adapter.windowControl("close")
1176
+ },
1177
+ isMaximized() {
1178
+ return adapter.windowControl("isMaximized").then((maximized) => !!maximized)
1179
+ },
1180
+ // Call from a pointerdown handler on your custom title bar. Tauri's
1181
+ // data-tauri-drag-region attribute does this for you; this is the escape
1182
+ // hatch for a drag region you compute yourself.
1183
+ startDragging() {
1184
+ return adapter.windowControl("startDragging")
1185
+ }
1186
+ },
1187
+
1037
1188
  updates: {
1038
1189
  // True only when the shell actually has a feed to check (everywhere.yml
1039
1190
  // updates: url + public_key made it into the shipped config).
@@ -201,3 +201,64 @@ html[data-bridge-platform] [data-everywhere-nav-menu] {
201
201
  @keyframes everywhere-sheet-rise {
202
202
  from { transform: translateY(1rem); opacity: 0; }
203
203
  }
204
+
205
+ /* ---------------------------------------------------------------------------
206
+ * Desktop window chrome — for `window.title_bar: overlay | frameless` in
207
+ * everywhere.yml. The bridge stamps data-everywhere-titlebar on <html>, so
208
+ * every rule here is scoped to the mode that needs it: on mobile, in a plain
209
+ * browser tab, and in a normal decorated window the attribute is absent and
210
+ * these classes do nothing. Safe to apply unconditionally.
211
+ * ------------------------------------------------------------------------- */
212
+
213
+ :root {
214
+ /* macOS traffic lights: a ~78px cluster inside a 28px band at the top left.
215
+ * Override both if you draw a taller bar. */
216
+ --everywhere-titlebar-height: 28px;
217
+ --everywhere-titlebar-controls: 78px;
218
+ }
219
+
220
+ /* Overlay mode: the system still draws (and positions) the traffic lights over
221
+ * your page. Reserve the band so your content doesn't slide underneath them.
222
+ * Frameless draws no controls at all, so it reserves nothing — the page owns
223
+ * that space and decides what goes there. */
224
+ [data-everywhere-titlebar="overlay"] .everywhere-titlebar-inset {
225
+ padding-top: var(--everywhere-titlebar-height);
226
+ }
227
+ [data-everywhere-titlebar="overlay"] .everywhere-titlebar-controls-inset {
228
+ padding-left: var(--everywhere-titlebar-controls);
229
+ }
230
+
231
+ /* A title-bar strip across the top of the window. Purely layout — the shell
232
+ * already makes the top 28px drag the window, so this needs no attribute:
233
+ *
234
+ * <header class="everywhere-titlebar">My App</header>
235
+ *
236
+ * Buttons and links inside it keep their clicks. Mark a region outside the
237
+ * strip with data-tauri-drag-region to make that draggable too, or set
238
+ * window.drag_height: 0 in everywhere.yml to take dragging over entirely.
239
+ *
240
+ * In overlay mode it clears the traffic lights; in frameless it starts at the
241
+ * left edge, because there's nothing there to clear. */
242
+ [data-everywhere-titlebar] .everywhere-titlebar {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 0.5rem;
246
+ height: var(--everywhere-titlebar-height);
247
+ user-select: none;
248
+ -webkit-user-select: none;
249
+ cursor: default;
250
+ }
251
+ [data-everywhere-titlebar="overlay"] .everywhere-titlebar {
252
+ padding-left: var(--everywhere-titlebar-controls);
253
+ }
254
+
255
+ /* Anything clickable inside the drag strip has to opt out, or the drag region
256
+ * swallows the click before the control ever sees it. */
257
+ [data-everywhere-titlebar] .everywhere-titlebar button,
258
+ [data-everywhere-titlebar] .everywhere-titlebar a,
259
+ [data-everywhere-titlebar] .everywhere-titlebar input,
260
+ [data-everywhere-titlebar] .everywhere-titlebar select,
261
+ [data-everywhere-titlebar] .everywhere-titlebar [data-tauri-drag-region="false"] {
262
+ -webkit-app-region: no-drag;
263
+ cursor: default;
264
+ }
@@ -151,8 +151,14 @@ module Everywhere
151
151
  # A provider path requested from inside the shell's web view — the visits
152
152
  # the shell couldn't intercept natively (a `data-turbo="false"` link, or the
153
153
  # POST that OmniAuth 2 requires). Bounce to a page that can ask it to.
154
+ #
155
+ # mobile_platform_of, not native_platform_of: the whole handoff exists
156
+ # because a phone shell must open the provider in ASWebAuthenticationSession
157
+ # / Custom Tabs. The desktop shell already sends external links to the system
158
+ # browser, so diverting there would bounce sign-in to a page nothing can
159
+ # complete.
154
160
  def divert(env, config)
155
- return nil unless Everywhere.native_platform_of(env["HTTP_USER_AGENT"])
161
+ return nil unless Everywhere.mobile_platform_of(env["HTTP_USER_AGENT"])
156
162
  return nil unless config.oauth_path?(env["PATH_INFO"])
157
163
 
158
164
  redirect("#{NATIVE}?#{Rack::Utils.build_query("to" => path_with_query(env))}")
@@ -172,7 +178,7 @@ module Everywhere
172
178
  # Only ever the auth browser: inside the web view the app is already
173
179
  # holding the jar we would be handing it, and a stale marker there must
174
180
  # not divert an ordinary visit into a callback URL it can't open.
175
- marker = auth_marker(env) unless Everywhere.native_platform_of(env["HTTP_USER_AGENT"])
181
+ marker = auth_marker(env) unless Everywhere.mobile_platform_of(env["HTTP_USER_AGENT"])
176
182
  return @app.call(env) if marker.nil? || config.oauth_path?(env["PATH_INFO"]) || !navigation?(env)
177
183
 
178
184
  _, headers, body = @app.call(env)