diff --git a/slab/README.md b/slab/README.md index 521416a956..b7c5afe78f 100644 --- a/slab/README.md +++ b/slab/README.md @@ -21,6 +21,12 @@ What the menu bar does today: rocks, ⌃⌥⌘A flips dark mode fleet-wide. - **Status sounds.** Three synthesized cues (complete / awaiting / disconnected) that share one voice and differ only in contour. +- **Game Mode.** GeForce NOW calls Tailscale a VPN on sight of the tunnel + interface, even with no exit node routing a single packet. One switch + pauses the tailnet and pins AirDrop's `awdl0` radio down (it channel-hops + the Wi-Fi card mid-stream), and puts both back on the way out — optionally + by itself, the moment GeForce NOW opens. + (`menubar-swift/Sources/SlabMenubar/GameMode.swift`) - **Fleet.** Every Mac runs the same menu bar; they share a ledger of live sessions over the tailnet (`host:name`), `frame` photographs any desk and `puppet` acts on it. Rocks are never drawn for remote machines. diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index 6d72f82186..c77195c376 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -400,6 +400,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { // requested, so no Screen Recording prompt at launch (lazy grant). FrameCapture.shared.start() + // Game Mode watches for GeForce NOW launching/quitting. Opt-in: with + // "Auto" off this observes and does nothing, so the wiring is the + // same either way and costs a couple of workspace notifications. + GameMode.shared.startWatchingGFN() + // `reel` — the moving-picture sibling of frame. Same lazy-grant file // watcher; hardware-encodes an SCStream straight to mp4 when asked. if #available(macOS 15.0, *) { ScreenRecord.shared.start() } @@ -1687,6 +1692,48 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } + // ── Game Mode ──────────────────────────────────────────────────────── + // A clean GeForce NOW client is two unrelated fixes applied together: + // pause Tailscale (GFN calls any visible tunnel a VPN) and hold AirDrop's + // awdl0 radio down (it hops the Wi-Fi card off-channel mid-stream). See + // GameMode.swift for the measurements and the root-helper handshake. + + @objc func toggleGameMode() { + GameMode.shared.toggle() + // Tailscale's own up/down takes a beat to settle; refresh once now so + // the menu is honest immediately, and again once the tunnel is gone. + refresh() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in self?.refresh() } + } + + @objc func toggleGameModeAuto() { + GameMode.shared.setAutoDetect(!state.gameMode.autoDetect) + refresh() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in self?.refresh() } + } + + @objc func installGameModeHelper() { + if let error = GameMode.shared.installHelper() { + presentGameModeError("Could not install the radio helper", error) + } + refresh() + } + + @objc func removeGameModeHelper() { + if let error = GameMode.shared.removeHelper() { + presentGameModeError("Could not remove the radio helper", error) + } + refresh() + } + + private func presentGameModeError(_ title: String, _ detail: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = detail + alert.alertStyle = .warning + alert.runModal() + } + @objc func sleepNow() { ShellRunner.runAsync(Paths.claudeSleep, args: ["now"]) } diff --git a/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift b/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift new file mode 100644 index 0000000000..158fc45ed8 --- /dev/null +++ b/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift @@ -0,0 +1,313 @@ +import AppKit + +/// Game Mode — one switch that turns this Mac into a clean GeForce NOW client. +/// +/// GFN refuses to stream when it decides you're on a VPN, and it does not +/// inspect routing: a live `utun` carrying a 100.64/10 address, plus a DNS +/// resolver that isn't the gateway, is enough. Tailscale trips both tells +/// even with no exit node set, where not one streamed packet enters the +/// tunnel. Separately, AirDrop's `awdl0` radio hops the Wi-Fi card +/// off-channel every few seconds; measured on blueberry 2026-09-02 that is +/// the difference between avg 18 ms / max 80 ms and avg 3.5 / max 18 on the +/// gateway hop. +/// +/// Both faults are network-adjacent and neither is the other's cause, so the +/// mode does both at once and undoes both on the way out: +/// +/// on → `tailscale down` + awdl0 pinned down + AirDrop undiscoverable +/// off → `tailscale up` + awdl0 released + AirDrop restored +/// +/// Nothing else about the machine changes, and the tailnet comes back with +/// its peers intact — `down`/`up` is Tailscale's own pause, not a logout. +/// +/// Privilege: `ifconfig awdl0 down` is root-only, and macOS re-raises awdl0 +/// whenever Continuity is touched, so the holding is done by a small root +/// LaunchDaemon that watches a flag file this app owns. That costs exactly +/// one admin prompt, ever; every toggle afterwards is a file write. The +/// tailnet half needs no privilege at all, which is what lets auto-detect +/// engage unattended without a dialog ever appearing behind a running game. +final class GameMode { + static let shared = GameMode() + + static let helperLabel = "computer.aesthetic.slab.gamemode" + static var helperPlist: String { "/Library/LaunchDaemons/\(helperLabel).plist" } + /// `toolchain/macos/gfn.sh on` installs its own unconditional awdl-down + /// daemon. Two holders would fight over the same interface, so installing + /// ours evicts that one. + static let legacyLabel = "computer.aesthetic.gfn-awdl" + + static let gfnBundleID = "com.nvidia.gfnpc.mall" + + /// True when this mode engaged itself because GeForce NOW launched. Only + /// an automatic engage may auto-disengage: a hold you switched on by hand + /// outlives the app that happened to be open. + private var engagedAutomatically = false + private var watching = false + + // ── state ──────────────────────────────────────────────────────────── + + /// Read entirely from flag files and `getifaddrs` — no forks, so + /// `StateSnapshot.gather()` can call it every tick for free. + static func state() -> GameModeState { + var s = GameModeState() + let fm = FileManager.default + s.on = fm.fileExists(atPath: Paths.gameModeFlag) + s.autoDetect = fm.fileExists(atPath: Paths.gameModeAutoFlag) + s.helperInstalled = fm.fileExists(atPath: helperPlist) + let (awdlUp, tunnelVisible) = interfaces() + s.awdlUp = awdlUp + s.tailnetVisible = tunnelVisible + s.gfnRunning = !NSRunningApplication + .runningApplications(withBundleIdentifier: gfnBundleID).isEmpty + return s + } + + /// (awdl0 is up, a utun carries a 100.64/10 address). The second value is + /// precisely what GFN's VPN heuristic keys on, so the menu reports the + /// same fact the streamer is reading rather than a proxy for it. + private static func interfaces() -> (awdlUp: Bool, tunnelVisible: Bool) { + var head: UnsafeMutablePointer? + guard getifaddrs(&head) == 0, let first = head else { return (false, false) } + defer { freeifaddrs(head) } + + var awdlUp = false + var tunnel = false + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let name = String(cString: ptr.pointee.ifa_name) + if name == "awdl0", ptr.pointee.ifa_flags & UInt32(IFF_UP) != 0 { awdlUp = true } + guard name.hasPrefix("utun"), + let sa = ptr.pointee.ifa_addr, + sa.pointee.sa_family == UInt8(AF_INET) else { continue } + var addr = sockaddr_in() + memcpy(&addr, sa, MemoryLayout.size) + let ip = UInt32(bigEndian: addr.sin_addr.s_addr) + if ip & 0xFFC0_0000 == 0x6440_0000 { tunnel = true } // 100.64.0.0/10 + } + return (awdlUp, tunnel) + } + + // ── the switch ─────────────────────────────────────────────────────── + + func toggle() { + if FileManager.default.fileExists(atPath: Paths.gameModeFlag) { + disengage() + } else { + engage(automatic: false) + } + } + + /// Engage. `automatic` engages only what can be done silently, so a game + /// launching never raises an auth dialog over the stream; the awdl half + /// simply stays off until the helper is installed from the menu. + func engage(automatic: Bool) { + engagedAutomatically = automatic + try? FileManager.default.createDirectory( + atPath: (Paths.gameModeFlag as NSString).deletingLastPathComponent, + withIntermediateDirectories: true) + FileManager.default.createFile(atPath: Paths.gameModeFlag, contents: nil) + + stashAirDropMode() + setAirDrop("Off") + tailscale("down") + // The helper polls, but a direct nudge drops the radio now rather + // than up to one poll interval into the first match. + if FileManager.default.fileExists(atPath: GameMode.helperPlist) { pokeHelper() } + } + + func disengage() { + engagedAutomatically = false + try? FileManager.default.removeItem(atPath: Paths.gameModeFlag) + tailscale("up") + setAirDrop(stashedAirDropMode() ?? "Contacts Only") + try? FileManager.default.removeItem(atPath: Paths.airDropStash) + if FileManager.default.fileExists(atPath: GameMode.helperPlist) { pokeHelper() } + } + + private func tailscale(_ verb: String) { + guard let ts = Tools.resolve("tailscale") else { return } + DispatchQueue.global(qos: .userInitiated).async { + ShellRunner.run(ts, args: [verb], timeout: 20) + } + } + + /// The helper wakes on its own poll; this only shortens the wait by + /// touching the flag's directory so a watching `stat` sees it sooner. + private func pokeHelper() { + try? FileManager.default.setAttributes( + [.modificationDate: Date()], + ofItemAtPath: (Paths.gameModeFlag as NSString).deletingLastPathComponent) + } + + // AirDrop discoverability is saved and put back rather than forced to + // "Everyone", because most Macs sit on "Contacts Only" and silently + // widening that is not this feature's business. + private func stashAirDropMode() { + guard !FileManager.default.fileExists(atPath: Paths.airDropStash) else { return } + let current = ShellRunner.output( + "/usr/bin/defaults", + args: ["read", "com.apple.sharingd", "DiscoverableMode"], timeout: 5 + )?.trimmingCharacters(in: .whitespacesAndNewlines) + guard let current, !current.isEmpty, current != "Off" else { return } + try? current.write(toFile: Paths.airDropStash, atomically: true, encoding: .utf8) + } + + private func stashedAirDropMode() -> String? { + guard let raw = try? String(contentsOfFile: Paths.airDropStash, encoding: .utf8) else { return nil } + let value = raw.trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } + + private func setAirDrop(_ mode: String) { + DispatchQueue.global(qos: .utility).async { + ShellRunner.run("/usr/bin/defaults", + args: ["write", "com.apple.sharingd", "DiscoverableMode", "-string", mode], + timeout: 5) + ShellRunner.run("/usr/bin/killall", args: ["sharingd"], timeout: 5) + } + } + + // ── auto-detect ────────────────────────────────────────────────────── + + /// Watch for GeForce NOW coming and going. Opt-in: with the preference + /// off this observes and does nothing, so the notifications stay cheap + /// and the wiring is identical either way. + func startWatchingGFN() { + guard !watching else { return } + watching = true + let center = NSWorkspace.shared.notificationCenter + center.addObserver(self, selector: #selector(appLaunched(_:)), + name: NSWorkspace.didLaunchApplicationNotification, object: nil) + center.addObserver(self, selector: #selector(appTerminated(_:)), + name: NSWorkspace.didTerminateApplicationNotification, object: nil) + + // Slab restarts far more often than a game session lasts; adopt a GFN + // that is already up rather than waiting for the next launch. + let s = GameMode.state() + if s.autoDetect, s.gfnRunning, !s.on { engage(automatic: true) } + } + + @objc private func appLaunched(_ note: Notification) { + guard bundleID(note) == GameMode.gfnBundleID else { return } + let s = GameMode.state() + guard s.autoDetect, !s.on else { return } + engage(automatic: true) + } + + @objc private func appTerminated(_ note: Notification) { + guard bundleID(note) == GameMode.gfnBundleID else { return } + guard engagedAutomatically else { return } + disengage() + } + + private func bundleID(_ note: Notification) -> String? { + (note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication)? + .bundleIdentifier + } + + func setAutoDetect(_ enabled: Bool) { + if enabled { + try? FileManager.default.createDirectory( + atPath: (Paths.gameModeAutoFlag as NSString).deletingLastPathComponent, + withIntermediateDirectories: true) + FileManager.default.createFile(atPath: Paths.gameModeAutoFlag, contents: nil) + // Turning it on while the game is already open should act now, + // not at the next launch. + let s = GameMode.state() + if s.gfnRunning, !s.on { engage(automatic: true) } + } else { + try? FileManager.default.removeItem(atPath: Paths.gameModeAutoFlag) + } + } + + // ── the root helper ────────────────────────────────────────────────── + + /// The daemon body. Holding awdl0 down has to be a loop rather than a + /// one-shot because macOS re-raises the interface whenever anything + /// touches Continuity; releasing is edge-triggered so the interface is + /// brought back exactly once, not re-upped every poll against a user who + /// turned AirDrop off for their own reasons. + private static func helperScript(flag: String) -> String { + """ + held=0 + while true; do + if [ -f '\(flag)' ]; then + /sbin/ifconfig awdl0 down 2>/dev/null + held=1 + elif [ "$held" = 1 ]; then + /sbin/ifconfig awdl0 up 2>/dev/null + held=0 + fi + sleep 5 + done + """ + } + + private static func helperPlistXML(flag: String) -> String { + """ + + + + Label\(helperLabel) + ProgramArguments + /bin/bash-c + \(helperScript(flag: flag).replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">")) + + RunAtLoad + KeepAlive + + """ + } + + /// One admin prompt, once. Returns an error string on failure, nil on + /// success. Must run on the main thread — NSAppleScript puts up the + /// authorization dialog itself. + @discardableResult + func installHelper() -> String? { + let staged = NSTemporaryDirectory() + "\(GameMode.helperLabel).plist" + do { + try GameMode.helperPlistXML(flag: Paths.gameModeFlag) + .write(toFile: staged, atomically: true, encoding: .utf8) + } catch { + return "could not stage the helper: \(error.localizedDescription)" + } + // Deliberately quote-free so it survives the AppleScript string layer + // intact; every path here is a fixed location with no spaces. + let cmd = [ + "/bin/cp \(staged) \(GameMode.helperPlist)", + "/usr/sbin/chown root:wheel \(GameMode.helperPlist)", + "/bin/chmod 644 \(GameMode.helperPlist)", + "/bin/launchctl bootout system/\(GameMode.legacyLabel) 2>/dev/null", + "/bin/rm -f /Library/LaunchDaemons/\(GameMode.legacyLabel).plist", + "/bin/launchctl bootout system/\(GameMode.helperLabel) 2>/dev/null", + "/bin/launchctl bootstrap system \(GameMode.helperPlist)", + ].joined(separator: "; ") + return GameMode.runAsAdmin( + cmd, prompt: "Slab Game Mode needs to install a small helper so it can hold the AirDrop radio down while you stream.") + } + + @discardableResult + func removeHelper() -> String? { + let cmd = [ + "/bin/launchctl bootout system/\(GameMode.helperLabel) 2>/dev/null", + "/bin/rm -f \(GameMode.helperPlist)", + "/sbin/ifconfig awdl0 up 2>/dev/null", + ].joined(separator: "; ") + return GameMode.runAsAdmin(cmd, prompt: "Slab Game Mode is removing its radio helper.") + } + + private static func runAsAdmin(_ command: String, prompt: String) -> String? { + let escaped = command + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + let source = "do shell script \"\(escaped)\" with administrator privileges with prompt \"\(prompt)\"" + var err: NSDictionary? + NSAppleScript(source: source)?.executeAndReturnError(&err) + guard let err else { return nil } + // -128 is the user clicking Cancel on the auth dialog: a decision, + // not a failure worth reporting back as one. + if (err[NSAppleScript.errorNumber] as? Int) == -128 { return nil } + return (err[NSAppleScript.errorMessage] as? String) ?? "authorization failed" + } +} diff --git a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift index 808bbb5090..d89ec92603 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift @@ -89,6 +89,7 @@ enum MenuBuilder { let fleet = NSMenu() fleet.addItem(buildSystem(state: state, target: target)) fleet.addItem(buildTailnet(state: state, target: target)) + fleet.addItem(buildGameMode(state: state, target: target)) if state.deskflow.configured { fleet.addItem(buildDeskflow(state: state, target: target)) } @@ -512,6 +513,73 @@ enum MenuBuilder { return parent } + /// "Game Mode" — the GeForce NOW switch. GFN calls Tailscale a VPN on + /// sight of the tunnel interface even with no exit node, and AirDrop's + /// awdl0 radio hops the Wi-Fi card off-channel mid-stream; this drops + /// both together and puts both back. The rows below the switch report + /// the machine's real state, including the exact tell GFN reads. + private static func buildGameMode(state: StateSnapshot, target: AppDelegate) -> NSMenuItem { + let g = state.gameMode + let title: String + if g.on { + var held = ["tailnet down"] + if g.helperInstalled && !g.awdlUp { held.append("awdl held") } + title = "Game Mode: on · " + held.joined(separator: " · ") + } else { + title = g.gfnRunning ? "Game Mode: off · GFN open" : "Game Mode: off" + } + let parent = NSMenuItem(title: title, action: nil, keyEquivalent: "") + let sub = NSMenu() + sub.autoenablesItems = false + + let toggle = item(g.on ? "Leave game mode" : "Enter game mode", + selector: #selector(AppDelegate.toggleGameMode), target: target) + toggle.state = g.on ? .on : .off + toggle.toolTip = "Pause Tailscale and pin the AirDrop radio down for a clean stream, then restore both on the way out." + sub.addItem(toggle) + + let auto = item("Auto when GeForce NOW opens", + selector: #selector(AppDelegate.toggleGameModeAuto), target: target) + auto.state = g.autoDetect ? .on : .off + auto.toolTip = "Engage on GeForce NOW's launch and stand down when it quits. A hold you switched on by hand is left alone." + sub.addItem(auto) + + sub.addItem(.separator()) + sub.addItem(info(g.tailnetVisible + ? "Tailnet: up — GFN reads this as a VPN" + : "Tailnet: paused — nothing for GFN to see")) + // Down and held are different states: macOS parks awdl0 whenever + // nothing is using Continuity, and it raises it again unprompted. + // Only the helper plus an engaged mode keeps it down through a match. + let radio: String + if g.awdlUp { + radio = "up — channel-hopping" + } else if g.on && g.helperInstalled { + radio = "held down by Slab" + } else { + radio = "down (idle — macOS may raise it any time)" + } + sub.addItem(info("AirDrop radio: " + radio)) + sub.addItem(info("GeForce NOW: " + (g.gfnRunning ? "running" : "not running"))) + + sub.addItem(.separator()) + if g.helperInstalled { + let remove = item("Remove radio helper…", + selector: #selector(AppDelegate.removeGameModeHelper), target: target) + remove.toolTip = "Uninstall the root LaunchDaemon and release awdl0. Game mode keeps working; it just stops touching the radio." + sub.addItem(remove) + } else { + let install = item("Install radio helper… (one admin prompt)", + selector: #selector(AppDelegate.installGameModeHelper), target: target) + install.toolTip = "Holding awdl0 down needs root, and macOS re-raises it constantly. Installs a small daemon once; every toggle after this is free." + sub.addItem(install) + sub.addItem(info("Without it, game mode pauses the tailnet only")) + } + + parent.submenu = sub + return parent + } + /// List Claude/Codex sessions inside the Agents section. Each row is /// colored with the same palette as its icon /// edge — cyan for working, red for awaiting, gray for stale — so the diff --git a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift index fe0c39698e..2a5dcf8442 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift @@ -208,6 +208,16 @@ enum Paths { /// independent of the ambient mute, which silences the hooks' TTS/pad /// layer; this marker exists only once turned off from the menu. static var statusSoundsDisabledFlag: String { "\(slabHome)/state/status-sounds-off" } + + /// Game Mode — the GeForce NOW switch (GameMode.swift). Presence of the + /// flag means engaged; the root awdl helper reads this exact path, so it + /// is the handshake between the menubar and the daemon as well as the + /// menu's own state. The auto flag is opt-in, absent by default. + static var gameModeFlag: String { "\(slabHome)/state/game-mode" } + static var gameModeAutoFlag: String { "\(slabHome)/state/game-mode-auto" } + /// AirDrop discoverability as it was before game mode turned it off, + /// so leaving the mode restores the user's setting instead of a guess. + static var airDropStash: String { "\(slabHome)/state/game-mode-airdrop" } } enum Tools { diff --git a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift index d51f19f23e..f9705afc03 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift @@ -124,6 +124,20 @@ struct DeployStatusState { var url: String = "" } +/// Game Mode's live picture, all of it cheap to read (flag files + +/// getifaddrs), so the menu can show what GFN itself is seeing. +struct GameModeState { + var on = false // the flag file — mode engaged + var autoDetect = false // engage by itself when GeForce NOW opens + var helperInstalled = false // root awdl daemon present + /// awdl0 is up. macOS idles this interface down on its own, so "down" + /// alone never means Slab is holding it — only the helper plus an + /// engaged mode does, which is why the menu reports the two apart. + var awdlUp = false + var tailnetVisible = true // a utun carries 100.64/10 — GFN's VPN tell + var gfnRunning = false +} + struct StateSnapshot { var lidClosed: Bool = false var sleepDisabled: Bool = false @@ -166,6 +180,8 @@ struct StateSnapshot { /// Path to the active recording WAV when callRecording is true. /// Empty otherwise. var callRecordingPath: String = "" + /// GeForce NOW game mode — tailnet paused + AirDrop radio pinned down. + var gameMode: GameModeState = GameModeState() var totalActive: Int { activePrompts + activeSubagents } var hasWork: Bool { totalActive > 0 } @@ -222,6 +238,7 @@ struct StateSnapshot { let (rec, recPath) = readCallRecordingState() s.callRecording = rec s.callRecordingPath = recPath + s.gameMode = GameMode.state() return s } diff --git a/toolchain/macos/gfn.sh b/toolchain/macos/gfn.sh index 411d919253..5eddcc37e6 100755 --- a/toolchain/macos/gfn.sh +++ b/toolchain/macos/gfn.sh @@ -10,8 +10,16 @@ # macOS re-raises awdl0 whenever AirDrop/Continuity is touched, so `on` # installs a root LaunchDaemon that re-lowers it every 15 s; `off` removes it. # Privilege: sudo when there's a TTY, else a macOS admin dialog (works from -# Claude's `!` runner). Tailscale is left alone: no exit node, so GFN traffic -# never enters it. +# Claude's `!` runner). Tailscale is left alone here — but note that GFN's own +# VPN check does NOT care about routing: a visible utun holding a 100.64/10 +# address is enough for it to refuse to stream, exit node or not. +# +# Slab's menu bar now does both halves together (Fleet & System → Game Mode): +# tailnet paused + this same awdl hold, engaged by hand or automatically when +# GeForce NOW launches. Installing Slab's helper evicts this script's daemon +# so the two never fight over the interface — see +# slab/menubar-swift/Sources/SlabMenubar/GameMode.swift. This script remains +# the no-Slab path and the measurement tool (`gfn.sh check`). set -u LABEL=computer.aesthetic.gfn-awdl