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
@@ -25,6 +25,15 @@ use tauri::{Manager, RunEvent, WebviewUrl, WebviewWindowBuilder};
25
25
 
26
26
  mod updater;
27
27
 
28
+ // The event bridge extension commands answer on. Frozen shell code.
29
+ mod extension_host;
30
+
31
+ // The app's own Rust, from native/desktop/ in the app repo. The CLI regenerates
32
+ // src/extensions/mod.rs on every build from what `native.desktop` declares in
33
+ // everywhere.yml; the copy checked in here registers nothing, so the bare
34
+ // template still builds. See support/desktop/README.md for the freeze contract.
35
+ mod extensions;
36
+
28
37
  struct Sidecar(Mutex<Option<Child>>);
29
38
 
30
39
  struct MenuEntry {
@@ -44,6 +53,28 @@ struct AppConfig {
44
53
  background_light: Option<(u8, u8, u8)>,
45
54
  menu: Vec<MenuEntry>,
46
55
  tray: Vec<TrayEntry>,
56
+ window: WindowConfig,
57
+ }
58
+
59
+ // Window chrome, from `window:` in everywhere.yml. Every field is optional and
60
+ // the defaults live HERE rather than in the Ruby config, so there is exactly one
61
+ // place each default is written down.
62
+ struct WindowConfig {
63
+ title_bar: TitleBar,
64
+ // Tri-state: None leaves the shell's default (show the title) alone.
65
+ title: Option<bool>,
66
+ resizable: Option<bool>,
67
+ size: Option<(f64, f64)>,
68
+ min_size: Option<(f64, f64)>,
69
+ }
70
+
71
+ #[derive(Clone, Copy, PartialEq)]
72
+ enum TitleBar {
73
+ Decorated,
74
+ // macOS: traffic lights float over the page, no title text. There is no
75
+ // Windows/Linux equivalent, so it degrades to Frameless there.
76
+ Overlay,
77
+ Frameless,
47
78
  }
48
79
 
49
80
  struct TrayEntry {
@@ -112,9 +143,32 @@ fn load_config() -> AppConfig {
112
143
  .collect()
113
144
  })
114
145
  .unwrap_or_default(),
146
+ // Indexing a missing key yields Null rather than panicking, so an app
147
+ // with no `window:` at all lands on every default below.
148
+ window: WindowConfig {
149
+ title_bar: match value["window"]["title_bar"].as_str() {
150
+ Some("overlay") => TitleBar::Overlay,
151
+ Some("frameless") => TitleBar::Frameless,
152
+ _ => TitleBar::Decorated,
153
+ },
154
+ title: value["window"]["title"].as_bool(),
155
+ resizable: value["window"]["resizable"].as_bool(),
156
+ size: dimensions(&value["window"]["size"]),
157
+ min_size: dimensions(&value["window"]["min_size"]),
158
+ },
115
159
  }
116
160
  }
117
161
 
162
+ // A [width, height] pair. The Ruby side already rejects the near-misses with a
163
+ // readable error; this is the belt-and-braces so a hand-edited everywhere.json
164
+ // can't produce a zero-pixel window.
165
+ fn dimensions(value: &serde_json::Value) -> Option<(f64, f64)> {
166
+ let pair = value.as_array()?;
167
+ let width = pair.first()?.as_f64()?;
168
+ let height = pair.get(1)?.as_f64()?;
169
+ (width > 0.0 && height > 0.0).then_some((width, height))
170
+ }
171
+
118
172
  fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
119
173
  let h = hex.trim_start_matches('#');
120
174
  match h.len() {
@@ -130,6 +184,92 @@ fn parse_hex(hex: &str) -> Option<(u8, u8, u8)> {
130
184
  }
131
185
  }
132
186
 
