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
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/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data, root) ⇒ Config

Returns a new instance of Config.



85
86
87
88
# File 'lib/everywhere/config.rb', line 85

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

Instance Attribute Details

#rootObject (readonly)

Returns the value of attribute root.



83
84
85
# File 'lib/everywhere/config.rb', line 83

def root
  @root
end

Class Method Details

.load(root = Dir.pwd) ⇒ Object



77
78
79
80
81
# File 'lib/everywhere/config.rb', line 77

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.



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

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

Instance Method Details

#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.



626
627
628
629
630
631
632
633
634
# File 'lib/everywhere/config.rb', line 626

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



636
637
638
639
640
# File 'lib/everywhere/config.rb', line 636

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.



643
644
645
646
647
648
649
650
# File 'lib/everywhere/config.rb', line 643

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


652
653
654
655
656
# File 'lib/everywhere/config.rb', line 652

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

#background_colorObject



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

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

#buildObject

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



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

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

#build_rubyObject

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



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

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).



104
105
106
107
# File 'lib/everywhere/config.rb', line 104

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:..."]


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

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

#deep_linking?Boolean

Returns:

  • (Boolean)


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

def deep_linking? = deep_linking_apple? || deep_linking_android?

#deep_linking_androidObject



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

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

#deep_linking_android?Boolean

Returns:

  • (Boolean)


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

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

#deep_linking_android_fingerprintsObject



598
599
600
# File 'lib/everywhere/config.rb', line 598

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



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

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

#deep_linking_apple?Boolean

Returns:

  • (Boolean)


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

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.



580
581
582
583
584
585
586
# File 'lib/everywhere/config.rb', line 580

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).



611
612
613
614
615
616
617
618
619
620
621
# File 'lib/everywhere/config.rb', line 611

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.



590
591
592
593
# File 'lib/everywhere/config.rb', line 590

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

#entry_pathObject



128
129
130
131
# File 'lib/everywhere/config.rb', line 128

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.



111
112
113
114
115
116
117
118
# File 'lib/everywhere/config.rb', line 111

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.



520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/everywhere/config.rb', line 520

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_rulesObject

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


342
343
344
345
346
347
348
349
350
351
# File 'lib/everywhere/config.rb', line 342

def mobile_rules
  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)

    { "patterns" => rule["patterns"].map(&:to_s), "properties" => rule["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



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

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

#name(target: nil) ⇒ Object



96
97
98
# File 'lib/everywhere/config.rb', line 96

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

#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


383
384
385
386
# File 'lib/everywhere/config.rb', line 383

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

#native_ios?Boolean

Returns:

  • (Boolean)


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

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

#native_ios_componentsObject



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

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

#native_ios_errorsObject



462
463
464
465
466
467
468
469
470
471
472
473
474
# File 'lib/everywhere/config.rb', line 462

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).



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

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

#native_ios_package_errorsObject



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/everywhere/config.rb', line 482

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



432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/everywhere/config.rb', line 432

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)


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

def native_ios_packages? = !native_ios_packages.empty?

#native_ios_screensObject



390
391
392
393
394
395
# File 'lib/everywhere/config.rb', line 390

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



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

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.



402
403
404
405
# File 'lib/everywhere/config.rb', line 402

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

#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:).



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

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).



358
359
360
361
362
363
# File 'lib/everywhere/config.rb', line 358

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 }
end

#path_configuration_json(os, tabs: nil) ⇒ Object



365
366
367
368
# File 'lib/everywhere/config.rb', line 365

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.



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

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

OS-integration permissions the app declares. Recorded in the receipt and (later) used to generate the shell's capabilities.



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

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

#remote?Boolean

Returns:

  • (Boolean)


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

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)


174
175
176
# File 'lib/everywhere/config.rb', line 174

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

#remote_urlObject



164
165
166
# File 'lib/everywhere/config.rb', line 164

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.



123
124
125
126
# File 'lib/everywhere/config.rb', line 123

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.



236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/everywhere/config.rb', line 236

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.



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

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.



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

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

#tint_colorObject



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

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).



661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
# File 'lib/everywhere/config.rb', line 661

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?),
    "lazy_load_tabs" => native_ios_lazy_load_tabs,
    "splash_min_seconds" => native_ios_splash_min_seconds,
    "updates" => updates_shell_hash,
    "permissions" => (permissions.keys unless permissions.empty?),
    # 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



684
685
686
687
# File 'lib/everywhere/config.rb', line 684

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


544
545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/everywhere/config.rb', line 544

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).



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

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.



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

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

#updates_channelObject



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

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

#updates_intervalObject



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

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

#updates_public_keyObject



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

def updates_public_key = updates["public_key"]

#updates_s3Object



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

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.



202
203
204
205
206
207
208
209
210
211
212
# File 'lib/everywhere/config.rb', line 202

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



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

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.



137
138
139
# File 'lib/everywhere/config.rb', line 137

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