Class: Everywhere::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/everywhere/config.rb

Overview

Loads config/everywhere.yml:

app:
name: My Really Awesome App
entry_path: /dashboard        # initial url on app launch
appearance:
tint_color: "#CC342D"         # a hex string...
background_color:             # ...or split by system theme
  light: "#FAF9F7"
  dark: "#1C1B1A"

Colors always normalize to { "light" => ..., "dark" => ... }.

Constant Summary collapse

FILE =
File.join("config", "everywhere.yml")
DEFAULT_TAB_ICONS =
{ "ios" => "circle", "android" => "circle" }.freeze
MOBILE_PATH_RULES =

The Hotwire Native path-configuration rules every RubyEverywhere mobile shell starts from. Single source of truth: the builder bakes this into the app bundle and MobileConfigEndpoint serves it live.

[
  { "patterns" => [".*"],
    "properties" => { "context" => "default", "pull_to_refresh_enabled" => true } },
  { "patterns" => ["/new$", "/edit$"],
    "properties" => { "context" => "modal", "pull_to_refresh_enabled" => false } }
].freeze
MOBILE_PERMISSIONS =

Native permissions the app declares (top-level permissions:). Only declared permissions can be requested at runtime — the shell answers undeclared for everything else, so an app that doesn't need a permission can never prompt for it. For camera and location the value is the user-facing usage string iOS shows in the permission prompt (the "why"); it's mandatory there because requesting without one crashes the app. Notifications need no string — declare with true.

permissions:
notifications: true
camera: "Scan QR codes to pair devices."
location:
  ios: "Find build agents near you."
biometrics: "Unlock your account with Face ID."

Returns { name => { "ios" => usage-or-nil, ... } }.

%w[notifications camera location biometrics].freeze
IOS_USAGE_KEYS =

Info.plist usage-description keys per permission. Presence here makes the usage string mandatory on iOS.

{
  "camera" => "NSCameraUsageDescription",
  "location" => "NSLocationWhenInUseUsageDescription",
  "biometrics" => "NSFaceIDUsageDescription"
}.freeze
ANDROID_PERMISSIONS =

Manifest names per permission, stamped into src/stamped/AndroidManifest.xml. Deliberately not the mirror of IOS_USAGE_KEYS: presence here mandates nothing, because Android has no Info.plist-style contract — a runtime request without a rationale string prompts normally instead of terminating the process, and the rationale is a dialog the app draws itself, not a manifest value. So the map only answers "which manifest entry does this permission need", and every declared name has one.

ACCESS_FINE_LOCATION implies ACCESS_COARSE_LOCATION on API 31+ only when both are declared, but the shell asks for precise location, so the fine permission alone is the honest declaration.

{
  "notifications" => "android.permission.POST_NOTIFICATIONS",
  "camera" => "android.permission.CAMERA",
  "location" => "android.permission.ACCESS_FINE_LOCATION",
  "biometrics" => "android.permission.USE_BIOMETRIC"
}.freeze
PACKAGE_REQUIREMENT_KEYS =

Third-party Swift packages this app pins for its native/ios code. Each entry is one SPM dependency; every build writes them into the shell's local NativeExtensions package and re-exports every product, so native/ios code reaches them with a single import NativeExtensions.

packages:
- url: https://github.com/simibac/ConfettiSwiftUI
  from: "1.1.0"                 # or exact: / branch: / revision:
  products: [ConfettiSwiftUI]   # optional; defaults to the repo name

Returns normalized entries: { "url", "requirement" => { "kind", "value" }, "products" => [...] }. Assumes the config validated clean (see native_ios_package_errors) — the requirement is always present here.

%w[from exact branch revision].freeze
SWIFT_TYPE_NAME =

Everything declared here is interpolated into generated Swift, so names are validated hard: type names must be plain Swift identifiers, screen identifiers must be safe inside a Swift string literal.

/\A[A-Za-z_][A-Za-z0-9_]*\z/
SCREEN_IDENTIFIER =
/\A[A-Za-z0-9_-]+\z/
PACKAGE_URL =

A URL SPM accepts (https for the common case, git@ for SSH remotes) and a loose semver for from:/exact: — enough to catch typos without reimplementing SPM's resolver, which reports anything subtler at build time.

