ruby_everywhere 0.1.2 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bbeb5f84c4ec19197b0943357aab3392040876afe678f504f717acb22201d18
4
- data.tar.gz: c48f81139c0aac8763b0c4fd9d04ace837e9131676a151d8d05fe964219ef79e
3
+ metadata.gz: 19d066bb2584933621929b270c69e6205804d07441a6e4a8bfcc052cc6483e55
4
+ data.tar.gz: c337dfa569b91412f4cb03027006d3199c9f224ebe63e5c5a066396986729194
5
5
  SHA512:
6
- metadata.gz: 4d144af27b9eaaa9ea2c11c847b41956021ea92d94929d12764f80d1b06dbfa2b43d9fbc43b18e0697c782866a1f46b9a5c5229ed8287ce086aab4c86832d83a
7
- data.tar.gz: ff6f45ab57847acd8897cf0548e3d6b076289f67524863067f1f581a48af5131b34c6da0a4ac82bcdde269c60aa9d809e93f15710395eeae49b61bc46d6fea67
6
+ metadata.gz: b0865c4cc65cfdc86706332f2d00c704fcd361ffe72ed6bbc51100297640634d246a3537b2f4ed38318fbacacbbd490ff422e4b6449905d06918ce7663dbc2f5
7
+ data.tar.gz: 119672c4cf55ea3ea05e768c3eba161ee2aa6b81b6f5f68046c6327bc99f2dcd9897658d7320e0a0d7a58cbc64d8ac457a2522d6cd71434f1df8ef974367a783
@@ -5,6 +5,7 @@ require_relative "../everywhere"
5
5
  require_relative "framework"
6
6
  require_relative "commands/build"
7
7
  require_relative "commands/dev"
8
+ require_relative "commands/icon"
8
9
  require_relative "commands/doctor"
9
10
  require_relative "commands/install"
10
11
  require_relative "commands/release"
@@ -33,6 +34,7 @@ module Everywhere
33
34
  register "install", Commands::Install
34
35
  register "dev", Commands::Dev
35
36
  register "build", Commands::Build
37
+ register "icon", Commands::Icon
36
38
  register "release", Commands::Release
37
39
 
38
40
  register "platform login", Commands::PlatformLogin
@@ -6,6 +6,7 @@ require "tmpdir"
6
6
  require "shellwords"
7
7
  require_relative "../shellout"
8
8
  require_relative "../png"
9
+ require_relative "../icon"
9
10
  require_relative "../paths"
10
11
 
11
12
  module Everywhere
@@ -28,20 +29,23 @@ module Everywhere
28
29
  option :ruby, default: "4.0.6", desc: "Ruby version to package"
29
30
  option :entry, default: "native_boot.rb", desc: "Entry point script relative to root"
30
31
  option :app_name, default: nil, desc: "Bundle name (default: capitalized app dir)"
32
+ option :target, default: "macos-arm64", desc: "Build target (os-arch) — selects any platforms: overrides"
31
33
  option :shell_dir, default: nil, desc: "Path to the shell's src-tauri directory"
32
34
  option :skip_press, type: :boolean, default: false, desc: "Reuse the existing pressed binary, only rebuild the .app"
33
35
 
34
36
  def call(root: ".", output: nil, ruby: "4.0.6", entry: "native_boot.rb",
35
- app_name: nil, shell_dir: nil, skip_press: false, **)
37
+ app_name: nil, target: "macos-arm64", shell_dir: nil, skip_press: false, **)
36
38
  root_path = File.expand_path(root)
37
39
  config = Everywhere::Config.load(root_path)
38
- app_name ||= config.name
40
+ # The os of `target` selects any per-platform overrides (bundle id,
41
+ # version, name) — local builds always target the host, macOS today.
42
+ app_name ||= config.name(target: target)
39
43
 
40
44
  # Remote mode: thin shell around an already-deployed app. No Rails app
41
45
  # required locally, nothing to press — just shell + config in a bundle.
42
46
  if config.remote?
43
47
  UI.step("#{UI.bold("remote")} mode → #{UI.cyan(config.remote_url)} #{UI.dim("(no local server packaged)")}")
44
- app_dir = bundle_app(nil, app_name, shell_dir, config, dist_dir: File.expand_path("dist", root_path))
48
+ app_dir = bundle_app(nil, app_name, shell_dir, config, target: target, dist_dir: File.expand_path("dist", root_path))
45
49
  return
46
50
  end
47
51
 
@@ -63,7 +67,7 @@ module Everywhere
63
67
  press!(framework, entry, ruby, output_path)
64
68
  end
65
69
 
66
- bundle_app(output_path, app_name, shell_dir, config) if RUBY_PLATFORM.include?("darwin")
70
+ bundle_app(output_path, app_name, shell_dir, config, target: target) if RUBY_PLATFORM.include?("darwin")
67
71
 
68
72
  UI.success("built #{output} (#{(File.size(output_path) / 1024.0 / 1024.0).round}MB)")
69
73
  end
@@ -111,7 +115,7 @@ module Everywhere
111
115
  # Assemble dist/<App Name>.app: the release shell binary + the pressed
112
116
  # Rails binary (as "app-server") + Info.plist + icon. An .app bundle is
113
117
  # just a directory layout, so we build it by hand — no tauri-cli needed.
114
- def bundle_app(server_binary, app_name, shell_dir, config, dist_dir: nil)
118
+ def bundle_app(server_binary, app_name, shell_dir, config, target: "macos-arm64", dist_dir: nil)
115
119
  shell_dir ||= Everywhere::Paths.shell_dir!
116
120
  dist_dir ||= File.dirname(server_binary)
117
121
 
@@ -122,15 +126,40 @@ module Everywhere
122
126
  end
