Class: Everywhere::Config
- Inherits:
-
Object
- Object
- Everywhere::Config
- 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 answersundeclaredfor 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 withtrue.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 buildwrites them into the shell's local NativeExtensions package and re-exports every product, so native/ios code reaches them with a singleimport NativeExtensions.packages: - url: https://github.com/simibac/ConfettiSwiftUI from: "1.1.0" # or exact: / branch: / revision: products: [ConfettiSwiftUI] # optional; defaults to the repo nameReturns 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.androidnames — 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.bundledis the whole reason this is a map and not a list: only the classic set (357 KB) rides inside the gem, becauseruby_everywhereis 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.codepointsis 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 offEvery reader returns nil when the key is absent so
to_shell_hashdrops it and the shell keeps its own default — there is exactly one place each default lives, and it's the Rust side.overlayis 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
-
#root ⇒ Object
readonly
Returns the value of attribute root.
Class Method Summary collapse
- .load(root = Dir.pwd) ⇒ Object
-
.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.
Instance Method Summary collapse
-
#android_manifest_permissions ⇒ Object
The manifest permissions an Android build declares, in declaration order.
-
#android_screen_properties(properties) ⇒ Object
Hotwire Native iOS picks a native screen with
view_controller: <id>; Android picks one withuri: hotwire://fragment/<id>, matched against the Fragment's own @HotwireDestinationDeepLink annotation. -
#apple_app_site_association ⇒ Object
The apple-app-site-association document (a Hash), or nil when no Apple app ids are configured.
- #apple_app_site_association_json ⇒ Object
-
#asset_links ⇒ Object
The Digital Asset Links document (an Array), or nil without an Android app.
- #asset_links_json ⇒ Object
-
#auth ⇒ Object
Third-party sign-in (Sign in with Apple / Google / GitHub / anything OmniAuth speaks).
-
#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.
- #auth_errors ⇒ Object
- #auth_scheme ⇒ Object
-
#auth_shell_hash ⇒ Object
The auth subset the shell needs: which visits to divert into the auth session, and the scheme to bring the answer back through.
-
#auth_token_ttl ⇒ Object
How long a minted handoff token stays valid.
- #background_color ⇒ Object
-
#build ⇒ Object
The
build:section — durable build knobs that were onceevery buildflags (ruby, targets, capabilities). -
#build_capabilities ⇒ Object
Desktop OS-integration capabilities the app declares (
build.capabilities), as a plain list. -
#build_ruby ⇒ Object
Ruby version to package.
-
#bundle_id(target: nil) ⇒ Object
Reverse-DNS identifier: macOS bundle id, and the name of the per-user app-data directory on every platform.
-
#deep_linking ⇒ Object
Deep linking / universal links.
- #deep_linking? ⇒ Boolean
- #deep_linking_android ⇒ Object
- #deep_linking_android? ⇒ Boolean
- #deep_linking_android_fingerprints ⇒ Object
- #deep_linking_android_package ⇒ Object
- #deep_linking_apple? ⇒ Boolean
-
#deep_linking_apple_app_ids ⇒ Object
The iOS/tvOS/...
-
#deep_linking_domains ⇒ Object
Domains for the iOS Associated Domains entitlement (applinks:
) and the shell's universal-link host allowlist: the remote host plus any extra domains:. -
#deep_linking_paths ⇒ Object
URL path patterns that open the app.
- #entry_path ⇒ Object
-
#icon ⇒ Object
App icon source: a PNG (ideally square, 1024px+).
-
#initialize(data, root) ⇒ Config
constructor
A new instance of Config.
-
#menu ⇒ Object
Custom native menu items (macOS app menu).
-
#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:. -
#mode ⇒ Object
"local" — the app is tebako-pressed and runs on-device (default) "remote" — thin shell around an already-deployed app: no press, no sidecar.
- #name(target: nil) ⇒ Object
-
#native_android ⇒ Object
The Android half of "supernative": Kotlin the app repo carries in native/android/ that
every build --androidcompiles into the shell, declared the same way iOS declares Swift. - #native_android? ⇒ Boolean
- #native_android_components ⇒ Object
- #native_android_errors ⇒ Object
- #native_android_icon_font ⇒ Object
-
#native_android_icon_font_codepoints ⇒ Object
The sidecar map the builder validates names against, alongside the font.
- #native_android_icon_font_errors ⇒ Object
-
#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. -
#native_android_icon_font_file ⇒ Object
The font's asset filename (assets/fonts/
), or nil for "none". - #native_android_icons? ⇒ Boolean
-
#native_android_lazy_load_tabs ⇒ Object
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.
- #native_android_package_errors ⇒ Object
-
#native_android_packages ⇒ Object
Third-party dependencies for native/android code, as Gradle sees them: plain Maven coordinates.
- #native_android_packages? ⇒ Boolean
- #native_android_screens ⇒ Object
- #native_android_splash ⇒ Object
-
#native_android_splash_min_seconds ⇒ Object
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.
-
#native_desktop ⇒ Object
The desktop half of "supernative": Rust the app repo carries in native/desktop/ that
every buildcompiles into the Tauri shell. - #native_desktop? ⇒ Boolean
- #native_desktop_commands ⇒ Object
- #native_desktop_crate_errors ⇒ Object
-
#native_desktop_crates ⇒ Object
Cargo dependencies, normalized to { "name" => .....table } so the generator has one shape to write.
- #native_desktop_errors ⇒ Object
- #native_desktop_setup? ⇒ Boolean
-
#native_ios ⇒ Object
Native code extensions ("supernative"): Swift the app repo carries in native/ios/ that
every buildcompiles into the shell, plus this declaration of what to hook up. - #native_ios? ⇒ Boolean
- #native_ios_components ⇒ Object
- #native_ios_errors ⇒ Object
-
#native_ios_lazy_load_tabs ⇒ Object
Whether the native tab bar defers each non-selected tab's initial visit until the tab is first selected (Hotwire Native
lazyLoadTabs). - #native_ios_package_errors ⇒ Object
- #native_ios_packages ⇒ Object
- #native_ios_packages? ⇒ Boolean
- #native_ios_screens ⇒ Object
- #native_ios_splash ⇒ Object
-
#native_ios_splash_min_seconds ⇒ Object
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).
-
#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. - #native_splash_min_seconds(target: nil) ⇒ Object
- #oauth? ⇒ Boolean
-
#oauth_path?(path) ⇒ Boolean
Whether a request path starts a provider flow.
- #oauth_paths ⇒ Object
-
#package_identity(url) ⇒ Object
SPM's package identity: the URL's last path segment without a .git suffix (github.com/simibac/ConfettiSwiftUI → "ConfettiSwiftUI").
-
#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).
- #path_configuration_json(os, tabs: nil) ⇒ Object
-
#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.
- #permissions ⇒ Object
- #remote? ⇒ Boolean
-
#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. - #remote_url ⇒ Object
-
#splash ⇒ Object
Optional custom splash page (HTML file, path relative to the app root).
-
#tabs ⇒ Object
Mobile tab bar, platform-neutral.
-
#tabs_for(os) ⇒ Object
Tabs with the icon resolved for one platform: icons.
, then the shared icon, then a safe default. -
#targets ⇒ Object
Build targets (os-arch strings) — the CI matrix, expressed once.
- #tint_color ⇒ Object
-
#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).
- #to_shell_json(target: nil) ⇒ Object
-
#tray ⇒ Object
System tray: same entry shape as
menu:plusaction: quit | show. -
#updates ⇒ Object
The
updates:section — self-hosted auto-updates. -
#updates_auto ⇒ Object
off — never check; check — check + notify (default); download — also fetch/verify in the background; install — silent auto-update.
- #updates_channel ⇒ Object
- #updates_interval ⇒ Object
- #updates_public_key ⇒ Object
- #updates_s3 ⇒ Object
-
#updates_shell_hash ⇒ Object
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.
- #updates_url ⇒ Object
-
#version(target: nil) ⇒ Object
Marketing/display version of the app (CFBundleShortVersionString, and the
versionrecorded in the build receipt). - #window ⇒ Object
-
#window_drag_height ⇒ Object
How much of the top of the page drags the window, in CSS pixels.
- #window_errors ⇒ Object
- #window_min_size ⇒ Object
- #window_resizable ⇒ Object
-
#window_shell_hash ⇒ Object
The window subset the shell needs.
- #window_size ⇒ Object
-
#window_title ⇒ Object
Tri-state, like native_ios_lazy_load_tabs: an absent key leaves the shell on its default rather than forcing one.
- #window_title_bar ⇒ Object
Constructor Details
Instance Attribute Details
#root ⇒ Object (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_permissions ⇒ Object
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 .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_association ⇒ Object
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_json ⇒ Object
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 |
#asset_links ⇒ Object
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 |
#asset_links_json ⇒ Object
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 |
#auth ⇒ Object
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.
989 990 991 992 993 994 995 996 997 |
# File 'lib/everywhere/config.rb', line 989 def (name) = auth["cookies"] return true unless .is_a?(Hash) only = Array(["only"]).map(&:to_s) return only.include?(name.to_s) unless only.empty? !Array(["except"]).map(&:to_s).include?(name.to_s) end |
#auth_errors ⇒ Object
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.})" end unless auth_scheme.match?(URL_SCHEME) errors << "auth.scheme #{auth_scheme.inspect} is not a URL scheme (letters, digits, +, -, .)" end errors end |
#auth_scheme ⇒ Object
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_hash ⇒ Object
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_ttl ⇒ Object
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_color ⇒ Object
297 298 299 |
# File 'lib/everywhere/config.rb', line 297 def background_color normalize_color(appearance["background_color"]) end |
#build ⇒ Object
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_capabilities ⇒ Object
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_ruby ⇒ Object
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_linking ⇒ Object
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
1211 |
# File 'lib/everywhere/config.rb', line 1211 def deep_linking? = deep_linking_apple? || deep_linking_android? |
#deep_linking_android ⇒ Object
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
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_fingerprints ⇒ Object
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_package ⇒ Object
1201 |
# File 'lib/everywhere/config.rb', line 1201 def deep_linking_android_package = deep_linking_android["package"]&.to_s |
#deep_linking_apple? ⇒ Boolean
1209 |
# File 'lib/everywhere/config.rb', line 1209 def deep_linking_apple? = !deep_linking_apple_app_ids.empty? |
#deep_linking_apple_app_ids ⇒ Object
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_domains ⇒ Object
Domains for the iOS Associated Domains entitlement (applinks: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_paths ⇒ Object
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_path ⇒ Object
204 205 206 207 |
# File 'lib/everywhere/config.rb', line 204 def entry_path path = app["entry_path"] || "/" path.start_with?("/") ? path : "/#{path}" end |
#icon ⇒ Object
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.(explicit, root) else default = File.join(root, "icon.png") File.exist?(default) ? default : nil end end |
#menu ⇒ Object
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 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 |
#mode ⇒ Object
"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.(root)).split(/[-_]/).map(&:capitalize).join(" ") end |
#native_android ⇒ Object
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
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_components ⇒ Object
674 |
# File 'lib/everywhere/config.rb', line 674 def native_android_components = Array(native_android["components"]).map(&:to_s) |
#native_android_errors ⇒ Object
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_font ⇒ Object
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_codepoints ⇒ Object
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_errors ⇒ Object
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.
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_file ⇒ Object
The font's asset filename (assets/fonts/
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
817 |
# File 'lib/everywhere/config.rb', line 817 def native_android_icons? = !native_android_icon_font_file.nil? |
#native_android_lazy_load_tabs ⇒ Object
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_errors ⇒ Object
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_packages ⇒ Object
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
717 |
# File 'lib/everywhere/config.rb', line 717 def native_android_packages? = !native_android_packages.empty? |
#native_android_screens ⇒ Object
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_splash ⇒ Object
683 |
# File 'lib/everywhere/config.rb', line 683 def native_android_splash = native_android["splash"]&.to_s |
#native_android_splash_min_seconds ⇒ Object
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_desktop ⇒ Object
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
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_commands ⇒ Object
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_errors ⇒ Object
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_crates ⇒ Object
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_errors ⇒ Object
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
861 |
# File 'lib/everywhere/config.rb', line 861 def native_desktop_setup? = native_desktop["setup"] == true |
#native_ios ⇒ Object
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
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_components ⇒ Object
523 |
# File 'lib/everywhere/config.rb', line 523 def native_ios_components = Array(native_ios["components"]).map(&:to_s) |
#native_ios_errors ⇒ Object
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_tabs ⇒ Object
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_errors ⇒ Object
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_packages ⇒ Object
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
582 |
# File 'lib/everywhere/config.rb', line 582 def native_ios_packages? = !native_ios_packages.empty? |
#native_ios_screens ⇒ Object
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_splash ⇒ Object
532 |
# File 'lib/everywhere/config.rb', line 532 def native_ios_splash = native_ios["splash"]&.to_s |
#native_ios_splash_min_seconds ⇒ Object
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
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).
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_paths ⇒ Object
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 (os) .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 |
#permissions ⇒ Object
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/everywhere/config.rb', line 394 def 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
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.
253 254 255 |
# File 'lib/everywhere/config.rb', line 253 def remote_instances? @data.dig("remote", "instances") == true end |
#remote_url ⇒ Object
243 244 245 |
# File 'lib/everywhere/config.rb', line 243 def remote_url @data.dig("remote", "url")&.chomp("/") end |
#splash ⇒ Object
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.(path, root) if path end |
#tabs ⇒ Object
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/
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.
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 |
#targets ⇒ Object
Build targets (os-arch strings) — the CI matrix, expressed once.
233 |
# File 'lib/everywhere/config.rb', line 233 def targets = Array(build["targets"]) |
#tint_color ⇒ Object
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" => ( unless .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" => (.keys unless .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 |
#tray ⇒ Object
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 |
#updates ⇒ Object
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_auto ⇒ Object
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_channel ⇒ Object
266 |
# File 'lib/everywhere/config.rb', line 266 def updates_channel = updates["channel"] || "stable" |
#updates_interval ⇒ Object
276 |
# File 'lib/everywhere/config.rb', line 276 def updates_interval = [(updates["interval"] || 21_600).to_i, 300].max |
#updates_public_key ⇒ Object
270 |
# File 'lib/everywhere/config.rb', line 270 def updates_public_key = updates["public_key"] |
#updates_s3 ⇒ Object
264 |
# File 'lib/everywhere/config.rb', line 264 def updates_s3 = updates.fetch("s3", nil) || {} |
#updates_shell_hash ⇒ Object
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_url ⇒ Object
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 |
#window ⇒ Object
1091 1092 1093 1094 |
# File 'lib/everywhere/config.rb', line 1091 def window section = @data["window"] section.is_a?(Hash) ? section : {} end |
#window_drag_height ⇒ Object
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_errors ⇒ Object
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 = [] = raw["title_bar"] if && !WINDOW_TITLE_BARS.include?(.to_s) errors << "window.title_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_size ⇒ Object
1107 |
# File 'lib/everywhere/config.rb', line 1107 def window_min_size = window_dimensions("min_size") |
#window_resizable ⇒ Object
1104 |
# File 'lib/everywhere/config.rb', line 1104 def window_resizable = boolean_or_nil(window["resizable"]) |
#window_shell_hash ⇒ Object
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" => , "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_size ⇒ Object
1106 |
# File 'lib/everywhere/config.rb', line 1106 def window_size = window_dimensions("size") |
#window_title ⇒ Object
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_bar ⇒ Object
1096 1097 1098 1099 |
# File 'lib/everywhere/config.rb', line 1096 def value = window["title_bar"].to_s value if WINDOW_TITLE_BARS.include?(value) end |