%r{\A(https?://|git@).+}
PACKAGE_VERSION =
/\A\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?\z/
KOTLIN_TYPE_NAME =

Kotlin's identifier rules match Swift's across everything we generate (no backticked names, no unicode escapes), so one pattern validates both. Aliased rather than reused by name so the Android messages can say "Kotlin" and the two paths can diverge later without touching iOS.

SWIFT_TYPE_NAME
MAVEN_SEGMENT =

group:artifact:version — Gradle's shorthand form, and the only one we accept. The charset is narrower than Maven strictly allows on purpose: every coordinate is interpolated into a generated Kotlin DSL file inside a double-quoted literal, so a quote, a backslash, a $ (Kotlin string templates), a newline or a space would rewrite the build script rather than name a dependency. Version ranges ("[1.0,2.0)") and BOM-style two-part coordinates are rejected for the same reason and because Gradle resolves anything subtler at build time anyway.

/[A-Za-z0-9_][A-Za-z0-9_.-]*/
MAVEN_COORDINATE =
/\A#{MAVEN_SEGMENT}:#{MAVEN_SEGMENT}:[A-Za-z0-9_][A-Za-z0-9_.+-]*\z/
ANDROID_ICON_FONTS =

Which Material icon font icons.android names — and the per-platform icon names nav buttons and menu items carry in page HTML — resolve against. Android has no UIImage(systemName:), so the shell draws the icon's codepoint from a font; the choice is which font ships.

bundled is the whole reason this is a map and not a list: only the classic set (357 KB) rides inside the gem, because ruby_everywhere is 229 KB today and Symbols is ~10.6 MB — a 48x install cost paid by every user, including the desktop-only ones. The Symbols variants are fetched once on first Android build and cached under ~/.rubyeverywhere, which is noise next to the Gradle and AGP downloads the same build already makes.

codepoints is the name → codepoint map Ruby validates every declared icon name against at build time, so a typo fails the build naming the tab it came from instead of rendering a blank icon on device.

{
  "symbols" => { "file" => "MaterialSymbolsOutlined.ttf", "bundled" => false },
  "symbols-rounded" => { "file" => "MaterialSymbolsRounded.ttf", "bundled" => false },
  "symbols-sharp" => { "file" => "MaterialSymbolsSharp.ttf", "bundled" => false },
  "classic" => { "file" => "MaterialIcons-Regular.ttf", "bundled" => true },
  # No font at all: the app brings its own drawables in native/android/res/.
  "none" => { "file" => nil, "bundled" => true }
}.freeze
DEFAULT_ANDROID_ICON_FONT =
"symbols"
RUST_FN_NAME =

Everything here is interpolated into generated Rust and TOML, so names are validated hard — same reasoning as MAVEN_COORDINATE above. A command name becomes a path segment in a generate_handler! list; a crate name and its features become bare TOML keys and quoted strings.

/\A[a-z_][a-z0-9_]*\z/
CRATE_NAME =
/\A[a-zA-Z0-9_][a-zA-Z0-9_-]*\z/
CRATE_VERSION =

Cargo's requirement syntax, loosely: ^1.2, ~1.2, >=1.0, 1.*, 1.2.3-beta.1. Deliberately no spaces and no commas — a multi-requirement string would need quoting rules we don't want to own, and cargo reports anything subtler at build time.

/\A[\^~<>=]{0,2}\d+(\.\d+){0,2}(\.\*)?(-[0-9A-Za-z.-]+)?\z/
CRATE_FEATURE =
/\A[A-Za-z0-9_][A-Za-z0-9_+-]*\z/
CRATE_GIT_URL =

Where a git dependency may point. Same rule the SPM packages use.

PACKAGE_URL
DEFAULT_OAUTH_PATHS =

Paths that begin a third-party auth flow, as regex source strings matched against the request path. Declaring auth: at all opts in, defaulting to OmniAuth's /auth/… convention.

["^/auth/"].freeze
URL_SCHEME =

The custom URL scheme ASWebAuthenticationSession returns through. Defaults to the iOS bundle id — the convention, and already unique per app. Only the characters RFC 3986 allows in a scheme.

/\A[a-zA-Z][a-zA-Z0-9+.-]*\z/
WINDOW_TITLE_BARS =

Desktop window chrome. Desktop-only: on mobile the OS owns the frame, so mobile builds ignore this section entirely.

window:
title_bar: overlay   # decorated (default) | overlay | frameless
title: false         # draw the title text (default true)
size: [1100, 750]    # initial inner size
min_size: [600, 400]
resizable: true
drag_height: 28      # top strip that drags the window; 0 turns it off

Every reader returns nil when the key is absent so to_shell_hash drops it and the shell keeps its own default — there is exactly one place each default lives, and it's the Rust side.

overlay is the macOS "traffic lights floating over your content" look (TitleBarStyle::Overlay + hidden title). Windows and Linux have no such style, so the shell falls back to frameless there and the page draws its own controls via Everywhere.window.

%w[decorated overlay frameless].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data, root) ⇒ Config

Returns a new instance of Config.



161
162
163
164
# File 'lib/everywhere/config.rb', line 161

def initialize(data, root)
  @data = data
  @root = root
end

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



159
160
161
# File 'lib/everywhere/config.rb', line 159

def root
  @root
end

Class Method Details

.load(root = Dir.pwd) ⇒ Object



153
154
155
156
157
# File 'lib/everywhere/config.rb', line 153

def self.load(root = Dir.pwd)
  path = File.join(root, FILE)
  data = File.exist?(path) ? YAML.safe_load_file(path, aliases: true) || {} : {}
  new(data, root)
end

.os_of(target) ⇒ Object

Every target (os-arch string) resolves to a single platform (its os), and a platform may override the app-level identity/version keys — see platforms: below. Pass target: to any of these accessors to apply the matching override; omit it for the shared app-level value.



170
# File 'lib/everywhere/config.rb', line 170

def self.os_of(target) = target.to_s.split("-", 2).first

Instance Method Details

#android_manifest_permissionsObject

The manifest permissions an Android build declares, in declaration order. Only known names map to anything; permission_errors("android") has already failed the build on the rest by the time the builder asks.



413
414
415
# File 'lib/everywhere/config.rb', line 413

def android_manifest_permissions
  permissions.keys.filter_map { |name| ANDROID_PERMISSIONS[name] }.uniq
end

#android_screen_properties(properties) ⇒ Object

Hotwire Native iOS picks a native screen with view_controller: <id>; Android picks one with uri: hotwire://fragment/<id>, matched against the Fragment's own @HotwireDestinationDeepLink annotation. Same rule, same identifier, different property name — so the Android document derives the uri rather than making apps write the route twice and keep the two in sync by hand.

Derived only for ids declared under native.android.screens: an id that names an iOS-only screen must fall through to the web fragment, because a uri pointing at a Fragment this build doesn't contain resolves to nothing and the visit dead-ends. An explicit uri: always wins — that's the escape hatch for a screen whose annotation says something else.



480
481
482
483
484
485
486
# File 'lib/everywhere/config.rb', line 480

def android_screen_properties(properties)
  id = properties["view_controller"]
  return properties if id.nil? || properties.key?("uri")
  return properties unless native_android_screens.key?(id.to_s)

  properties.merge("uri" => "hotwire://fragment/#{id}")
end

#apple_app_site_associationObject

The apple-app-site-association document (a Hash), or nil when no Apple app ids are configured. applinks for universal links; webcredentials so associated-domains password autofill / Face ID credentials work too.



1231
1232
1233
1234
1235
1236
1237
1238
1239
# File 'lib/everywhere/config.rb', line 1231

def apple_app_site_association
  ids = deep_linking_apple_app_ids
  return nil if ids.empty?

  { "applinks" => {
      "details" => [{ "appIDs" => ids, "components" => deep_linking_paths.map { |p| { "/" => p } } }]
    },
    "webcredentials" => { "apps" => ids } }
end

#apple_app_site_association_jsonObject



1241
1242
1243
1244
1245
# File 'lib/everywhere/config.rb', line 1241

def apple_app_site_association_json
  require "json"
  doc = apple_app_site_association or return nil
  JSON.generate(doc)
end

The Digital Asset Links document (an Array), or nil without an Android app.



1248
1249
1250
1251
1252
1253
1254
1255
# File 'lib/everywhere/config.rb', line 1248

def asset_links
  return nil unless deep_linking_android?

  [{ "relation" => ["delegate_permission/common.handle_all_urls"],
     "target" => { "namespace" => "android_app",
                   "package_name" => deep_linking_android_package,
                   "sha256_cert_fingerprints" => deep_linking_android_fingerprints } }]
end


1257
1258
1259
1260
1261
# File 'lib/everywhere/config.rb', line 1257

def asset_links_json
  require "json"
  doc = asset_links or return nil
  JSON.generate(doc)
end

#authObject

Third-party sign-in (Sign in with Apple / Google / GitHub / anything OmniAuth speaks). Providers refuse to run — or run badly — inside an app's web view, so the mobile shell hands these paths to an ASWebAuthenticationSession instead, and the gem bridges the resulting session back into the app (see AuthHandoff).

auth:
oauth_paths:                # paths that begin a provider flow
  - ^/auth/                 # unanchored regexes, like `rules:`
scheme: com.example.app     # callback scheme (default: the iOS bundle id)
cookies:                    # which cookies cross back into the app
  except: ["_ga"]           #   (default: all of them)

The app keeps its own auth: auth: declares nothing about providers, only which paths the shell must not open in its web view.



950
# File 'lib/everywhere/config.rb', line 950

def auth = @data.fetch("auth", nil) || {}

#auth_cookie?(name) ⇒ Boolean

Cookie names to carry from the auth browser back into the app's web view, filtered by an optional allow/deny list. Everything the flow set is carried by default: the gem can't know which cookie an app's auth library signs its session with.

Returns:

  • (Boolean)


989
990
991
992
993
994
995
996
997
# File 'lib/everywhere/config.rb', line 989

def auth_cookie?(name)
  cookies = auth["cookies"]
  return true unless cookies.is_a?(Hash)

  only = Array(cookies["only"]).map(&:to_s)
  return only.include?(name.to_s) unless only.empty?

  !Array(cookies["except"]).map(&:to_s).include?(name.to_s)
end

#auth_errorsObject



1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
# File 'lib/everywhere/config.rb', line 1003

def auth_errors
  return [] unless @data.key?("auth")

  errors = oauth_paths.filter_map do |pattern|
    Regexp.new(pattern)
    nil
  rescue RegexpError => e
    "auth.oauth_paths: #{pattern.inspect} is not a valid pattern (#{e.message})"
  end

  unless auth_scheme.match?(URL_SCHEME)
    errors << "auth.scheme #{auth_scheme.inspect} is not a URL scheme (letters, digits, +, -, .)"
  end
  errors
end

#auth_schemeObject



980
981
982
983
# File 'lib/everywhere/config.rb', line 980

def auth_scheme
  explicit = auth["scheme"].to_s.strip
  explicit.empty? ? bundle_id(target: "ios") : explicit
end

#auth_shell_hashObject

The auth subset the shell needs: which visits to divert into the auth session, and the scheme to bring the answer back through. nil (absent from everywhere.json) when the app declares no auth: — a shell that can't divert anything is a shell with no new surface.



1023
1024
1025
1026
1027
# File 'lib/everywhere/config.rb', line 1023

def auth_shell_hash
  return nil unless oauth?

  { "oauth_paths" => oauth_paths, "scheme" => auth_scheme }
end

#auth_token_ttlObject

How long a minted handoff token stays valid. It travels device-locally (browser sheet → shell → web view), so seconds are plenty.



1001
# File 'lib/everywhere/config.rb', line 1001

def auth_token_ttl = [(auth["token_ttl"] || 60).to_i, 5].max

#background_colorObject



297
298
299
# File 'lib/everywhere/config.rb', line 297

def background_color
  normalize_color(appearance["background_color"])
end

#buildObject

The build: section — durable build knobs that were once every build flags (ruby, targets, capabilities). CLI flags still override per-run. See platform/docs/build-engine.md §2.



220
# File 'lib/everywhere/config.rb', line 220

def build = @data.fetch("build", nil) || {}

#build_capabilitiesObject

Desktop OS-integration capabilities the app declares (build.capabilities), as a plain list. Deliberately not called permissions: the top-level permissions: below is the mobile prompt declaration — a different shape for a different consumer — and while both were spelled "permissions" this accessor was silently redefined by that one and could never be read.



230
# File 'lib/everywhere/config.rb', line 230

def build_capabilities = Array(build["capabilities"])

#build_rubyObject

Ruby version to package. nil here means "let the CLI decide its default".



223
# File 'lib/everywhere/config.rb', line 223

def build_ruby = build["ruby"]

#bundle_id(target: nil) ⇒ Object

Reverse-DNS identifier: macOS bundle id, and the name of the per-user app-data directory on every platform. Set it explicitly and never change it — renaming moves users' data. A platform may override it (e.g. the App Store often wants com.example.app.ios).



180
181
182
183
# File 'lib/everywhere/config.rb', line 180

def bundle_id(target: nil)
  resolved(target)["bundle_id"] ||
    "com.rubyeverywhere.#{name(target: target).downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")}"