123
127
  UI.step("app icon: #{icon_source ? icon_source.sub("#{config.root}/", "") : "placeholder"}")
124
128
 
129
+ # macOS renders icons literally — no OS-applied rounding or margins — so
130
+ # a full-bleed square looks wrong next to native apps. Bake the Big Sur
131
+ # look (inset + squircle) into a shaped 1024px master and use it for
132
+ # both the compiled Dock icon and the bundle .icns. work/ is torn down
133
+ # when the .app is assembled.
134
+ work = Dir.mktmpdir("everywhere-icon")
135
+ macos_icon = icon_source
136
+ if icon_source
137
+ shaped = File.join(work, "AppIcon-macos.png")
138
+ if Everywhere::Icon.macos_master(icon_source, shaped)
139
+ macos_icon = shaped
140
+ else
141
+ UI.warn "couldn't read #{File.basename(icon_source)} (unusual PNG flavor) — using it unshaped"
142
+ end
143
+ end
144
+
125
145
  # Tauri bakes icons/icon.png into the shell binary and sets it as the
126
146
  # Dock icon at runtime (stomping the bundle's icns after launch), so the
127
- # shell must be COMPILED with the app's icon. Swap it in for the build,
128
- # restore the placeholder after; touch build.rs so tauri-build re-embeds.
147
+ # shell must be COMPILED with the app's icon. Swap in the shaped master
148
+ # for the build, restore the placeholder after; touch build.rs so
149
+ # tauri-build re-embeds.
129
150
  shell_icon = File.join(shell_dir, "icons", "icon.png")
130
151
  placeholder_backup = "#{shell_icon}.placeholder"
131
152
  if icon_source
132
153
  FileUtils.cp(shell_icon, placeholder_backup) unless File.exist?(placeholder_backup)
133
- unless Everywhere::PNG.ensure_rgba(icon_source, shell_icon)
154
+ # The shaped master is already RGBA; fall back to raw RGBA conversion
155
+ # if shaping failed, and to the placeholder if even that fails.
156
+ dock_ok = if macos_icon == icon_source
157
+ Everywhere::PNG.ensure_rgba(icon_source, shell_icon)
158
+ else
159
+ FileUtils.cp(macos_icon, shell_icon)
160
+ true
161
+ end
162
+ unless dock_ok
134
163
  UI.warn "couldn't convert #{File.basename(icon_source)} to RGBA (unusual PNG flavor) — Dock icon will use the placeholder after launch"
135
164
  FileUtils.cp(placeholder_backup, shell_icon)
136
165
  end
@@ -162,7 +191,7 @@ module Everywhere
162
191
  FileUtils.cp(shell_bin, File.join(macos, exe_name))
163
192
  FileUtils.cp(server_binary, File.join(macos, "app-server")) if server_binary
164
193
 
165
- File.write(File.join(resources, "everywhere.json"), config.to_shell_json)
194
+ File.write(File.join(resources, "everywhere.json"), config.to_shell_json(target: target))
166
195
  if (splash = config.splash)
167
196
  if File.exist?(splash)
168
197
  FileUtils.cp(splash, File.join(resources, "splash.html"))
@@ -171,9 +200,9 @@ module Everywhere
171
200
  UI.warn "splash #{splash} not found — using the default splash"
172
201
  end
173
202
  end
174
- icns = make_icns(icon_source || File.join(shell_dir, "icons", "icon.png"), resources)
203
+ icns = make_icns(macos_icon || File.join(shell_dir, "icons", "icon.png"), resources)
175
204
  File.write(File.join(app_dir, "Contents", "Info.plist"),
176
- info_plist(app_name, exe_name, icns, config.bundle_id, config.version))
205
+ info_plist(app_name, exe_name, icns, config.bundle_id(target: target), config.version(target: target)))
177
206
 
178
207
  UI.step("signing #{File.basename(app_dir)}")
179
208
  Shellout.run!({}, "codesign", "-s", "-", "-f", File.join(macos, "app-server")) if server_binary
@@ -185,24 +214,19 @@ module Everywhere
185
214
  Shellout.run?("/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f #{app_dir.shellescape} 2>/dev/null")
186
215
 