187
+ // --- app assets (native/desktop/assets) -------------------------------------
188
+ //
189
+ // Images the APP ships for the SHELL to draw: the tray icon, splash artwork,
190
+ // anything extension Rust loads. Not for pages — those keep using the asset
191
+ // pipeline, which already fingerprints and serves them.
192
+
193
+ // Same resolution order as load_config, for the same reason: under `every dev`
194
+ // there is no .app to look inside, so the CLI stages the folder and points here.
195
+ fn assets_dir() -> Option<PathBuf> {
196
+ if let Ok(dir) = std::env::var("NATIVE_ASSETS_DIR") {
197
+ let path = PathBuf::from(dir);
198
+ if path.is_dir() {
199
+ return Some(path);
200
+ }
201
+ }
202
+ let exe = std::env::current_exe().ok()?;
203
+ let path = exe.parent()?.join("../Resources/assets");
204
+ path.is_dir().then_some(path)
205
+ }
206
+
207
+ // Resolve a logical asset name to a file. The folder is flat and the suffixes
208
+ // carry the variants (<name>[@2x|@3x][~dark].<ext>) — the CLI validated them,
209
+ // so anything unparseable here simply doesn't match.
210
+ //
211
+ // Highest scale wins: nothing at this layer knows which display the image will
212
+ // land on, and downscaling a @3x looks far better than upscaling a 1x.
213
+ fn asset_path(name: &str, dark: bool) -> Option<PathBuf> {
214
+ let dir = assets_dir()?;
215
+ let mut best: Option<(u8, PathBuf)> = None;
216
+ for entry in std::fs::read_dir(dir).ok()?.flatten() {
217
+ let path = entry.path();
218
+ let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
219
+ continue;
220
+ };
221
+ let (base, scale, is_dark) = parse_asset_stem(stem);
222
+ if base != name || is_dark != dark {
223
+ continue;
224
+ }
225
+ if best.as_ref().map_or(true, |(best_scale, _)| scale > *best_scale) {
226
+ best = Some((scale, path));
227
+ }
228
+ }
229
+ best.map(|(_, path)| path)
230
+ }
231
+
232
+ // The ~dark variant when the app shipped one, else the plain asset.
233
+ fn asset(name: &str, dark: bool) -> Option<PathBuf> {
234
+ if dark {
235
+ if let Some(path) = asset_path(name, true) {
236
+ return Some(path);
237
+ }
238
+ }
239
+ asset_path(name, false)
240
+ }
241
+
242
+ // Trailing @2x/@3x and ~dark markers, in either order. Mirrors the Ruby
243
+ // DesktopAssets#parse — one filename convention across all three platforms.
244
+ fn parse_asset_stem(stem: &str) -> (String, u8, bool) {
245
+ let mut name = stem.to_string();
246
+ let mut scale = 1u8;
247
+ let mut dark = false;
248
+ loop {
249
+ if let Some(rest) = name.strip_suffix("~dark") {
250
+ dark = true;
251
+ name = rest.to_string();
252
+ } else if let Some(rest) = name.strip_suffix("@2x") {
253
+ scale = 2;
254
+ name = rest.to_string();
255
+ } else if let Some(rest) = name.strip_suffix("@3x") {
256
+ scale = 3;
257
+ name = rest.to_string();
258
+ } else {
259
+ break;
260
+ }
261
+ }
262
+ (name, scale, dark)
263
+ }
264
+
265
+ fn asset_mime(path: &Path) -> &'static str {
266
+ match path.extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase().as_str() {
267
+ "jpg" | "jpeg" => "image/jpeg",
268
+ "ico" => "image/x-icon",
269
+ _ => "image/png",
270
+ }
271
+ }
272
+
133
273
  fn sidecar_log_path(app_data: &Path) -> PathBuf {
134
274
  let dir = app_data.join("log");
135
275
  let _ = std::fs::create_dir_all(&dir);
@@ -216,7 +356,7 @@ fn main() {
216
356
  .plugin(tauri_plugin_dialog::init())
217
357
  .plugin(tauri_plugin_clipboard_manager::init())
218
358
  .plugin(window_state)
219
- .invoke_handler(tauri::generate_handler![notify_command])
359
+ .invoke_handler(extensions::handler())
220
360
  .setup(move |app| {
221
361
  // A persisted user channel choice (Everywhere.updates.setChannel)
222
362
  // overrides everywhere.yml's updates.channel. Fold it in before
@@ -247,6 +387,12 @@ fn main() {
247
387
  build_menu(app, &config)?;
248
388
  updater::init(app, &config.raw_json);
249
389
 
390
+ // The app's own Rust gets the handle before any window exists, so it
391
+ // can register plugins, open ports, or spawn workers that the first
392
+ // page load already depends on. It also installs the event-based
393
+ // command transport remote-mode pages use.
394
+ extensions::setup(app)?;
395
+
250
396
  // Dev mode (`every dev`): no sidecar, just point the webview at the
251
397
  // developer's normal dev server.
252
398
  if let Ok(dev_url) = std::env::var("NATIVE_DEV_URL") {
@@ -401,6 +547,17 @@ fn main() {
401
547
  .build(tauri::generate_context!())
402
548
  .expect("error while building tauri application")
403
549
  .run(|app, event| {
550
+ // Tauri applies its own compiled-in icons/icon.png to the Dock as
551
+ // part of finishing launch — AFTER setup() — so setting ours there
552
+ // gets stomped. Ready is the first point we're guaranteed to win.
553
+ #[cfg(target_os = "macos")]
554
+ if let RunEvent::Ready = event {
555
+ if let Ok(icon) = std::env::var("NATIVE_ICON_PATH") {
556
+ if !icon.is_empty() {
557
+ set_dock_icon(&icon);
558
+ }
559
+ }
560
+ }
404
561
  if let RunEvent::Exit = event {
405
562
  // A fully staged silent update (updates.auto: install) swaps
406
563
  // in now — two renames, no perceptible quit latency.
@@ -581,8 +738,28 @@ fn build_tray(app: &tauri::App, config: &AppConfig) -> tauri::Result<()> {
581
738
  .menu(&menu)
582
739
  .show_menu_on_left_click(true)
583
740
  .tooltip(&config.name);
584
- if let Some(icon) = app.default_window_icon() {
585
- tray = tray.icon(icon.clone());
741
+
742
+ // A `tray` asset (native/desktop/assets/tray.png) beats the app icon here.
743
+ // An app icon is a 1024px full-colour square; macOS squeezes that into
744
+ // ~22pt of menu bar and it turns to mush.
745
+ //
746
+ // `tray-template` is the macOS convention instead: a black-and-alpha image
747
+ // the system recolours itself, so it stays legible in a light menu bar, a
748
+ // dark one, and under a highlight. Two names rather than a config key —
749
+ // whether an image is a template is a property of the image.
750
+ let (template, icon_path) = match asset("tray-template", false) {
751
+ Some(path) => (true, Some(path)),
752
+ None => (false, asset("tray", false)),
753
+ };
754
+ match icon_path.and_then(|path| tauri::image::Image::from_path(path).ok()) {
755
+ Some(icon) => {
756
+ tray = tray.icon(icon).icon_as_template(template);
757
+ }
758
+ None => {
759
+ if let Some(icon) = app.default_window_icon() {
760
+ tray = tray.icon(icon.clone());
761
+ }
762
+ }
586
763
  }
587
764
  tray.on_menu_event(|app, event| handle_menu_id(app, event.id().0.as_str()))
588
765
  .build(app)?;
@@ -644,6 +821,10 @@ const INIT_SCRIPT: &str = r#"
644
821
  // fall back to the app name, suffix "(dev)" in dev mode.
645
822
  const syncTitle = () => {
646
823
  try {
824
+ // `window.title: false` means the window has no title text, so don't put
825
+ // one back on the next page load — this runs on every visit and would
826
+ // otherwise quietly undo what build_window set.
827
+ if (cfg.window && cfg.window.title === false) return;
647
828
  const base = document.title || cfg.name || "";
648
829
  const title = cfg.dev ? base + " (dev)" : base;
649
830
  if (title && window.__TAURI__ && window.__TAURI__.window) {
@@ -658,6 +839,53 @@ const INIT_SCRIPT: &str = r#"
658
839
  });
659
840
  document.addEventListener('turbo:load', syncTitle);
660
841
 
842
+ // Overlay and frameless windows have no system title bar left to grab: the
843
+ // webview covers that strip, so every mousedown in it belongs to the page and
844
+ // the window stops being draggable. Rather than make every app hand-roll a
845
+ // drag region just to hide a title, the top strip drags the window by default.
846
+ //
847
+ // Interactive elements keep their clicks — a toolbar button up there still
848
+ // works — and `window.drag_height: 0` in everywhere.yml turns it off for an
849
+ // app that wants to own the whole area itself.
850
+ const dragTop = () => {
851
+ const win = cfg.window || {};
852
+ if (!win.title_bar || win.title_bar === 'decorated') return;
853
+ const height = win.drag_height === undefined ? 28 : win.drag_height;
854
+ if (!height) return;
855
+
856
+ const NO_DRAG = 'a,button,input,select,textarea,label,summary,[role="button"],' +
857
+ '[contenteditable],[data-no-drag]';
858
+ // data-tauri-drag-region is deliberately NOT deferred to. Tauri's own
859
+ // handling of it round-trips through the IPC, which is blocked on http
860
+ // origins — the same wall invoke hits — so on a RubyEverywhere page that
861
+ // attribute does nothing on its own. We honour it here instead, and extend
862
+ // it past the strip: marking a region draggable should work wherever it is.
863
+ const draggable = (e) => {
864
+ if (e.button !== 0 || !e.target || !e.target.closest) return false;
865
+ if (e.target.closest(NO_DRAG)) return false;
866
+ return e.target.closest('[data-tauri-drag-region]') !== null || e.clientY <= height;
867
+ };
868
+
869
+ // Double-click-to-zoom is detected here rather than with a dblclick
870
+ // listener: startDragging() takes over the mouse on mousedown, so the
871
+ // dblclick event never arrives.
872
+ let lastDown = 0;
873
+ document.addEventListener('mousedown', (e) => {
874
+ if (!draggable(e)) return;
875
+ try {
876
+ const w = window.__TAURI__.window.getCurrentWindow();
877
+ e.preventDefault();
878
+ // Keep Tauri's own drag-region listener from starting a second drag on
879
+ // top of ours if a future version stops needing the IPC for it.
880
+ e.stopPropagation();
881
+ const now = Date.now();
882
+ if (now - lastDown < 400) { lastDown = 0; w.toggleMaximize(); }
883
+ else { lastDown = now; w.startDragging(); }
884
+ } catch (_) {}
885
+ });
886
+ };
887
+ dragTop();
888
+
661
889
  const hookMenu = () => {
662
890
  if (window.__TAURI__ && window.__TAURI__.event) {
663
891
  window.__TAURI__.event.listen('everywhere:menu', (e) => {
@@ -682,6 +910,32 @@ const INIT_SCRIPT: &str = r#"
682
910
  // tells it the request came from the native shell and which app version (from
683
911
  // everywhere.yml → the submitted build) made it. Version is absent only in bare
684
912
  // `cargo run` dev where no config was passed.
913
+ // --- macOS Dock icon under `every dev` --------------------------------------
914
+ //
915
+ // The app NAME is not handled here: macOS reads it from CFBundleName, and the
916
+ // menu bar plus the About/Hide/Quit items are built from the bundle before any
917
+ // of our Rust runs. (Setting NSProcessInfo's processName doesn't move them —
918
+ // tried it.) `every dev` solves that by running this binary from inside a
919
+ // throwaway .app it assembles; see Commands::Dev#stage_dev_bundle.
920
+ //
921
+ // The icon does need doing here, because Tauri bakes icons/icon.png into the
922
+ // binary and applies it at launch — which in dev is the gem's placeholder, and
923
+ // it would win over a bundle icns anyway. This replaces it with the app's own,
924
+ // already shaped by the CLI exactly like the packaged .icns, so the Dock looks
925
+ // the same in dev as it does in a release build.
926
+ #[cfg(target_os = "macos")]
927
+ fn set_dock_icon(path: &str) {
928
+ use objc2::{AllocAnyThread, MainThreadMarker};
929
+ use objc2_app_kit::{NSApplication, NSImage};
930
+ use objc2_foundation::NSString;
931
+
932
+ let Some(mtm) = MainThreadMarker::new() else { return };
933
+ let image = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path));
934
+ if let Some(image) = image {
935
+ unsafe { NSApplication::sharedApplication(mtm).setApplicationIconImage(Some(&image)) };
936
+ }
937
+ }
938
+
685
939
  fn ua_marker(config: &AppConfig) -> String {
686
940
  let os = std::env::consts::OS;
687
941
  match &config.version {
@@ -739,16 +993,51 @@ fn build_window(
739
993
  url: WebviewUrl,
740
994
  ) -> tauri::Result<tauri::WebviewWindow> {
741
995
  let dev_mode = std::env::var("NATIVE_DEV_URL").is_ok();
996
+ let window = &config.window;
997
+ let (width, height) = window.size.unwrap_or((1100.0, 750.0));
742
998
 
743
999
  let mut builder = WebviewWindowBuilder::new(app, "main", url)
744
- .title(title)
745
- .inner_size(1100.0, 750.0)
1000
+ // `title: false` blanks the text everywhere it can still show up (the
1001
+ // Window menu, Mission Control) — not just in the title bar. The " (dev)"
1002
+ // suffix the callers add goes with it; the bridge already tells the page
1003
+ // it's a dev build via config.dev.
1004
+ .title(if window.title == Some(false) { "" } else { title })
1005
+ .inner_size(width, height)
746
1006
  // Injected into every page: app config global, remote console, native
747
1007
  // confirm default, and the menu-navigation listener.
748
1008
  .initialization_script(INIT_SCRIPT.replace("/*__CONFIG_JSON__*/ null", &config.raw_json));
749
1009
  if let Some((r, g, b)) = config.background_light {
750
1010
  builder = builder.background_color(tauri::window::Color(r, g, b, 255));
751
1011
  }
1012
+ if let Some((min_width, min_height)) = window.min_size {
1013
+ builder = builder.min_inner_size(min_width, min_height);
1014
+ }
1015
+ if let Some(resizable) = window.resizable {
1016
+ builder = builder.resizable(resizable);
1017
+ }
1018
+ // Title bar. Overlay is a real macOS window style — the system keeps drawing
1019
+ // (and positioning) the traffic lights, we just hide the title and let the
1020
+ // webview run full-height underneath. Windows and Linux have no equivalent,
1021
+ // so overlay degrades to frameless there and the page supplies its own
1022
+ // controls through Everywhere.window.
1023
+ match window.title_bar {
1024
+ TitleBar::Decorated => {}
1025
+ TitleBar::Overlay => {
1026
+ #[cfg(target_os = "macos")]
1027
+ {
1028
+ builder = builder
1029
+ .title_bar_style(tauri::TitleBarStyle::Overlay)
1030
+ .hidden_title(true);
1031
+ }
1032
+ #[cfg(not(target_os = "macos"))]
1033
+ {
1034
+ builder = builder.decorations(false);
1035
+ }
1036
+ }
1037
+ TitleBar::Frameless => {
1038
+ builder = builder.decorations(false);
1039
+ }
1040
+ }
752
1041
 
753
1042
  // Cookie / session persistence: give the webview a stable data store so
754
1043
  // logins survive relaunches. macOS carries it (and the UA marker) on a
@@ -791,8 +1080,46 @@ fn build_window(
791
1080
  // everywhere.yml) loads via data: URL — no server needed before Rails boots.
792
1081
  fn custom_splash_url() -> Option<WebviewUrl> {
793
1082
  let exe = std::env::current_exe().ok()?;
794
- let html = std::fs::read(exe.parent()?.join("../Resources/splash.html")).ok()?;
795
- format!("data:text/html;base64,{}", base64(&html)).parse().ok().map(WebviewUrl::External)
1083
+ let html = std::fs::read_to_string(exe.parent()?.join("../Resources/splash.html")).ok()?;
1084
+ let html = inline_splash_assets(html);
1085
+ format!("data:text/html;base64,{}", base64(html.as_bytes())).parse().ok().map(WebviewUrl::External)
1086
+ }
1087
+
1088
+ // A data: URL page has no base to resolve relative URLs against, so an <img
1089
+ // src="assets/logo.png"> in a splash would silently render nothing. Rewrite
1090
+ // those references into data: URIs first, so the markup works the way it reads.
1091
+ //
1092
+ // Substitution rather than parsing: the splash is one small file the app author
1093
+ // wrote, the asset names come from a directory we control, and a real HTML
1094
+ // parse would pull in a dependency to solve a problem nobody has.
1095
+ fn inline_splash_assets(html: String) -> String {
1096
+ let Some(dir) = assets_dir() else {
1097
+ return html;
1098
+ };
1099
+ let Ok(entries) = std::fs::read_dir(dir) else {
1100
+ return html;
1101
+ };
1102
+
1103
+ let mut out = html;
1104
+ for entry in entries.flatten() {
1105
+ let path = entry.path();
1106
+ let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
1107
+ continue;
1108
+ };
1109
+ if !out.contains(name) {
1110
+ continue;
1111
+ }
1112
+ let Ok(bytes) = std::fs::read(&path) else {
1113
+ continue;
1114
+ };
1115
+ let uri = format!("data:{};base64,{}", asset_mime(&path), base64(&bytes));
1116
+ // Longest prefix first: replacing the bare "assets/…" first would strip
1117
+ // the path off "./assets/…" and leave a dangling "./" glued to the URI.
1118
+ for prefix in ["./assets/", "/assets/", "assets/"] {
1119
+ out = out.replace(&format!("{prefix}{name}"), &uri);
1120
+ }
1121
+ }
1122
+ out
796
1123
  }
797
1124
 
798
1125
  fn base64(data: &[u8]) -> String {
@@ -836,6 +1163,17 @@ fn authorize_remote_origin(app: &tauri::App, remote_url: &str) -> tauri::Result<
836
1163
  "permissions": [
837
1164
  "core:default",
838
1165
  "core:window:allow-set-title",
1166
+ // Window controls (Everywhere.window.*): a frameless or overlay
1167
+ // title bar is drawn by the page, so the page must be able to drag
1168
+ // and close the window it lives in. Kept in lockstep with
1169
+ // capabilities/default.json — see the mirroring rule above.
1170
+ "core:window:allow-minimize",
1171
+ "core:window:allow-maximize",
1172
+ "core:window:allow-unmaximize",
1173
+ "core:window:allow-toggle-maximize",
1174
+ "core:window:allow-close",
1175
+ "core:window:allow-is-maximized",
1176
+ "core:window:allow-start-dragging",
839
1177
  "notification:default",
840
1178
  "dialog:default",
841
1179
  "clipboard-manager:allow-read-text",
@@ -898,7 +1236,14 @@ fn notify(app: &tauri::AppHandle, title: &str, body: &str) {
898
1236
  }
899
1237
  }
900
1238
 
901
- #[tauri::command]
902
- fn notify_command(app: tauri::AppHandle, title: String, body: String) {
903
- notify(&app, &title, &body);
1239
+ // The shell's own Tauri commands. In a module rather than at the crate root
1240
+ // because #[tauri::command] expands to helper macros that generate_handler!
1241
+ // resolves by path — and a command sitting in main.rs can only be named by an
1242
+ // absolute `crate::` path, which is exactly the one form that doesn't work from
1243
+ // another module. The extensions registry has to name this, so it lives here.
1244
+ pub(crate) mod commands {
1245
+ #[tauri::command]
1246
+ pub fn notify_command(app: tauri::AppHandle, title: String, body: String) {
1247
+ super::notify(&app, &title, &body);
1248
+ }
904
1249
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
- "productName": "ExampleNative",
3
+ "productName": "RubyEverywhere Shell",
4
4
  "version": "0.2.0",
5
- "identifier": "com.rubyeverywhere.example",
5
+ "identifier": "com.rubyeverywhere.shell",
6
6
  "build": {
7
7
  "frontendDist": "../splash"
8
8
  },
@@ -7,7 +7,7 @@
7
7
  # easiest way is to source your release env first:
8
8
  #
9
9
  # source ~/Projects/atlastools.dev/atlas-workspaces/.env.release
10
- # cli/support/macos/notarize.sh "example/dist/Example Notes.app"
10
+ # cli/support/release/macos/notarize.sh "example/dist/Example Notes.app"
11
11
  #
12
12
  # Required env vars (these match Tauri's names, so your existing .env works):
13
13
  # APPLE_SIGNING_IDENTITY e.g. "Developer ID Application: Your Name (TEAMID)"
@@ -33,7 +33,7 @@ APP="${1:?usage: notarize.sh /path/to/App.app}"
33
33
  # containing spaces/parens (e.g. APPLE_SIGNING_IDENTITY=Developer ID
34
34
  # Application: Name (TEAM)) that plain `source .env.release` cannot.
35
35
  #
36
- # ENV_FILE=.env.release cli/support/macos/notarize.sh "App.app"
36
+ # ENV_FILE=.env.release cli/support/release/macos/notarize.sh "App.app"
37
37
  if [ -n "${ENV_FILE:-}" ]; then
38
38
  [ -f "$ENV_FILE" ] || { echo "ENV_FILE not found: $ENV_FILE" >&2; exit 1; }
39
39
  while IFS= read -r line || [ -n "$line" ]; do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_everywhere
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrea Fomera
@@ -94,7 +94,9 @@ files:
94
94
  - lib/everywhere/blake2b.rb
95
95
  - lib/everywhere/boot.rb
96
96
  - lib/everywhere/builders/android.rb
97
+ - lib/everywhere/builders/desktop.rb
97
98
  - lib/everywhere/builders/ios.rb
99
+ - lib/everywhere/child_processes.rb
98
100
  - lib/everywhere/cli.rb
99
101
  - lib/everywhere/commands/build.rb
100
102
  - lib/everywhere/commands/clean.rb
@@ -113,12 +115,20 @@ files:
113
115
  - lib/everywhere/commands/shell_dir.rb
114
116
  - lib/everywhere/commands/updates_keygen.rb
115
117
  - lib/everywhere/config.rb
118
+ - lib/everywhere/console.rb
116
119
  - lib/everywhere/database.rb
120
+ - lib/everywhere/desktop_assets.rb
121
+ - lib/everywhere/dock.rb
122
+ - lib/everywhere/dock/footer.rb
123
+ - lib/everywhere/dock/screen.rb
124
+ - lib/everywhere/dock/state.rb
117
125
  - lib/everywhere/emulator.rb
118
126
  - lib/everywhere/engine.rb
127
+ - lib/everywhere/fatal.rb
119
128
  - lib/everywhere/framework.rb
120
129
  - lib/everywhere/icon.rb
121
130
  - lib/everywhere/ignore.rb
131
+ - lib/everywhere/line_pump.rb
122
132
  - lib/everywhere/log_filter.rb
123
133
  - lib/everywhere/minisign.rb
124
134
  - lib/everywhere/mobile_config_endpoint.rb
@@ -132,16 +142,31 @@ files:
132
142
  - lib/everywhere/png.rb
133
143
  - lib/everywhere/raster.rb
134
144
  - lib/everywhere/receipt.rb
145
+ - lib/everywhere/relay.rb
135
146
  - lib/everywhere/s3.rb
136
147
  - lib/everywhere/shellout.rb
137
148
  - lib/everywhere/simulator.rb
149
+ - lib/everywhere/task_pool.rb
138
150
  - lib/everywhere/ui.rb
139
151
  - lib/everywhere/update_manifest.rb
140
152
  - lib/everywhere/version.rb
141
153
  - lib/ruby_everywhere.rb
142
- - support/github/build.yml
143
- - support/macos/entitlements.plist
144
- - support/macos/notarize.sh
154
+ - support/desktop/README.md
155
+ - support/desktop/splash/index.html
156
+ - support/desktop/src-tauri/Cargo.lock
157
+ - support/desktop/src-tauri/Cargo.toml
158
+ - support/desktop/src-tauri/build.rs
159
+ - support/desktop/src-tauri/capabilities/default.json
160
+ - support/desktop/src-tauri/gen/schemas/acl-manifests.json
161
+ - support/desktop/src-tauri/gen/schemas/capabilities.json
162
+ - support/desktop/src-tauri/gen/schemas/desktop-schema.json
163
+ - support/desktop/src-tauri/gen/schemas/macOS-schema.json
164
+ - support/desktop/src-tauri/icons/icon.png
165
+ - support/desktop/src-tauri/src/extension_host.rs
166
+ - support/desktop/src-tauri/src/extensions/mod.rs
167
+ - support/desktop/src-tauri/src/main.rs
168
+ - support/desktop/src-tauri/src/updater.rs
169
+ - support/desktop/src-tauri/tauri.conf.json
145
170
  - support/mobile/android/README.md
146
171
  - support/mobile/android/app/build.gradle.kts
147
172
  - support/mobile/android/app/everywhere.properties
@@ -228,19 +253,8 @@ files:
228
253
  - support/mobile/ios/NativeExtensions/Package.swift
229
254
  - support/mobile/ios/NativeExtensions/Sources/NativeExtensions/Exports.swift
230
255
  - support/mobile/ios/README.md
231
- - support/shell/splash/index.html
232
- - support/shell/src-tauri/Cargo.lock
233
- - support/shell/src-tauri/Cargo.toml
234
- - support/shell/src-tauri/build.rs
235
- - support/shell/src-tauri/capabilities/default.json
236
- - support/shell/src-tauri/gen/schemas/acl-manifests.json
237
- - support/shell/src-tauri/gen/schemas/capabilities.json
238
- - support/shell/src-tauri/gen/schemas/desktop-schema.json
239
- - support/shell/src-tauri/gen/schemas/macOS-schema.json
240
- - support/shell/src-tauri/icons/icon.png
241
- - support/shell/src-tauri/src/main.rs
242
- - support/shell/src-tauri/src/updater.rs
243
- - support/shell/src-tauri/tauri.conf.json
256
+ - support/release/macos/entitlements.plist
257
+ - support/release/macos/notarize.sh
244
258
  homepage: https://rubyeverywhere.com
245
259
  licenses:
246
260
  - MIT