end

#deep_linkingObject

Deep linking / universal links. Declares the app's association with its web domains so the OS can hand matching URLs to the app instead of the browser. The gem serves the two well-known association files this needs — /.well-known/apple-app-site-association and /.well-known/assetlinks.json — generated from this config (Everywhere::Engine in Rails; MobileConfigEndpoint for Sinatra/Hanami). The iOS builder stamps the matching Associated Domains entitlement, and the shell routes an incoming link to its path.

deep_linking:
team_id: ABCDE12345          # derives the iOS app id from bundle_id
# or set app ids explicitly:
# apple_app_id: ABCDE12345.com.example.app
# apple_app_ids: ["ABCDE12345.com.example.app"]
paths: ["/*"]                # which paths open the app (default: all)
domains: ["www.example.com"] # extra applinks: domains (remote host is implicit)
android:
  package: com.example.app
  sha256_cert_fingerprints: ["AB:CD:EF:..."]


1181
# File 'lib/everywhere/config.rb', line 1181

def deep_linking = @data.fetch("deep_linking", nil) || {}

#deep_linking?Boolean

Returns:

  • (Boolean)


1211
# File 'lib/everywhere/config.rb', line 1211

def deep_linking? = deep_linking_apple? || deep_linking_android?

#deep_linking_androidObject



1200
# File 'lib/everywhere/config.rb', line 1200

def deep_linking_android = deep_linking["android"].is_a?(Hash) ? deep_linking["android"] : {}

#deep_linking_android?Boolean

Returns:

  • (Boolean)


1207
# File 'lib/everywhere/config.rb', line 1207

def deep_linking_android? = !deep_linking_android_package.to_s.empty? && !deep_linking_android_fingerprints.empty?

#deep_linking_android_fingerprintsObject



1203
1204
1205
# File 'lib/everywhere/config.rb', line 1203

def deep_linking_android_fingerprints
  Array(deep_linking_android["sha256_cert_fingerprints"]).map { |f| f.to_s.strip }.reject(&:empty?)
end

#deep_linking_android_packageObject



1201
# File 'lib/everywhere/config.rb', line 1201

def deep_linking_android_package = deep_linking_android["package"]&.to_s

#deep_linking_apple?Boolean

Returns:

  • (Boolean)


1209
# File 'lib/everywhere/config.rb', line 1209

def deep_linking_apple? = !deep_linking_apple_app_ids.empty?

#deep_linking_apple_app_idsObject

The iOS/tvOS/... app identifiers (TeamID.bundleID) the association file advertises. Explicit ids win; otherwise team_id + the app's iOS bundle id.



1185
1186
1187
1188
1189
1190
1191
# File 'lib/everywhere/config.rb', line 1185

def deep_linking_apple_app_ids
  explicit = Array(deep_linking["apple_app_ids"]) + [deep_linking["apple_app_id"]].compact
  return explicit.map(&:to_s).uniq unless explicit.empty?

  team = deep_linking["team_id"].to_s.strip
  team.empty? ? [] : ["#{team}.#{bundle_id(target: "ios")}"]
end

#deep_linking_domainsObject

Domains for the iOS Associated Domains entitlement (applinks:) and the shell's universal-link host allowlist: the remote host plus any extra domains:. Bare hostnames (no scheme, no path).



1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
# File 'lib/everywhere/config.rb', line 1216