187
216
  UI.success("app bundle: #{app_dir.sub("#{Dir.pwd}/", "")} #{UI.dim("(open it!)")}")
217
+ ensure
218
+ FileUtils.remove_entry(work) if work && File.directory?(work)
188
219
  end
189
220
 
190
- # macOS ships everything needed to make an .icns from a png.
221
+ # Write Resources/AppIcon.icns from a (shaped) PNG. Pure Ruby no sips or
222
+ # iconutil — so it produces the same icns on any build host.
191
223
  def make_icns(png, resources_dir)
192
224
  return nil unless File.exist?(png)
193
225
 
194
- require "shellwords"
195
- Dir.mktmpdir do |tmp|
196
- iconset = File.join(tmp, "App.iconset")
197
- FileUtils.mkdir_p(iconset)
198
- [16, 32, 128, 256, 512].each do |size|
199
- [[size, "icon_#{size}x#{size}.png"], [size * 2, "icon_#{size}x#{size}@2x.png"]].each do |px, name|
200
- Shellout.run?("sips -z #{px} #{px} #{png.shellescape} --out #{File.join(iconset, name).shellescape} >/dev/null 2>&1")
201
- end
202
- end
203
- out = File.join(resources_dir, "AppIcon.icns")
204
- return "AppIcon" if Shellout.run?("iconutil -c icns #{iconset.shellescape} -o #{out.shellescape} >/dev/null 2>&1")
205
- end
226
+ out = File.join(resources_dir, "AppIcon.icns")
227
+ Everywhere::Icon.write_icns(png, out) ? "AppIcon" : nil
228
+ rescue StandardError => e
229
+ UI.warn "icon generation failed (#{e.message}) — bundling without a custom icon"
206
230
  nil
207
231
  end
208
232
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "fileutils"
5
+ require_relative "../icon"
6
+ require_relative "../config"
7
+ require_relative "../ui"
8
+
9
+ module Everywhere
10
+ module Commands
11
+ # Generate platform-native app icons from a single source PNG — the same
12
+ # pipeline `every build` uses, exposed on its own for previewing and CI.
13
+ class Icon < Dry::CLI::Command
14
+ desc "Generate macOS (.icns), Windows (.ico) and Linux icons from a source PNG"
15
+
16
+ argument :source, required: false, desc: "Source PNG (default: app.icon / icon.png from everywhere.yml)"
17
+ option :root, default: ".", desc: "App root directory (to resolve the default icon)"
18
+ option :output, default: nil, desc: "Output directory (default: dist/icons)"
19
+
20
+ def call(source: nil, root: ".", output: nil, **)
21
+ root_path = File.expand_path(root)
22
+ source ||= default_source(root_path)
23
+ UI.die!("no source PNG — pass one or set app.icon in everywhere.yml") unless source
24
+ UI.die!("source PNG not found: #{source}") unless File.exist?(source)
25
+
26
+ out = File.expand_path(output || File.join(root_path, "dist", "icons"))
27
+ FileUtils.mkdir_p(out)
28
+ UI.step("generating icons from #{File.basename(source)} → #{out.sub("#{Dir.pwd}/", "")}")
29
+
30
+ master = File.join(out, "AppIcon-macos.png")
31
+ unless Everywhere::Icon.macos_master(source, master)
32
+ UI.die!("couldn't read #{File.basename(source)} — expected an 8-bit RGB/RGBA PNG")
33
+ end
34
+ UI.ok("macOS master #{rel(master, out)} #{UI.dim("(inset + squircle, 1024px)")}")
35
+
36
+ Everywhere::Icon.write_icns(master, File.join(out, "AppIcon.icns"))
37
+ UI.ok("macOS icon AppIcon.icns")
38
+
39
+ Everywhere::Icon.write_ico(source, File.join(out, "icon.ico"))
40
+ UI.ok("Windows icon icon.ico")
41
+
42
+ pngs = Everywhere::Icon.write_png_set(source, File.join(out, "linux"))
43
+ UI.ok("Linux icons linux/ #{UI.dim("(#{pngs.size} sizes)")}")
44
+
45
+ UI.success("icons written to #{out.sub("#{Dir.pwd}/", "")}")
46
+ end
47
+
48
+ private
49
+
50
+ def rel(path, base)
51
+ path.sub("#{base}/", "")
52
+ end
53
+
54
+ def default_source(root_path)
55
+ Everywhere::Config.load(root_path).icon
56
+ rescue StandardError
57
+ default = File.join(root_path, "icon.png")
58
+ File.exist?(default) ? default : nil
59
+ end
60
+ end
61
+ end
62
+ end
@@ -102,17 +102,26 @@ module Everywhere
102
102
  return change("create config/everywhere.yml", false) if File.exist?(config)
103
103
 
104
104
  app_name = File.basename(@root).split(/[-_]/).map(&:capitalize).join(" ")
105
+ slug = app_name.downcase.gsub(/[^a-z0-9]+/, "-")
105
106
  File.write(config, <<~YAML)
106
- # RubyEverywhere app configuration — https://rubyeverywhere.com
107
+ # RubyEverywhere app configuration.
108
+ # Full reference: https://rubyeverywhere.com/docs/shared/everywhere-yml
107
109
  app:
108
110
  name: #{app_name}
109
111
  # Reverse-DNS app identifier: names the app-data directory on every
110
112
  # platform (and the macOS bundle id). Set once, never change it.
111
- bundle_id: com.example.#{app_name.downcase.gsub(/[^a-z0-9]+/, "-")}
113
+ bundle_id: com.example.#{slug}
114
+ version: "0.1.0" # marketing version (CFBundleShortVersionString)
115
+ mode: local # "local" (compile & ship the app) or "remote" (native shell around a deployed site)
112
116
  entry_path: / # initial url on app launch
113
117
  # icon: icon.png # PNG used to generate app icons (defaults to icon.png at the app root if present)
114
118
  # splash: splash.html # custom boot splash (HTML); window.__EVERYWHERE_CONFIG__ carries name/colors
115
119
 
120
+ # Remote mode only — the deployed URL the native shell opens.
121
+ # Set mode: remote above, then uncomment:
122
+ # remote:
123
+ # url: https://example.com
124
+
116
125
  appearance:
117
126
  # Colors take a hex string, or split by system theme:
118
127
  # tint_color:
@@ -122,6 +131,42 @@ module Everywhere
122
131
  background_color:
123
132
  light: "#FFFFFF"
124
133
  dark: "#1C1B1A"
134
+
135
+ # Native menu-bar items — each one visits a route in your app (no native code).
136
+ # menu:
137
+ # - label: Settings…
138
+ # path: /settings
139
+ # accelerator: CmdOrCtrl+,
140
+ # - separator
141
+ # - label: Documentation
142
+ # path: /help
143
+
144
+ # System tray / menu-bar-extra items — same shape as menu, plus action: show | quit.
145
+ # tray:
146
+ # - label: Open #{app_name}
147
+ # action: show
148
+ # - label: New…
149
+ # path: /new
150
+ # - separator
151
+ # - label: Quit
152
+ # action: quit
153
+
154
+ # Durable build knobs (CLI flags still override per-run).
155
+ # build:
156
+ # ruby: "4.0.6"
157
+ # targets: # the platforms you ship — the CI matrix, expressed once
158
+ # - macos-arm64
159
+ # # - ios-arm64 # future — mobile is remote mode only
160
+ # # - android-arm64 # future — mobile is remote mode only
161
+ # permissions: [notifications, filesystem:read, clipboard]
162
+
163
+ # Per-platform overrides. Keys are platform names (macos / ios / android /
164
+ # windows / linux); anything here overrides the app: defaults above for that
165
+ # platform — handy when a store needs a distinct bundle id or version.
166
+ # platforms:
167
+ # ios:
168
+ # bundle_id: com.example.#{slug}.ios
169
+ # version: "0.1.1"
125
170
  YAML
126
171
  change("create config/everywhere.yml", true)
127
172
  end
@@ -37,7 +37,7 @@ module Everywhere
37
37
  app_name: nil, shell_dir: nil, env_file: nil, skip_build: false, **)
38
38
  root_path = File.expand_path(root)
39
39
  config = Everywhere::Config.load(root_path)
40
- app_name ||= config.name
40
+ app_name ||= config.name(target: target)
41
41
  ruby ||= config.build_ruby || "4.0.6"
42
42
 
43
43
  preflight!(target)
@@ -49,7 +49,7 @@ module Everywhere
49
49
  # notarize step below re-signs it inside-out with the Developer ID.
50
50
  unless skip_build
51
51
  UI.step("building #{UI.bold(app_name)} for #{UI.cyan(target)}")
52
- Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, shell_dir: shell_dir, **)
52
+ Build.new.call(root: root, ruby: ruby, entry: entry, app_name: app_name, target: target, shell_dir: shell_dir, **)
53
53
  end
54
54
 
55
55
  app_dir = Dir[File.join(dist_dir, "*.app")].max_by { |d| File.mtime(d) }
@@ -31,15 +31,23 @@ module Everywhere
31
31
  @root = root
32
32
  end
33
33
 
34
- def name
35
- app["name"] || File.basename(File.expand_path(root)).split(/[-_]/).map(&:capitalize).join(" ")
34
+ # Every target (os-arch string) resolves to a single platform (its os), and
35
+ # a platform may override the app-level identity/version keys — see
36
+ # `platforms:` below. Pass `target:` to any of these accessors to apply the
37
+ # matching override; omit it for the shared app-level value.
38
+ def self.os_of(target) = target.to_s.split("-", 2).first
39
+
40
+ def name(target: nil)
41
+ resolved(target)["name"] || File.basename(File.expand_path(root)).split(/[-_]/).map(&:capitalize).join(" ")
36
42
  end
37
43
 
38
44
  # Reverse-DNS identifier: macOS bundle id, and the name of the per-user
39
45
  # app-data directory on every platform. Set it explicitly and never change
40
- # it — renaming moves users' data.
41
- def bundle_id
42
- app["bundle_id"] || "com.rubyeverywhere.#{name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")}"
46
+ # it — renaming moves users' data. A platform may override it (e.g. the App
47
+ # Store often wants `com.example.app.ios`).
48
+ def bundle_id(target: nil)
49
+ resolved(target)["bundle_id"] ||
50
+ "com.rubyeverywhere.#{name(target: target).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")}"
43
51
  end
