149 lines
6.3 KiB
Swift
149 lines
6.3 KiB
Swift
import SwiftUI
|
|
|
|
/// How this copy of Nucleic was built. Selected at build time by the
|
|
/// `NUCLEIC_CHANNEL` make/env setting (see Package.swift), which also sets the
|
|
/// executable's process name. Drives the warning header that keeps a non-release
|
|
/// build from being mistaken for a shipping release.
|
|
enum BuildChannel {
|
|
/// The `dev` channel — process `nucleic-local`, red "Local Development Build" banner.
|
|
case local
|
|
/// The `canary` channel — process `nucleic-canary`, canary-yellow "Canary" banner. The
|
|
/// bleeding-edge distributed channel, cut before beta (see BUILD.md).
|
|
case canary
|
|
/// The `beta` channel — process `nucleic-beta`, blue "Beta" banner.
|
|
case beta
|
|
/// The `rc` channel — process `nucleic-rc`, gold "Release Candidate" banner.
|
|
case releaseCandidate
|
|
/// The `stable` channel — process `nucleic`, no banner.
|
|
case release
|
|
}
|
|
|
|
/// Build identity for the running app: which channel it came from and the commit it
|
|
/// was built from. The commit is embedded at build time by the `EmbedGitCommit`
|
|
/// prebuild plugin (`GitCommit.hash`), so it's baked into the binary and stays
|
|
/// correct even on a packaged beta / rc / stable archive where git isn't present.
|
|
struct BuildInfo {
|
|
let channel: BuildChannel
|
|
/// The short git commit the binary was built from (e.g. "a1b2c3d", "+" suffix when
|
|
/// the tree was dirty). Falls back to `CFBundleVersion` / "local" if the plugin
|
|
/// couldn't resolve a hash.
|
|
let buildLabel: String
|
|
/// `CFBundleShortVersionString` when present, else `nil`.
|
|
let version: String?
|
|
/// The monotonic `CFBundleVersion` build number (from `NUCLEIC_BUILD_NUMBER`),
|
|
/// or `nil` when not present in the bundle.
|
|
let buildNumber: String?
|
|
|
|
/// Resolved once — none of this changes while the app is running.
|
|
static let current = BuildInfo()
|
|
|
|
init() {
|
|
let info = Bundle.main.infoDictionary
|
|
version = info?["CFBundleShortVersionString"] as? String
|
|
buildNumber = info?["CFBundleVersion"] as? String
|
|
let commit = GitCommit.hash
|
|
buildLabel = (commit == "unknown" ? nil : commit)
|
|
?? (info?["CFBundleVersion"] as? String)
|
|
?? "local"
|
|
channel = Self.detectChannel()
|
|
}
|
|
|
|
/// The channel is fixed at build time by the `NUCLEIC_CHANNEL` setting, which
|
|
/// Package.swift turns into one of these defines. Defaults to `.local` (the `dev`
|
|
/// channel) when nothing is set.
|
|
private static func detectChannel() -> BuildChannel {
|
|
#if NUCLEIC_STABLE
|
|
return .release
|
|
#elseif NUCLEIC_RC
|
|
return .releaseCandidate
|
|
#elseif NUCLEIC_BETA
|
|
return .beta
|
|
#elseif NUCLEIC_CANARY
|
|
return .canary
|
|
#else
|
|
return .local
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// Header strip warning that the window is a non-release build: a red tint for
|
|
/// locally-built / pre-prod copies, canary-yellow for canary, blue for beta, gold for a release candidate. The
|
|
/// leading edge names the channel; the commit hash and an all-caps "EXPERIMENTAL"
|
|
/// badge sit on the trailing
|
|
/// edge. The tint is kept translucent so it reads as a quiet marker rather than
|
|
/// fighting the window background. Renders nothing for a shipping App Store release.
|
|
struct BuildBanner: View {
|
|
var info: BuildInfo = .current
|
|
|
|
var body: some View {
|
|
if let style = Style(channel: info.channel) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: style.icon)
|
|
.font(.system(size: style.iconSize, weight: .heavy))
|
|
Text(style.label)
|
|
.font(.body.weight(.semibold))
|
|
Spacer(minLength: 12)
|
|
Text(info.buildLabel)
|
|
.font(.caption.monospaced())
|
|
.opacity(0.85)
|
|
Text("EXPERIMENTAL")
|
|
.font(.caption2.weight(.heavy))
|
|
.tracking(0.9)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(.white.opacity(0.16), in: .capsule)
|
|
}
|
|
.foregroundStyle(.white.opacity(0.92))
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 5)
|
|
.frame(maxWidth: .infinity)
|
|
// A plain translucent tint, not its own glass: this strip sits directly
|
|
// under the window's unified toolbar, whose Liquid Glass samples and blurs it
|
|
// — so the *toolbar* is the glassy header and this is just its colored
|
|
// backdrop. Giving the strip its own `glassEffect` drew a bright glass edge on
|
|
// it; the toolbar's own glass is pinned visible in `RootView` instead.
|
|
.background(style.tint.opacity(0.5))
|
|
.help(style.tooltip(version: info.version, build: info.buildLabel))
|
|
}
|
|
}
|
|
|
|
/// Per-channel appearance. `nil` for `.release` so the banner disappears entirely.
|
|
private struct Style {
|
|
var tint: Color
|
|
var label: String
|
|
var icon: String
|
|
// Most channels share the default glyph size; the canary bell-with-waves
|
|
// glyph is unusually wide, so it's shrunk to sit in line with its label.
|
|
var iconSize: CGFloat = 17
|
|
|
|
init?(channel: BuildChannel) {
|
|
switch channel {
|
|
case .local:
|
|
tint = Color(red: 0.78, green: 0.18, blue: 0.18) // red
|
|
label = "Local Development Build"
|
|
icon = "hammer.fill"
|
|
case .canary:
|
|
tint = Color(red: 1.00, green: 0.87, blue: 0.00) // bright canary yellow
|
|
label = "Canary Build"
|
|
icon = "bell.and.waves.left.and.right.fill"
|
|
iconSize = 13
|
|
case .beta:
|
|
tint = Color(red: 0.13, green: 0.40, blue: 0.86) // blue
|
|
label = "Public Beta"
|
|
icon = "person.3.fill"
|
|
case .releaseCandidate:
|
|
tint = Color(red: 0.83, green: 0.65, blue: 0.12) // gold
|
|
label = "Release Candidate"
|
|
icon = "checkmark.seal.fill"
|
|
case .release:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func tooltip(version: String?, build: String) -> String {
|
|
let v = version.map { "\($0) " } ?? ""
|
|
return "\(label) — \(v)commit \(build). Not a release build."
|
|
}
|
|
}
|
|
}
|