def deep_linking_domains
  hosts = []
  if remote_url
    require "uri"
    hosts << URI(remote_url).host
  end
  hosts += Array(deep_linking["domains"]).map { |d| d.to_s.sub(%r{\Ahttps?://}, "").sub(%r{/.*\z}, "") }
  hosts.compact.reject(&:empty?).uniq
rescue URI::InvalidURIError
  Array(deep_linking["domains"]).map(&:to_s).reject(&:empty?).uniq
end

#deep_linking_pathsObject

URL path patterns that open the app. Modern (iOS 14+) component form; defaults to every path.



1195
1196
1197
1198
# File 'lib/everywhere/config.rb', line 1195

def deep_linking_paths
  paths = Array(deep_linking["paths"]).map(&:to_s).reject(&:empty?)
  paths.empty? ? ["*"] : paths
end

#entry_pathObject



204
205
206
207
# File 'lib/everywhere/config.rb', line 204

def entry_path
  path = app["entry_path"] || "/"
  path.start_with?("/") ? path : "/#{path}"
end

#iconObject

App icon source: a PNG (ideally square, 1024px+). Explicit app.icon path relative to the app root, or icon.png at the root by convention.



187
188
189
190
191
192
193
194
# File 'lib/everywhere/config.rb', line 187

def icon
  if (explicit = app["icon"])
    File.expand_path(explicit, root)
  else
    default = File.join(root, "icon.png")
    File.exist?(default) ? default : nil
  end
end

Custom native menu items (macOS app menu). Each entry: label + path (+ optional accelerator), or "separator". Clicks arrive in the page as "everywhere:menu" events and navigate via Turbo.



1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
# File 'lib/everywhere/config.rb', line 1032

def menu
  entries = @data["menu"]
  return [] unless entries.is_a?(Array)

  entries.filter_map do |item|
    if item == "separator" || (item.is_a?(Hash) && item["separator"])
      { "separator" => true }
    elsif item.is_a?(Hash) && item["label"] && item["path"]
      { "label" => item["label"], "path" => item["path"],
        "accelerator" => item["accelerator"] }.compact
    end
  end
end

#mobile_rules(os = nil) ⇒ Object

App-declared Hotwire Native path-configuration rules (everywhere.yml top-level rules:), appended after MOBILE_PATH_RULES — later rules win per property, so apps can make routes modal, disable pull-to-refresh, or set any other Hotwire path property without touching native code:

rules:
- patterns: ["/preferences$"]
  properties:
    context: default        # not a modal, unlike other /edit routes
- patterns: ["/live/"]
  properties:
    pull_to_refresh_enabled: false

Native screens are selected by a different property on each platform, so os decides how a rule's view_controller: is read (see #android_screen_properties).



455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/everywhere/config.rb', line 455

def mobile_rules(os = nil)
  entries = @data["rules"]
  return [] unless entries.is_a?(Array)

  entries.filter_map do |rule|
    next unless rule.is_a?(Hash) && rule["patterns"].is_a?(Array) && rule["properties"].is_a?(Hash)

    properties = rule["properties"]
    properties = android_screen_properties(properties) if android_target?(os)
    { "patterns" => rule["patterns"].map(&:to_s), "properties" => properties }
  end
end

#modeObject

"local" — the app is tebako-pressed and runs on-device (default) "remote" — thin shell around an already-deployed app: no press, no sidecar



237
238
239
# File 'lib/everywhere/config.rb', line 237

def mode
  app["mode"] || (remote_url ? "remote" : "local")
end

#name(target: nil) ⇒ Object



172
173
174
# File 'lib/everywhere/config.rb', line 172

def name(target: nil)
  resolved(target)["name"] || File.basename(File.expand_path(root)).split(/[-_]/).map(&:capitalize).join(" ")
end

#native_androidObject

The Android half of "supernative": Kotlin the app repo carries in native/android/ that every build --android compiles into the shell, declared the same way iOS declares Swift. Same build-time-only rule — Play forbids downloading executable code — and the same shape, so an app that already knows native.ios: knows this section too.

native:
android:
  components: [ChartComponent]   # BridgeComponent subclasses to register
  screens:                       # path rules with view_controller: <id>
    map: MapFragment             #   Fragment with an (url) argument
  splash: LaunchSplash           # Fragment/Activity shown while booting
  splash_min_seconds: 1.0        # how long that splash stays up at minimum
  lazy_load_tabs: true           # defer each tab's first visit until selected
  icon_font: symbols             # which Material icon font resolves icon names
  packages:                      # Maven coordinates, not SPM entries
    - "com.airbnb.android:lottie:6.4.0"


669
670
671
672
# File 'lib/everywhere/config.rb', line 669

def native_android
  section = @data.dig("native", "android")
  section.is_a?(Hash) ? section : {}
end

#native_android?Boolean

Returns:

  • (Boolean)


719
720
# File 'lib/everywhere/config.rb', line 719

def native_android? = !(native_android_components.empty? && native_android_screens.empty? &&
native_android_splash.nil?)

#native_android_componentsObject



674
# File 'lib/everywhere/config.rb', line 674

def native_android_components = Array(native_android["components"]).map(&:to_s)

#native_android_errorsObject



728
729
730
731
732
733
734
735
736
737
738
739
740
# File 'lib/everywhere/config.rb', line 728

def native_android_errors
  types = native_android_components + native_android_screens.values + [native_android_splash].compact
  errors = types.reject { |t| t.match?(KOTLIN_TYPE_NAME) }.map do |t|
    "native.android: #{t.inspect} is not a Kotlin type name (letters, digits, _)"
  end
  errors += native_android_screens.keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id|
    "native.android.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}"
  end

  raw = native_android["splash_min_seconds"]
  errors << "native.android.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric)
  errors + native_android_icon_font_errors + native_android_package_errors
end

#native_android_icon_fontObject



796
797
798
799
# File 'lib/everywhere/config.rb', line 796

def native_android_icon_font
  value = native_android["icon_font"].to_s.strip
  value.empty? ? DEFAULT_ANDROID_ICON_FONT : value
end

#native_android_icon_font_codepointsObject

The sidecar map the builder validates names against, alongside the font.



805
806
807
# File 'lib/everywhere/config.rb', line 805

def native_android_icon_font_codepoints
  native_android_icon_font_file&.sub(/\.ttf\z/, ".codepoints")
end

#native_android_icon_font_errorsObject



819
820
821
822
823
824
# File 'lib/everywhere/config.rb', line 819

def native_android_icon_font_errors
  return [] if ANDROID_ICON_FONTS.key?(native_android_icon_font)

  ["native.android.icon_font #{native_android_icon_font.inspect} is not a known font — " \
   "one of: #{ANDROID_ICON_FONTS.keys.join(", ")}"]
end

#native_android_icon_font_fetched?Boolean

True when the selected font has to be downloaded before the first build can stamp it — the only case that can fail offline, so the builder warns about it (and points at classic) rather than dying mid-Gradle.

Returns:

  • (Boolean)


812
813
814
815
# File 'lib/everywhere/config.rb', line 812

def native_android_icon_font_fetched?
  entry = ANDROID_ICON_FONTS[native_android_icon_font]
  !entry.nil? && entry["bundled"] == false
end

#native_android_icon_font_fileObject

The font's asset filename (assets/fonts/), or nil for "none".



802
# File 'lib/everywhere/config.rb', line 802

def native_android_icon_font_file = ANDROID_ICON_FONTS.dig(native_android_icon_font, "file")

#native_android_icons?Boolean

Returns:

  • (Boolean)


817
# File 'lib/everywhere/config.rb', line 817

def native_android_icons? = !native_android_icon_font_file.nil?

#native_android_lazy_load_tabsObject

Tri-state like native_ios_lazy_load_tabs: true defers each non-selected tab's first visit, false loads them all up front, absent leaves the shell on its default. Android's bottom-nav hosts are created eagerly at onCreate, so this decides whether they visit, not whether they exist.



697
698
699
700
# File 'lib/everywhere/config.rb', line 697

def native_android_lazy_load_tabs
  value = native_android["lazy_load_tabs"]
  value if value == true || value == false
end

#native_android_package_errorsObject



753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
# File 'lib/everywhere/config.rb', line 753

def native_android_package_errors
  raw = native_android["packages"]
  return [] if raw.nil?
  return ["native.android.packages must be a list"] unless raw.is_a?(Array)

  raw.each_with_index.filter_map do |entry, i|
    at = "native.android.packages[#{i}]"
    next "#{at} must be a Maven coordinate string like \"com.airbnb.android:lottie:6.4.0\"" unless entry.is_a?(String)

    coordinate = entry.strip
    next if coordinate.match?(MAVEN_COORDINATE)

    "#{at} #{coordinate.inspect} is not a Maven coordinate — " \
      "\"group:artifact:version\" (letters, digits, . _ -)"
  end
end

#native_android_packagesObject

Third-party dependencies for native/android code, as Gradle sees them: plain Maven coordinates. There is no SPM-style url + requirement split — a coordinate already carries group, artifact and version — so this accessor returns strings, one per implementation(…) line the builder writes into native-packages.gradle.kts. Assumes the config validated clean (see native_android_package_errors).



708
709
710
711
712
713
714
715
# File 'lib/everywhere/config.rb', line 708

def native_android_packages
  Array(native_android["packages"]).filter_map do |entry|
    next unless entry.is_a?(String)

    coordinate = entry.strip
    coordinate unless coordinate.empty?
  end
end

#native_android_packages?Boolean

Returns:

  • (Boolean)


717
# File 'lib/everywhere/config.rb', line 717

def native_android_packages? = !native_android_packages.empty?

#native_android_screensObject



676
677
678
679
680
681
# File 'lib/everywhere/config.rb', line 676

def native_android_screens
  entries = native_android["screens"]
  return {} unless entries.is_a?(Hash)

  entries.to_h { |id, type| [id.to_s, type.to_s] }
end

#native_android_splashObject



683
# File 'lib/everywhere/config.rb', line 683

def native_android_splash = native_android["splash"]&.to_s

#native_android_splash_min_secondsObject

Same contract as the iOS splash minimum, and the same clamp: the shell gives up waiting at 8s either way, so a larger number here would only promise something the shell won't honor.



688
689
690
691
# File 'lib/everywhere/config.rb', line 688

def native_android_splash_min_seconds
  value = native_android["splash_min_seconds"]
  value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric)
end

#native_desktopObject

The desktop half of "supernative": Rust the app repo carries in native/desktop/ that every build compiles into the Tauri shell.

The shape differs from ios/android because desktop differs: the whole UI is a webview, so there are no native screens or splash views to register. What an app actually wants down here is the machine — a serial port, a USB device, an FFI library, a Tauri plugin — reached from the page.

native:
desktop:
  commands: [scan_ports]        # fns reachable from the page
  setup: true                   # run native/desktop/setup.rs at boot
  crates:
    serialport: "4.3"
    reqwest: { version: "0.12", features: [json] }

A command is a plain function, NOT a #[tauri::command] — Tauri's typed commands only arrive through invoke, and invoke is unavailable to our pages (they're served over http; see extension_host.rs in the shell). The signature is uniform:

pub fn scan_ports(app: &tauri::AppHandle, payload: serde_json::Value)
  -> Result<serde_json::Value, String>

Declaring anything here has a real cost the mobile sections don't have: the shell stops being one prebuilt crate shared by every app and gets stamped and compiled per app (see Builders::Desktop). Apps that declare nothing keep the fast path.



854
855
856
857
# File 'lib/everywhere/config.rb', line 854

def native_desktop
  section = @data.dig("native", "desktop")
  section.is_a?(Hash) ? section : {}
end

#native_desktop?Boolean

Returns:

  • (Boolean)


876
877
# File 'lib/everywhere/config.rb', line 876

def native_desktop? = !(native_desktop_commands.empty? && native_desktop_crates.empty?) ||
native_desktop_setup?

#native_desktop_commandsObject



859
# File 'lib/everywhere/config.rb', line 859

def native_desktop_commands = Array(native_desktop["commands"]).map { |name| name.to_s.strip }

#native_desktop_crate_errorsObject



920
921
922
923
924
925
926
927
928
929
930
931
932
933
# File 'lib/everywhere/config.rb', line 920

def native_desktop_crate_errors
  raw = native_desktop["crates"]
  return [] if raw.nil?
  return ["native.desktop.crates must be a mapping of crate name to version"] unless raw.is_a?(Hash)

  raw.flat_map do |name, spec|
    at = "native.desktop.crates.#{name}"
    errors = []
    unless name.to_s.strip.match?(CRATE_NAME)
      errors << "#{at}: #{name.to_s.inspect} is not a crate name (letters, digits, _, -)"
    end
    errors + crate_spec_errors(at, spec)
  end
end

#native_desktop_cratesObject

Cargo dependencies, normalized to { "name" => Everywhere::Config......table } so the generator has one shape to write. The short form (a bare version string) becomes { "version" => "4.3" }.



866
867
868
869
870
871
872
873
874
# File 'lib/everywhere/config.rb', line 866

def native_desktop_crates
  entries = native_desktop["crates"]
  return {} unless entries.is_a?(Hash)

  entries.to_h do |name, spec|
    table = spec.is_a?(Hash) ? spec : { "version" => spec.to_s }
    [name.to_s.strip, table]
  end
end

#native_desktop_errorsObject



894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
# File 'lib/everywhere/config.rb', line 894

def native_desktop_errors
  raw = @data.dig("native", "desktop")
  return [] if raw.nil?
  return ["native.desktop must be a mapping"] unless raw.is_a?(Hash)

  errors = []
  commands = raw["commands"]
  if !commands.nil? && !commands.is_a?(Array)
    errors << "native.desktop.commands must be a list of function names"
  else
    errors += native_desktop_commands.reject { |name| name.match?(RUST_FN_NAME) }.map do |name|
      "native.desktop.commands: #{name.inspect} is not a Rust function name — " \
        "snake_case (letters, digits, _), matching the fn in native/desktop/"
    end
    duplicates = native_desktop_commands.tally.select { |_name, count| count > 1 }.keys
    errors += duplicates.map do |name|
      "native.desktop.commands: #{name.inspect} is listed twice"
    end
  end

  setup = raw["setup"]
  errors << "native.desktop.setup must be true or false" unless setup.nil? || boolean_or_nil(setup) == setup

  errors + native_desktop_crate_errors
end

#native_desktop_setup?Boolean

Returns:

  • (Boolean)


861
# File 'lib/everywhere/config.rb', line 861

def native_desktop_setup? = native_desktop["setup"] == true

#native_iosObject

Native code extensions ("supernative"): Swift the app repo carries in native/ios/ that every build compiles into the shell, plus this declaration of what to hook up. Build-time only — stores forbid downloading native code, so extensions ship with the app binary.

native:
ios:
  components: [ChartComponent]   # BridgeComponent subclasses to register
  screens:                       # path rules with view_controller: <id>
    map: MapScreen               #   SwiftUI View or UIViewController, init(url:)
  splash: LaunchSplash           # SwiftUI View or UIViewController, init()
  splash_min_seconds: 1.0        # how long the custom splash stays up at minimum
  lazy_load_tabs: true           # defer each tab's first visit until it's selected


518
519
520
521
# File 'lib/everywhere/config.rb', line 518

def native_ios
  section = @data.dig("native", "ios")
  section.is_a?(Hash) ? section : {}
end

#native_ios?Boolean

Returns:

  • (Boolean)


589
# File 'lib/everywhere/config.rb', line 589

def native_ios? = !(native_ios_components.empty? && native_ios_screens.empty? && native_ios_splash.nil?)

#native_ios_componentsObject



523
# File 'lib/everywhere/config.rb', line 523

def native_ios_components = Array(native_ios["components"]).map(&:to_s)

#native_ios_errorsObject



597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/everywhere/config.rb', line 597

def native_ios_errors
  types = native_ios_components + native_ios_screens.values + [native_ios_splash].compact
  errors = types.reject { |t| t.match?(SWIFT_TYPE_NAME) }.map do |t|
    "native.ios: #{t.inspect} is not a Swift type name (letters, digits, _)"
  end
  errors += native_ios_screens.keys.reject { |id| id.match?(SCREEN_IDENTIFIER) }.map do |id|
    "native.ios.screens: identifier #{id.inspect} must match #{SCREEN_IDENTIFIER.inspect}"
  end

  raw = native_ios["splash_min_seconds"]
  errors << "native.ios.splash_min_seconds must be a number (seconds)" if raw && !raw.is_a?(Numeric)
  errors + native_ios_package_errors
end

#native_ios_lazy_load_tabsObject

Whether the native tab bar defers each non-selected tab's initial visit until the tab is first selected (Hotwire Native lazyLoadTabs). Tri-state so apps can opt in or opt out explicitly: true lazy-loads, false loads every tab up front, and an absent key leaves the shell on its default (eager).



547
548
549
550
# File 'lib/everywhere/config.rb', line 547

def native_ios_lazy_load_tabs
  value = native_ios["lazy_load_tabs"]
  value if value == true || value == false
end

#native_ios_package_errorsObject



617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/everywhere/config.rb', line 617

def native_ios_package_errors
  raw = native_ios["packages"]
  return [] if raw.nil?
  return ["native.ios.packages must be a list"] unless raw.is_a?(Array)

  raw.each_with_index.flat_map do |entry, i|
    at = "native.ios.packages[#{i}]"
    next ["#{at} must be a mapping with a url:"] unless entry.is_a?(Hash)

    errors = []
    url = entry["url"].to_s.strip
    if url.empty?
      errors << "#{at}.url is required"
    elsif !url.match?(PACKAGE_URL)
      errors << "#{at}.url #{url.inspect} must be an https:// or git@ URL"
    end

    present = PACKAGE_REQUIREMENT_KEYS.select { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
    if present.empty?
      errors << "#{at} needs one version requirement (#{PACKAGE_REQUIREMENT_KEYS.join(" / ")})"
    elsif present.length > 1
      errors << "#{at} sets conflicting requirements (#{present.join(", ")}) — pick one"
    elsif %w[from exact].include?(present.first) && !entry[present.first].to_s.strip.match?(PACKAGE_VERSION)
      errors << "#{at}.#{present.first} #{entry[present.first].to_s.inspect} must be a version like \"1.2.0\""
    end

    Array(entry["products"]).each do |product|
      next if product.to_s.match?(SWIFT_TYPE_NAME)

      errors << "#{at}.products: #{product.to_s.inspect} is not a module name (letters, digits, _)"
    end
    errors
  end
end

#native_ios_packagesObject



567
568
569
570
571
572
573
574
575
576
577
578
579
580
# File 'lib/everywhere/config.rb', line 567

def native_ios_packages
  Array(native_ios["packages"]).filter_map do |entry|
    next unless entry.is_a?(Hash)

    url = entry["url"].to_s.strip
    kind = PACKAGE_REQUIREMENT_KEYS.find { |k| entry.key?(k) && !entry[k].to_s.strip.empty? }
    products = Array(entry["products"]).map { |p| p.to_s.strip }.reject(&:empty?)
    products = [package_identity(url)] if products.empty?

    { "url" => url,
      "requirement" => (kind && { "kind" => kind, "value" => entry[kind].to_s.strip }),
      "products" => products }
  end
end

#native_ios_packages?Boolean

Returns:

  • (Boolean)


582
# File 'lib/everywhere/config.rb', line 582

def native_ios_packages? = !native_ios_packages.empty?

#native_ios_screensObject



525
526
527
528
529
530
# File 'lib/everywhere/config.rb', line 525

def native_ios_screens
  entries = native_ios["screens"]
  return {} unless entries.is_a?(Hash)

  entries.to_h { |id, type| [id.to_s, type.to_s] }
end

#native_ios_splashObject



532
# File 'lib/everywhere/config.rb', line 532

def native_ios_splash = native_ios["splash"]&.to_s

#native_ios_splash_min_secondsObject

Minimum seconds a custom splash stays on screen (default 1.0 shell-side; a fast server would otherwise dismiss a branded splash in a ~50ms flash). Clamped to the shell's 8s give-up timeout.



537
538
539
540
# File 'lib/everywhere/config.rb', line 537

def native_ios_splash_min_seconds
  value = native_ios["splash_min_seconds"]
  value.to_f.clamp(0.0, 8.0) if value.is_a?(Numeric)
end

#native_lazy_load_tabs(target: nil) ⇒ Object

The two native: knobs that ship inside everywhere.json rather than being compiled in, resolved for the platform being stamped. They read the target's own section: everywhere.json is written once per target, so an Android build that inherited native.ios.lazy_load_tabs would silently apply a decision the app made about a different shell — and the two platforms load tabs differently enough that the answer legitimately differs. Without a target (the desktop dev loop, and every caller written before Android existed) the iOS answer stands, which is what those callers have always gotten.



1272
1273
1274
# File 'lib/everywhere/config.rb', line 1272

def native_lazy_load_tabs(target: nil)
  android_target?(target) ? native_android_lazy_load_tabs : native_ios_lazy_load_tabs
end

#native_splash_min_seconds(target: nil) ⇒ Object



1276
1277
1278
# File 'lib/everywhere/config.rb', line 1276

def native_splash_min_seconds(target: nil)
  android_target?(target) ? native_android_splash_min_seconds : native_ios_splash_min_seconds
end

#oauth?Boolean

Returns:

  • (Boolean)


964
# File 'lib/everywhere/config.rb', line 964

def oauth?  = !oauth_paths.empty?

#oauth_path?(path) ⇒ Boolean

Whether a request path starts a provider flow. Both halves of the system ask this — the middleware per request, the shell per visit proposal — so the patterns are compiled the same way on both sides (unanchored regex).

Returns:

  • (Boolean)


969
970
971
972
973
# File 'lib/everywhere/config.rb', line 969

def oauth_path?(path)
  oauth_paths.any? { |pattern| Regexp.new(pattern).match?(path.to_s) }
rescue RegexpError
  false
end

#oauth_pathsObject



957
958
959
960
961
962
# File 'lib/everywhere/config.rb', line 957

def oauth_paths
  return [] unless @data.key?("auth")

  paths = Array(auth["oauth_paths"]).map { |p| p.to_s.strip }.reject(&:empty?)
  paths.empty? ? DEFAULT_OAUTH_PATHS.dup : paths
end

#package_identity(url) ⇒ Object

SPM's package identity: the URL's last path segment without a .git suffix (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI"). Used as the default product name and as the package: key in .product(name:package:).



587
# File 'lib/everywhere/config.rb', line 587

def package_identity(url) = File.basename(url.to_s.sub(%r{/+\z}, "")).sub(/\.git\z/, "")

#path_configuration_hash(os, tabs: nil) ⇒ Object

The full path-configuration document for one mobile platform — rules plus our settings (tabs live in settings, per Hotwire Native convention). Pass tabs: to override the resolved list (the mobile config endpoint passes a per-request-filtered list; build-time stamping omits it and bakes them all).



493
494
495
496
497
498
# File 'lib/everywhere/config.rb', line 493

def path_configuration_hash(os, tabs: nil)
  settings = {}
  platform_tabs = tabs || tabs_for(os)
  settings["tabs"] = platform_tabs unless platform_tabs.empty?
  { "settings" => settings, "rules" => MOBILE_PATH_RULES + mobile_rules(os) }
end

#path_configuration_json(os, tabs: nil) ⇒ Object



500
501
502
503
# File 'lib/everywhere/config.rb', line 500

def path_configuration_json(os, tabs: nil)
  require "json"
  JSON.generate(path_configuration_hash(os, tabs: tabs))
end

#permission_errors(os) ⇒ Object

Problems with the permissions declaration for one platform, as human-readable strings — unknown names, and camera/location missing the mandatory usage string. Empty means buildable.

Only the unknown-name half applies to Android: the usage string exists to satisfy iOS, which kills the app when a prompt has no Info.plist entry. Android just prompts, so requiring the sentence there would fail builds over a value nothing reads.



425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/everywhere/config.rb', line 425

def permission_errors(os)
  permissions.flat_map do |name, usage|
    unless MOBILE_PERMISSIONS.include?(name)
      next ["unknown permission #{name.inspect} — supported: #{MOBILE_PERMISSIONS.join(", ")}"]
    end

    if os == "ios" && IOS_USAGE_KEYS.key?(name) && usage["ios"].to_s.strip.empty?
      next ["#{name} needs a usage string — the sentence iOS shows when asking. " \
            "In config/everywhere.yml:\n  permissions:\n    #{name}: \"Why the app needs #{name}.\""]
    end

    []
  end
end

#permissionsObject



394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
# File 'lib/everywhere/config.rb', line 394

def permissions
  entries = @data["permissions"]
  return {} unless entries.is_a?(Hash)

  entries.each_with_object({}) do |(name, value), acc|
    next if value.nil? || value == false

    acc[name.to_s] =
      case value
      when String then { "ios" => value }
      when Hash then value.transform_keys(&:to_s).transform_values(&:to_s)
      else {}
      end
  end
end

#remote?Boolean

Returns:

  • (Boolean)


241
# File 'lib/everywhere/config.rb', line 241

def remote? = mode == "remote"

#remote_instances?Boolean

Multi-instance apps (remote.instances: true): the mobile shell boots into remote.url — a hosted instance-picker page — and lets that page re-root the app onto a chosen instance via Everywhere.instance.set, persisted across launches until Everywhere.instance.clear. Opt-in because it lets page JS repoint the whole app: apps that aren't multi-instance shouldn't carry that surface.

Returns:

  • (Boolean)


253
254
255
# File 'lib/everywhere/config.rb', line 253

def remote_instances?
  @data.dig("remote", "instances") == true
end

#remote_urlObject



243
244
245
# File 'lib/everywhere/config.rb', line 243

def remote_url
  @data.dig("remote", "url")&.chomp("/")
end

#splashObject

Optional custom splash page (HTML file, path relative to the app root). Shown while the packaged server boots; the shell injects window.EVERYWHERE_CONFIG so it can use the app's name and colors.



199
200
201
202
# File 'lib/everywhere/config.rb', line 199

def splash
  path = app["splash"]
  File.expand_path(path, root) if path
end

#tabsObject

Mobile tab bar, platform-neutral. Icons are per-platform (ios = SF Symbol, android = Material icon later), with a shared icon: fallback:

tabs:
- name: Builds
  path: /builds
  icons:
    ios: hammer
    android: build

Tabs ship two ways from the same source: baked into the app's bundled path-configuration.json at build time (offline/first-launch), and served live from /everywhere/_v1.json (MobileConfigEndpoint) so tab changes deploy with the web app — no app-store release.



315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/everywhere/config.rb', line 315

def tabs
  entries = @data["tabs"]
  return [] unless entries.is_a?(Array)

  entries.filter_map do |tab|
    next unless tab.is_a?(Hash) && tab["name"] && tab["path"]

    path = tab["path"].start_with?("/") ? tab["path"] : "/#{tab["path"]}"
    { "name" => tab["name"], "path" => path,
      "icon" => tab["icon"], "icons" => (tab["icons"] if tab["icons"].is_a?(Hash)) }.compact
  end
end

#tabs_for(os) ⇒ Object

Tabs with the icon resolved for one platform: icons., then the shared icon, then a safe default.



330
331
332
333
334
335
# File 'lib/everywhere/config.rb', line 330

def tabs_for(os)
  tabs.map do |tab|
    { "title" => tab["name"], "path" => tab["path"],
      "icon" => tab.dig("icons", os.to_s) || tab["icon"] || DEFAULT_TAB_ICONS[os.to_s] }.compact
  end
end

#targetsObject

Build targets (os-arch strings) — the CI matrix, expressed once.



233
# File 'lib/everywhere/config.rb', line 233

def targets = Array(build["targets"])

#tint_colorObject



293
294
295
# File 'lib/everywhere/config.rb', line 293

def tint_color
  normalize_color(appearance["tint_color"])
end

#to_shell_hash(target: nil) ⇒ Object

The subset the shell needs, shipped as JSON (env var in dev, Resources/everywhere.json inside a bundled .app). Pass target: so the packaged config reflects the platform being built (its bundle id / name).



1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
# File 'lib/everywhere/config.rb', line 1283

def to_shell_hash(target: nil)
  {
    "name" => name(target: target),
    "bundle_id" => bundle_id(target: target),
    "version" => version(target: target),
    "mode" => mode,
    "remote_url" => remote_url,
    "remote_instances" => (true if remote_instances?),
    "entry_path" => entry_path,
    "tint_color" => tint_color,
    "background_color" => background_color,
    "menu" => (menu unless menu.empty?),
    "tray" => (tray unless tray.empty?),
    "window" => window_shell_hash,
    "lazy_load_tabs" => native_lazy_load_tabs(target: target),
    "splash_min_seconds" => native_splash_min_seconds(target: target),
    "updates" => updates_shell_hash,
    "permissions" => (permissions.keys unless permissions.empty?),
    "auth" => auth_shell_hash,
    # Hosts the shell treats as its own for incoming universal links, so a
    # tapped link to any associated domain opens in-app rather than Safari.
    "universal_link_hosts" => (deep_linking_domains if deep_linking? && !deep_linking_domains.empty?)
  }.compact
end

#to_shell_json(target: nil) ⇒ Object



1308
1309
1310
1311
# File 'lib/everywhere/config.rb', line 1308

def to_shell_json(target: nil)
  require "json"
  JSON.generate(to_shell_hash(target: target))
end

#trayObject

System tray: same entry shape as menu: plus action: quit | show.

tray:
- label: "Open My App"
  action: show
- label: "New Note"
  path: /notes/new
- separator
- label: Quit
  action: quit


1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
# File 'lib/everywhere/config.rb', line 1056

def tray
  entries = @data["tray"]
  return [] unless entries.is_a?(Array)

  entries.filter_map do |item|
    if item == "separator" || (item.is_a?(Hash) && item["separator"])
      { "separator" => true }
    elsif item.is_a?(Hash) && item["label"] && (item["path"] || item["action"])
      { "label" => item["label"], "path" => item["path"],
        "action" => item["action"] }.compact
    end
  end
end

#updatesObject

The updates: section — self-hosted auto-updates. Two halves: a client half (url/channel/public_key/auto/interval) that ships to the shell, and a publish-side s3: half that NEVER leaves the dev machine. channel here is a release channel (stable/beta) — not the receipt's distribution_channel (direct vs app stores).



262
# File 'lib/everywhere/config.rb', line 262

def updates = @data.fetch("updates", nil) || {}

#updates_autoObject

off — never check; check — check + notify (default); download — also fetch/verify in the background; install — silent auto-update.



274
# File 'lib/everywhere/config.rb', line 274

def updates_auto = updates["auto"] || "check"

#updates_channelObject



266
# File 'lib/everywhere/config.rb', line 266

def updates_channel = updates["channel"] || "stable"

#updates_intervalObject



276
# File 'lib/everywhere/config.rb', line 276

def updates_interval = [(updates["interval"] || 21_600).to_i, 300].max

#updates_public_keyObject



270
# File 'lib/everywhere/config.rb', line 270

def updates_public_key = updates["public_key"]

#updates_s3Object



264
# File 'lib/everywhere/config.rb', line 264

def updates_s3 = updates.fetch("s3", nil) || {}

#updates_shell_hashObject

The client subset for the shell; nil (and thus absent from everywhere.json) until both url and public_key are configured — an unsigned update feed is not a thing.



281
282
283
284
285
286
287
288
289
290
291
# File 'lib/everywhere/config.rb', line 281

def updates_shell_hash
  return nil unless updates_url && updates_public_key

  {
    "url" => updates_url,
    "channel" => updates_channel,
    "public_key" => updates_public_key,
    "auto" => updates_auto,
    "interval" => updates_interval
  }
end

#updates_urlObject



268
# File 'lib/everywhere/config.rb', line 268

def updates_url = updates["url"]&.chomp("/")

#version(target: nil) ⇒ Object

Marketing/display version of the app (CFBundleShortVersionString, and the version recorded in the build receipt). One shared app.version is the default for every target; a platform may override it when a store forces a different number. Defaults conservatively.



213
214
215
# File 'lib/everywhere/config.rb', line 213

def version(target: nil)
  resolved(target)["version"] || "0.1.0"
end

#windowObject



1091
1092
1093
1094
# File 'lib/everywhere/config.rb', line 1091

def window
  section = @data["window"]
  section.is_a?(Hash) ? section : {}
end

#window_drag_heightObject

How much of the top of the page drags the window, in CSS pixels. Only meaningful for overlay/frameless, where there's no system title bar left to grab. nil leaves the shell's 28px default (the height of the macOS traffic-light band); 0 turns dragging off for an app that wants to place its own drag regions.



1114
1115
1116
1117
# File 'lib/everywhere/config.rb', line 1114

def window_drag_height
  value = window["drag_height"]
  value.to_f if value.is_a?(Numeric) && !value.negative?
end

#window_errorsObject



1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
# File 'lib/everywhere/config.rb', line 1133

def window_errors
  raw = @data["window"]
  return [] if raw.nil?
  return ["window: must be a mapping"] unless raw.is_a?(Hash)

  errors = []
  bar = raw["title_bar"]
  if bar && !WINDOW_TITLE_BARS.include?(bar.to_s)
    errors << "window.title_bar #{bar.to_s.inspect} must be one of " \
              "#{WINDOW_TITLE_BARS.join(" / ")}"
  end

  errors += %w[size min_size].filter_map do |key|
    next if raw[key].nil? || window_dimensions(key)

    "window.#{key} must be two positive numbers, like [1100, 750]"
  end

  drag = raw["drag_height"]
  unless drag.nil? || (drag.is_a?(Numeric) && !drag.negative?)
    errors << "window.drag_height must be a number of pixels (0 turns dragging off)"
  end

  errors + %w[title resizable].filter_map do |key|
    next if raw[key].nil? || boolean_or_nil(raw[key]) == raw[key]

    "window.#{key} must be true or false"
  end
end

#window_min_sizeObject



1107
# File 'lib/everywhere/config.rb', line 1107

def window_min_size = window_dimensions("min_size")

#window_resizableObject



1104
# File 'lib/everywhere/config.rb', line 1104

def window_resizable = boolean_or_nil(window["resizable"])

#window_shell_hashObject

The window subset the shell needs. nil (absent from everywhere.json) when the app declares nothing, so the shell's own defaults stand untouched.



1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
# File 'lib/everywhere/config.rb', line 1121

def window_shell_hash
  hash = {
    "title_bar" => window_title_bar,
    "title" => window_title,
    "resizable" => window_resizable,
    "size" => window_size,
    "min_size" => window_min_size,
    "drag_height" => window_drag_height
  }.compact
  hash unless hash.empty?
end

#window_sizeObject



1106
# File 'lib/everywhere/config.rb', line 1106

def window_size = window_dimensions("size")

#window_titleObject

Tri-state, like native_ios_lazy_load_tabs: an absent key leaves the shell on its default rather than forcing one.



1103
# File 'lib/everywhere/config.rb', line 1103

def window_title = boolean_or_nil(window["title"])

#window_title_barObject



1096
1097
1098
1099
# File 'lib/everywhere/config.rb', line 1096

def window_title_bar
  value = window["title_bar"].to_s
  value if WINDOW_TITLE_BARS.include?(value)
end