44
52
 
45
53
  # App icon source: a PNG (ideally square, 1024px+). Explicit `app.icon`
@@ -67,9 +75,11 @@ module Everywhere
67
75
  end
68
76
 
69
77
  # Marketing/display version of the app (CFBundleShortVersionString, and the
70
- # `version` recorded in the build receipt). Defaults conservatively.
71
- def version
72
- app["version"] || "0.1.0"
78
+ # `version` recorded in the build receipt). One shared `app.version` is the
79
+ # default for every target; a platform may override it when a store forces a
80
+ # different number. Defaults conservatively.
81
+ def version(target: nil)
82
+ resolved(target)["version"] || "0.1.0"
73
83
  end
74
84
 
75
85
  # The `build:` section — durable build knobs that were once `every build`
@@ -149,11 +159,13 @@ module Everywhere
149
159
  end
150
160
 
151
161
  # The subset the shell needs, shipped as JSON (env var in dev,
152
- # Resources/everywhere.json inside a bundled .app).
153
- def to_shell_hash
162
+ # Resources/everywhere.json inside a bundled .app). Pass `target:` so the
163
+ # packaged config reflects the platform being built (its bundle id / name).
164
+ def to_shell_hash(target: nil)
154
165
  {
155
- "name" => name,
156
- "bundle_id" => bundle_id,
166
+ "name" => name(target: target),
167
+ "bundle_id" => bundle_id(target: target),
168
+ "version" => version(target: target),
157
169
  "mode" => mode,
158
170
  "remote_url" => remote_url,
159
171
  "entry_path" => entry_path,
@@ -164,9 +176,9 @@ module Everywhere
164
176
  }.compact
165
177
  end
166
178
 
167
- def to_shell_json
179
+ def to_shell_json(target: nil)
168
180
  require "json"
169
- JSON.generate(to_shell_hash)
181
+ JSON.generate(to_shell_hash(target: target))
170
182
  end
171
183
 
172
184
  private
@@ -174,6 +186,19 @@ module Everywhere
174
186
  def app = @data.fetch("app", nil) || {}
175
187
  def appearance = @data.fetch("appearance", nil) || {}
176
188
 
189
+ # Per-platform overrides, keyed by os (macos / ios / android / windows /
190
+ # linux). Each value overrides the matching `app:` keys for that platform.
191
+ def platforms = @data.fetch("platforms", nil) || {}
192
+
193
+ # The app-level keys with the target's platform override merged on top.
194
+ # Blank override values are dropped (`.compact`) so they don't erase a
195
+ # default. Without a target — or without an override for that os — this is
196
+ # just the plain `app:` hash.
197
+ def resolved(target)
198
+ override = target && platforms[self.class.os_of(target)]
199
+ override.is_a?(Hash) ? app.merge(override.compact) : app
200
+ end
201
+
177
202
  def normalize_color(value)
178
203
  case value
179
204
  when String
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "raster"
5
+
6
+ module Everywhere
7
+ # Turns one source PNG into platform-native app icons — no Tauri, no sips, no
8
+ # iconutil, no ImageMagick. Pure Ruby, so it runs the same locally and on the
9
+ # cloud build runners.
10
+ #
11
+ # The important part is macОS shaping. macOS renders an .icns *literally*: it
12
+ # does not round the corners or add margins the way iOS does. A full-bleed
13
+ # square PNG therefore shows up as a hard square, out of place next to every
14
+ # native app. So we bake the platform's look into the artwork ourselves —
15
+ # inset the icon to the Big Sur grid (~824px inside a 1024px canvas) and clip
16
+ # it to a superellipse ("squircle") with transparent corners.
17
+ #
18
+ # Windows (.ico) and Linux (hicolor PNGs) don't get the squircle — those
19
+ # desktops present square icons — so they're generated straight from the
20
+ # full-bleed source.
21
+ module Icon
22
+ # Big Sur icon-grid geometry, all as fractions of the 1024px canvas so it
23
+ # scales to any master size.
24
+ CANVAS = 1024
25
+ INSET = 0.8047 # icon body is 824/1024 of the canvas
26
+ CORNER_RATIO = 0.2237 # corner radius as a fraction of the body's side
27
+ EXPONENT = 5.0 # superellipse power — the iOS/macOS "squircle" corner
28
+
29
+ # .icns type codes -> pixel size. Mirrors what `iconutil` emits from a
30
+ # standard .iconset (retina variants included), so the result is
31
+ # indistinguishable from an Apple-toolchain icns.
32
+ ICNS_TYPES = [
33
+ ["icp4", 16], ["icp5", 32], ["ic07", 128], ["ic08", 256], ["ic09", 512],
34
+ ["ic11", 32], ["ic12", 64], ["ic13", 256], ["ic14", 512], ["ic10", 1024]
35
+ ].freeze
36
+
37
+ ICO_SIZES = [16, 32, 48, 64, 128, 256].freeze
38
+ LINUX_SIZES = [16, 32, 48, 64, 128, 256, 512].freeze
39
+
40
+ module_function
41
+
42
+ # Write a macOS-shaped 1024px RGBA master PNG (padding + squircle) from a
43
+ # source PNG. Returns true, or false if the source PNG can't be read.
44
+ def macos_master(source, dest, canvas: CANVAS)
45
+ raster = Raster.decode_png(source) or return false
46
+
47
+ body = (canvas * INSET).round
48
+ offset = (canvas - body) / 2
49
+
50
+ # Fit the artwork inside the body square preserving aspect ratio (square
51
+ # sources fill it exactly; non-square ones are centred, not stretched).
52
+ fit = [body.to_f / raster.width, body.to_f / raster.height].min
53
+ sw = (raster.width * fit).round.clamp(1, body)
54
+ sh = (raster.height * fit).round.clamp(1, body)
55
+ scaled = raster.resample(sw, sh)
56
+
57
+ out = Raster.transparent(canvas, canvas)
58
+ out.paste!(scaled, offset + (body - sw) / 2, offset + (body - sh) / 2)
59
+
60
+ centre = canvas / 2.0
61
+ half = body / 2.0
62
+ radius = [body * CORNER_RATIO, half].min
63
+ straight = half - radius # half-extent of the straight edges
64
+ out.mask_region!(offset, offset, offset + body, offset + body) do |x, y|
65
+ squircle_coverage(x, y, centre, half, straight, radius, EXPONENT)
66
+ end
67
+
68
+ out.write_png(dest)
69
+ true
70
+ end
71
+
72
+ # Build a macOS .icns from an already-shaped master PNG (ideally 1024px).
73
+ # Returns true on success.
74
+ def write_icns(master_png, dest)
75
+ master = Raster.decode_png(master_png) or return false
76
+
77
+ body = ICNS_TYPES.map do |type, size|
78
+ png = sized_png(master, size)
79
+ type.b + [png.bytesize + 8].pack("N") + png
80
+ end.join
81
+
82
+ File.binwrite(dest, "icns".b + [body.bytesize + 8].pack("N") + body)
83
+ true
84
+ end
85
+
86
+ # Build a Windows .ico (PNG-compressed entries, Vista+) from a full-bleed
87
+ # source PNG. Returns true on success.
88
+ def write_ico(source, dest, sizes: ICO_SIZES)
89
+ master = Raster.decode_png(source) or return false
90
+
91
+ images = sizes.map { |s| [s, sized_png(master, s)] }
92
+ offset = 6 + images.size * 16
93
+ entries = +"".b
94
+ blob = +"".b
95
+ images.each do |size, png|
96
+ dim = size >= 256 ? 0 : size # 0 means 256 in the ICONDIRENTRY
97
+ entries << [dim, dim, 0, 0, 1, 32, png.bytesize, offset].pack("CCCCvvVV")
98
+ blob << png
99
+ offset += png.bytesize
100
+ end
101
+
102
+ header = [0, 1, images.size].pack("vvv") # reserved, type=icon, count
103
+ File.binwrite(dest, header + entries + blob)
104
+ true
105
+ end
106
+
107
+ # Write a set of square PNGs (Linux/desktop use) named "<size>x<size>.png"
108
+ # into dest_dir from a full-bleed source. Returns the written paths.
109
+ def write_png_set(source, dest_dir, sizes: LINUX_SIZES)
110
+ master = Raster.decode_png(source) or return []
111
+
112
+ FileUtils.mkdir_p(dest_dir)
113
+ sizes.map do |size|
114
+ path = File.join(dest_dir, "#{size}x#{size}.png")
115
+ File.binwrite(path, sized_png(master, size))
116
+ path
117
+ end
118
+ end
119
+
120
+ # PNG bytes of `master` at `size`x`size` (no-op resample when already sized).
121
+ def sized_png(master, size)
122
+ (size == master.width && size == master.height ? master : master.resample(size, size)).encode_png
123
+ end
124
+
125
+ # Coverage (0.0..1.0) of pixel (x, y) by the squircle: a rounded rectangle
126
+ # with continuous (superelliptical) corners. Interior pixels return 1.0,
127
+ # exterior 0.0, and boundary pixels are antialiased by 4x4 supersampling.
128
+ def squircle_coverage(x, y, centre, half, straight, radius, exponent)
129
+ c0 = squircle_inside?(x, y, centre, half, straight, radius, exponent)
130
+ c1 = squircle_inside?(x + 1, y, centre, half, straight, radius, exponent)
131
+ c2 = squircle_inside?(x, y + 1, centre, half, straight, radius, exponent)
132
+ c3 = squircle_inside?(x + 1, y + 1, centre, half, straight, radius, exponent)
133
+ return 1.0 if c0 && c1 && c2 && c3
134
+ return 0.0 unless c0 || c1 || c2 || c3
135
+
136
+ hit = 0
137
+ 4.times do |i|
138
+ 4.times do |j|
139
+ hit += 1 if squircle_inside?(x + (i + 0.5) / 4.0, y + (j + 0.5) / 4.0,
140
+ centre, half, straight, radius, exponent)
141
+ end
142
+ end
143
+ hit / 16.0
144
+ end
145
+
146
+ # Is the continuous point (sx, sy) inside the squircle? Straight edges up to
147
+ # `straight` from centre, superelliptical corners of `radius` beyond that.
148
+ def squircle_inside?(sx, sy, centre, half, straight, radius, exponent)
149
+ u = (sx - centre).abs
150
+ v = (sy - centre).abs
151
+ return false if u > half || v > half
152
+ return true if u <= straight || v <= straight
153
+
154
+ du = (u - straight) / radius
155
+ dv = (v - straight) / radius
156
+ (du**exponent) + (dv**exponent) <= 1.0
157
+ end
158
+ end
159
+ end
@@ -39,6 +39,60 @@ module Everywhere
39
39
  true
40
40
  end
41
41
 
42
+ # Decode an 8-bit RGB/RGBA PNG to a flat RGBA pixel buffer. Returns
43
+ # [width, height, bytes] (bytes = width*height*4, no per-scanline filter
44
+ # byte) or nil for flavors we don't handle (palette, 16-bit, interlaced,
45
+ # grayscale). This is the pixel gateway the Raster/Icon generators build on.
46
+ def decode_rgba(path)
47
+ data = File.binread(path)
48
+ return nil unless data.start_with?(SIGNATURE)
49
+
50
+ chunks = read_chunks(data)
51
+ ihdr = chunks.find { |type, _| type == "IHDR" }&.last or return nil
52
+ width, height, depth, color, _comp, _filter, interlace = ihdr.unpack("NNC5")
53
+ return nil unless depth == 8 && interlace.zero?
54
+ return nil unless color == 2 || color == 6 # RGB or RGBA
55
+
56
+ bpp = color == 6 ? 4 : 3
57
+ raw = Zlib::Inflate.inflate(chunks.select { |t, _| t == "IDAT" }.map(&:last).join)
58
+ stride = width * bpp
59
+ prev = "\0".b * stride
60
+ out = String.new(capacity: width * height * 4)
61
+
62
+ height.times do |y|
63
+ filter = raw.getbyte(y * (stride + 1))
64
+ line = raw.byteslice(y * (stride + 1) + 1, stride).dup
65
+ unfilter!(line, prev, filter, bpp)
66
+ if bpp == 4
67
+ out << line
68
+ else
69
+ x = 0
70
+ while x < width
71
+ out << line.byteslice(x * 3, 3) << "\xFF".b
72
+ x += 1
73
+ end
74
+ end
75
+ prev = line
76
+ end
77
+ [width, height, out]
78
+ end
79
+
80
+ # Encode a flat RGBA pixel buffer to PNG bytes (filter 0, single IDAT).
81
+ def encode_rgba(width, height, rgba)
82
+ stride = width * 4
83
+ raw = String.new(capacity: (stride + 1) * height)
84
+ height.times do |y|
85
+ raw << "\0".b # filter: None
86
+ raw << rgba.byteslice(y * stride, stride)
87
+ end
88
+
89
+ out = SIGNATURE.dup
90
+ out << chunk("IHDR", [width, height, 8, 6, 0, 0, 0].pack("NNC5"))
91
+ out << chunk("IDAT", Zlib::Deflate.deflate(raw, Zlib::BEST_COMPRESSION))
92
+ out << chunk("IEND", "")
93
+ out
94
+ end
95
+
42
96
  def read_chunks(data)
43
97
  chunks = []
44
98
  pos = 8
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "png"
4
+
5
+ module Everywhere
6
+ # A mutable RGBA raster and the handful of pixel operations the icon
7
+ # generator needs — decode/encode PNG, high-quality resampling, pasting, and
8
+ # a superellipse ("squircle") alpha mask. Pure Ruby (zlib only): no
9
+ # ImageMagick, no sips, no Tauri. Works the same on any OS the CLI runs on.
10
+ #
11
+ # Pixels are stored as a flat 8-bit RGBA byte string (row-major, 4 bytes per
12
+ # pixel). Resampling is a separable area/box filter done in *premultiplied*
13
+ # alpha so transparent regions never bleed their colour into opaque edges.
14
+ class Raster
15
+ attr_reader :width, :height, :pixels
16
+
17
+ def initialize(width, height, pixels = nil)
18
+ @width = width
19
+ @height = height
20
+ @pixels = pixels || ("\x00".b * (width * height * 4))
21
+ end
22
+
23
+ # A fully transparent canvas.
24
+ def self.transparent(width, height)
25
+ new(width, height)
26
+ end
27
+
28
+ # Decode a PNG file into a Raster, or nil if it's a flavor PNG.decode_rgba
29
+ # can't read (palette, 16-bit, interlaced, grayscale).
30
+ def self.decode_png(path)
31
+ width, height, bytes = PNG.decode_rgba(path)
32
+ return nil unless width
33
+
34
+ new(width, height, bytes)
35
+ end
36
+
37
+ def encode_png
38
+ PNG.encode_rgba(width, height, pixels)
39
+ end
40
+
41
+ def write_png(path)
42
+ File.binwrite(path, encode_png)
43
+ end
44
+
45
+ # Resample to a new size with a separable area filter. Great for the
46
+ # downscales an icon set is made of; degrades gracefully on upscale.
47
+ def resample(new_width, new_height)
48
+ return self if new_width == width && new_height == height
49
+
50
+ # Premultiplied float channels: pr = r * (a/255), etc.
51
+ pr = Array.new(width * height)
52
+ pg = Array.new(width * height)
53
+ pb = Array.new(width * height)
54
+ pa = Array.new(width * height)
55
+ i = 0
56
+ (width * height).times do |p|
57
+ a = pixels.getbyte(i + 3)
58
+ af = a / 255.0
59
+ pr[p] = pixels.getbyte(i) * af
60
+ pg[p] = pixels.getbyte(i + 1) * af
61
+ pb[p] = pixels.getbyte(i + 2) * af
62
+ pa[p] = a.to_f
63
+ i += 4
64
+ end
65
+
66
+ # Horizontal pass (width -> new_width), then vertical (height -> new_height).
67
+ wx = self.class.axis_weights(width, new_width)
68
+ hr = resample_axis(pr, width, height, new_width, wx, rows: true)
69
+ hg = resample_axis(pg, width, height, new_width, wx, rows: true)
70
+ hb = resample_axis(pb, width, height, new_width, wx, rows: true)
71
+ ha = resample_axis(pa, width, height, new_width, wx, rows: true)
72
+
73
+ wy = self.class.axis_weights(height, new_height)
74
+ vr = resample_axis(hr, new_width, height, new_height, wy, rows: false)
75
+ vg = resample_axis(hg, new_width, height, new_height, wy, rows: false)
76
+ vb = resample_axis(hb, new_width, height, new_height, wy, rows: false)
77
+ va = resample_axis(ha, new_width, height, new_height, wy, rows: false)
78
+
79
+ out = String.new(capacity: new_width * new_height * 4)
80
+ (new_width * new_height).times do |p|
81
+ a = va[p]
82
+ if a <= 0.001
83
+ out << "\x00\x00\x00\x00".b
84
+ else
85
+ out << [
86
+ (vr[p] * 255.0 / a).round.clamp(0, 255),
87
+ (vg[p] * 255.0 / a).round.clamp(0, 255),
88
+ (vb[p] * 255.0 / a).round.clamp(0, 255),
89
+ a.round.clamp(0, 255)
90
+ ].pack("C4")
91
+ end
92
+ end
93
+ self.class.new(new_width, new_height, out)
94
+ end
95
+
96
+ # Copy src into this raster with its top-left at (ox, oy), replacing pixels
97
+ # (the canvas is transparent underneath, so a plain copy is the composite).
98
+ def paste!(src, ox, oy)
99
+ row = src.width * 4
100
+ src.height.times do |y|
101
+ dst_index = ((oy + y) * width + ox) * 4
102
+ pixels[dst_index, row] = src.pixels.byteslice(y * row, row)
103
+ end
104
+ self
105
+ end
106
+
107
+ # Multiply the alpha channel of every pixel in [x0, x1) x [y0, y1) by the
108
+ # coverage the block returns for pixel (x, y) — a value in 0.0..1.0.
109
+ # Pixels outside the range keep their (already transparent) alpha.
110
+ def mask_region!(x0, y0, x1, y1)
111
+ y0.upto(y1 - 1) do |y|
112
+ base = y * width
113
+ x0.upto(x1 - 1) do |x|
114
+ cov = yield(x, y)
115
+ next if cov >= 1.0
116
+
117
+ idx = (base + x) * 4 + 3
118
+ a = pixels.getbyte(idx)
119
+ next if a.zero?
120
+
121
+ pixels.setbyte(idx, cov <= 0.0 ? 0 : (a * cov).round)
122
+ end
123
+ end
124
+ self
125
+ end
126
+
127
+ # For each output index along an axis of `n_out` pixels, the list of
128
+ # [input_index, weight] contributions, where weights sum to 1 (area filter:
129
+ # each output pixel is the average of the input pixels it overlaps).
130
+ def self.axis_weights(n_in, n_out)
131
+ scale = n_in.to_f / n_out
132
+ Array.new(n_out) do |i|
133
+ start = i * scale
134
+ finish = [start + scale, n_in.to_f].min # float slop can push this past n_in
135
+ weights = []
136
+ k = [start.floor, 0].max
137
+ while k < finish && k < n_in
138
+ lo = [k.to_f, start].max
139
+ hi = [k + 1.0, finish].min
140
+ overlap = hi - lo
141
+ weights << [k, overlap / scale] if overlap > 0
142
+ k += 1
143
+ end
144
+ weights
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ # Resample one float channel along a single axis. `rows: true` resizes each
151
+ # row from `n_in` to `n_out` columns; `rows: false` resizes each column.
152
+ def resample_axis(src, in_w, in_h, n_out, weights, rows:)
153
+ if rows
154
+ out = Array.new(n_out * in_h, 0.0)
155
+ in_h.times do |y|
156
+ src_base = y * in_w
157
+ out_base = y * n_out
158
+ n_out.times do |o|
159
+ acc = 0.0
160
+ weights[o].each { |k, wt| acc += src[src_base + k] * wt }
161
+ out[out_base + o] = acc
162
+ end
163
+ end
164
+ out
165
+ else
166
+ out = Array.new(in_w * n_out, 0.0)
167
+ in_w.times do |x|
168
+ n_out.times do |o|
169
+ acc = 0.0
170
+ weights[o].each { |k, wt| acc += src[k * in_w + x] * wt }
171
+ out[o * in_w + x] = acc
172
+ end
173
+ end
174
+ out
175
+ end
176
+ end
177
+ end
178
+ end
@@ -27,6 +27,7 @@ module Everywhere
27
27
  @config = config
28
28
  @framework = framework
29
29
  @ruby = ruby
30
+ @target_str = target # raw "os-arch" — selects platforms: overrides
30
31
  @target = parse_target(target, channel)
31
32
  @shell_dir = shell_dir
32
33
  end
@@ -35,9 +36,9 @@ module Everywhere
35
36
  def manifest
36
37
  {
37
38
  "app" => {
38
- "name" => @config.name,
39
- "bundle_id" => @config.bundle_id,
40
- "version" => @config.version
39
+ "name" => @config.name(target: @target_str),
40
+ "bundle_id" => @config.bundle_id(target: @target_str),
41
+ "version" => @config.version(target: @target_str)
41
42
  },
42
43
  "framework" => framework_info,
43
44
  "ruby" => @ruby,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Everywhere
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
 
6
6
  # Version of the vendored @rubyeverywhere/bridge JS this gem ships. Tracks
7
7
  # bridge/package.json — bump it whenever lib/everywhere/javascript/bridge.js is
@@ -993,6 +993,9 @@ dependencies = [
993
993
  name = "example-shell"
994
994
  version = "0.1.0"
995
995
  dependencies = [
996
+ "objc2",
997
+ "objc2-foundation",
998
+ "objc2-web-kit",
996
999
  "serde_json",
997
1000
  "tauri",
998
1001
  "tauri-build",
@@ -2366,6 +2369,16 @@ dependencies = [
2366
2369
  "objc2-core-foundation",
2367
2370
  ]
2368
2371
 
2372
+ [[package]]
2373
+ name = "objc2-javascript-core"
2374
+ version = "0.3.2"
2375
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2376
+ checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
2377
+ dependencies = [
2378
+ "objc2",
2379
+ "objc2-core-foundation",
2380
+ ]
2381
+
2369
2382
  [[package]]
2370
2383
  name = "objc2-quartz-core"
2371
2384
  version = "0.3.2"
@@ -2378,6 +2391,17 @@ dependencies = [
2378
2391
  "objc2-foundation",
2379
2392
  ]
2380
2393
 
2394
+ [[package]]
2395
+ name = "objc2-security"
2396
+ version = "0.3.2"
2397
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2398
+ checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
2399
+ dependencies = [
2400
+ "bitflags 2.13.0",
2401
+ "objc2",
2402
+ "objc2-core-foundation",
2403
+ ]
2404
+
2381
2405
  [[package]]
2382
2406
  name = "objc2-ui-kit"
2383
2407
  version = "0.3.2"
@@ -2421,6 +2445,8 @@ dependencies = [
2421
2445
  "objc2-app-kit",
2422
2446
  "objc2-core-foundation",
2423
2447
  "objc2-foundation",
2448
+ "objc2-javascript-core",
2449
+ "objc2-security",
2424
2450
  ]
2425
2451
 
2426
2452
  [[package]]
@@ -14,6 +14,15 @@ tauri-plugin-notification = "2"
14
14
  tauri-plugin-dialog = "2"
15
15
  tauri-plugin-clipboard-manager = "2"
16
16
 
17
+ # macOS: set the WebView's `applicationNameForUserAgent`, which WKWebView
18
+ # natively APPENDS to its real default UA — so we get the true system UA plus
19
+ # our marker without ever reading or hardcoding the base string. Versions are
20
+ # pinned to match what wry 0.55 already resolves, so no duplicate objc2 trees.
21
+ [target.'cfg(target_os = "macos")'.dependencies]
22
+ objc2 = "0.6"
23
+ objc2-foundation = { version = "0.3", features = ["NSString", "NSUUID", "NSProcessInfo"] }
24
+ objc2-web-kit = { version = "0.3", features = ["WKWebViewConfiguration", "WKWebsiteDataStore"] }
25
+
17
26
  [profile.release]
18
27
  strip = true
19
28
  lto = true
@@ -32,6 +32,7 @@ struct AppConfig {
32
32
  raw_json: String,
33
33
  name: String,
34
34
  bundle_id: Option<String>,
35
+ version: Option<String>,
35
36
  entry_path: String,
36
37
  remote_url: Option<String>,
37
38
  background_light: Option<(u8, u8, u8)>,
@@ -71,6 +72,7 @@ fn load_config() -> AppConfig {
71
72
  raw_json: if value.is_null() { "{}".to_string() } else { value.to_string() },
72
73
  name: value["name"].as_str().unwrap_or("RubyEverywhere App").to_string(),
73
74
  bundle_id: value["bundle_id"].as_str().map(str::to_string),
75
+ version: value["version"].as_str().map(str::to_string),
74
76
  entry_path: value["entry_path"].as_str().unwrap_or("/").to_string(),
75
77
  remote_url: (value["mode"].as_str() == Some("remote"))
76
78
  .then(|| value["remote_url"].as_str().map(str::to_string))
@@ -549,12 +551,72 @@ const INIT_SCRIPT: &str = r#"
549
551
  })();
550
552
  "#;
551
553
 
554
+ // The marker we append to the WebView's own user agent. We never build or
555
+ // hardcode the browser part of the UA — the WebView keeps producing its real
556
+ // default and this is added on the end, so anything the Ruby side sniffs (the
557
+ // `browser` gem, `request.variant`) still sees a genuine desktop browser. The
558
+ // contract with the app: `request.user_agent =~ %r{RubyEverywhere/([\d.]+)}`
559
+ // tells it the request came from the native shell and which app version (from
560
+ // everywhere.yml → the submitted build) made it. Version is absent only in bare
561
+ // `cargo run` dev where no config was passed.
562
+ fn ua_marker(config: &AppConfig) -> String {
563
+ let os = std::env::consts::OS;
564
+ match &config.version {
565
+ Some(v) => format!("RubyEverywhere/{v} ({os})"),
566
+ None => format!("RubyEverywhere ({os})"),
567
+ }
568
+ }
569
+
570
+ // macOS: build the WKWebViewConfiguration ourselves so we can set
571
+ // `applicationNameForUserAgent` — WebKit appends it to the real default UA (the
572
+ // one clean, no-read way to *append* rather than replace). Because wry uses the
573
+ // config's own `websiteDataStore` when we supply a configuration (and ignores
574
+ // Tauri's `data_store_identifier`), we must reproduce the per-app persistent
575
+ // store here too, or logins would leak into the shared default store. Mirrors
576
+ // wry's own logic: identified store on macOS 14+, default otherwise / in dev.
577
+ #[cfg(target_os = "macos")]
578
+ fn macos_webview_configuration(
579
+ config: &AppConfig,
580
+ dev_mode: bool,
581
+ ) -> objc2::rc::Retained<objc2_web_kit::WKWebViewConfiguration> {
582
+ use objc2::MainThreadMarker;
583
+ use objc2_foundation::{NSProcessInfo, NSString, NSUUID, NSOperatingSystemVersion};
584
+ use objc2_web_kit::{WKWebViewConfiguration, WKWebsiteDataStore};
585
+
586
+ let mtm = MainThreadMarker::new().expect("webview configuration must be built on the main thread");
587
+ let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
588
+
589
+ unsafe {
590
+ cfg.setApplicationNameForUserAgent(Some(&NSString::from_str(&ua_marker(config))));
591
+ }
592
+
593
+ // Cookie / session persistence, keyed by bundle_id so each app is isolated.
594
+ // Dev mode keeps the default store (matches the previous ephemeral-ish dev
595
+ // behaviour). dataStoreForIdentifier needs macOS 14+.
596
+ if !dev_mode {
597
+ let os14 = NSProcessInfo::processInfo().isOperatingSystemAtLeastVersion(
598
+ NSOperatingSystemVersion { majorVersion: 14, minorVersion: 0, patchVersion: 0 },
599
+ );
600
+ if os14 {
601
+ let id = config.bundle_id.as_deref().unwrap_or("com.rubyeverywhere.app");
602
+ let store = unsafe {
603
+ WKWebsiteDataStore::dataStoreForIdentifier(&NSUUID::from_bytes(data_store_id(id)), mtm)
604
+ };
605
+ unsafe { cfg.setWebsiteDataStore(&store) };
606
+ }
607
+ }
608
+
609
+ cfg
610
+ }
611
+
552
612
  fn build_window(
553
613
  app: &tauri::App,
554
614
  config: &AppConfig,
555
615
  title: &str,
556
616
  url: WebviewUrl,
557
617
  ) -> tauri::Result<tauri::WebviewWindow> {
618
+ let dev_mode = std::env::var("NATIVE_DEV_URL").is_ok();
619
+
558
620
  let mut builder = WebviewWindowBuilder::new(app, "main", url)
559
621
  .title(title)
560
622
  .inner_size(1100.0, 750.0)
@@ -566,23 +628,19 @@ fn build_window(
566
628
  }
567
629
 
568
630
  // Cookie / session persistence: give the webview a stable data store so
569
- // logins survive relaunches. macOS wants a 16-byte identifier (derived from
570
- // bundle_id); Windows/Linux want an explicit directory. Dev mode stays
571
- // ephemeral — a persistent cache would keep serving stale dev assets.
572
- let dev_mode = std::env::var("NATIVE_DEV_URL").is_ok();
631
+ // logins survive relaunches. macOS carries it (and the UA marker) on a
632
+ // custom WKWebViewConfiguration; Windows/Linux want an explicit directory.
633
+ // Dev mode stays ephemeral — a persistent cache would keep serving stale
634
+ // dev assets.
635
+ #[cfg(target_os = "macos")]
636
+ {
637
+ builder = builder.with_webview_configuration(macos_webview_configuration(config, dev_mode));
638
+ }
639
+ #[cfg(any(windows, target_os = "linux"))]
573
640
  if !dev_mode {
574
- #[cfg(target_os = "macos")]
575
- {
576
- builder = builder.data_store_identifier(data_store_id(
577
- config.bundle_id.as_deref().unwrap_or("com.rubyeverywhere.app"),
578
- ));
579
- }
580
- #[cfg(any(windows, target_os = "linux"))]
581
- {
582
- if let Ok(dir) = app.path().data_dir() {
583
- let id = config.bundle_id.clone().unwrap_or("com.rubyeverywhere.app".into());
584
- builder = builder.data_directory(dir.join(id).join("webview"));
585
- }
641
+ if let Ok(dir) = app.path().data_dir() {
642
+ let id = config.bundle_id.clone().unwrap_or("com.rubyeverywhere.app".into());
643
+ builder = builder.data_directory(dir.join(id).join("webview"));
586
644
  }
587
645
  }
588
646
 
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.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrea Fomera
@@ -55,6 +55,7 @@ files:
55
55
  - lib/everywhere/commands/build.rb
56
56
  - lib/everywhere/commands/dev.rb
57
57
  - lib/everywhere/commands/doctor.rb
58
+ - lib/everywhere/commands/icon.rb
58
59
  - lib/everywhere/commands/install.rb
59
60
  - lib/everywhere/commands/platform/auth_status.rb
60
61
  - lib/everywhere/commands/platform/build.rb
@@ -65,6 +66,7 @@ files:
65
66
  - lib/everywhere/config.rb
66
67
  - lib/everywhere/database.rb
67
68
  - lib/everywhere/framework.rb
69
+ - lib/everywhere/icon.rb
68
70
  - lib/everywhere/ignore.rb
69
71
  - lib/everywhere/javascript/bridge.js
70
72
  - lib/everywhere/paths.rb
@@ -72,6 +74,7 @@ files:
72
74
  - lib/everywhere/platform/credentials.rb
73
75
  - lib/everywhere/platform/snapshot.rb
74
76
  - lib/everywhere/png.rb
77
+ - lib/everywhere/raster.rb
75
78
  - lib/everywhere/receipt.rb
76
79
  - lib/everywhere/shellout.rb
77
80
  - lib/everywhere/ui.rb