From 9aea4b0dbcea7cde33b1bdc4544976293effd889 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sun, 16 Aug 2026 13:05:29 -0400 Subject: [PATCH] =?UTF-8?q?The=20'part=20of=20the=20performance'=20light:?= =?UTF-8?q?=20a=20room=20receiver=20announces=20itself=20to=20its=20OWN=20?= =?UTF-8?q?Mac's=20Menu=20Band=20over=20a=20local=20roomGuest=20distribute?= =?UTF-8?q?d=20notification=20(start,=2030=20s=20heartbeat,=20atexit=20goo?= =?UTF-8?q?dbye),=20and=20that=20Menu=20Band=20spins=20the=20Juke=20disc?= =?UTF-8?q?=20reading=20'room=20right=20=E2=86=90=20neo'=20while=20the=20m?= =?UTF-8?q?achine=20carries=20someone=20else's=20channel=20=E2=80=94=20gua?= =?UTF-8?q?rded=20so=20the=20badge=20never=20covers=20a=20real=20local=20J?= =?UTF-8?q?uke=20display,=20and=20self-retiring=20after=20three=20missed?= =?UTF-8?q?=20heartbeats=20so=20a=20SIGKILLed=20receiver=20can't=20leave?= =?UTF-8?q?=20a=20phantom=20disc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/ACAudioRoomCLI/main.swift | 25 +++++++++ .../Sources/MenuBand/AppDelegate.swift | 52 +++++++++++++++++++ .../MenuBand/MenuBandJukeStatusItem.swift | 3 ++ 3 files changed, 80 insertions(+) diff --git a/slab/macos-audio/Sources/ACAudioRoomCLI/main.swift b/slab/macos-audio/Sources/ACAudioRoomCLI/main.swift index d8071d8b3b..7bafffccae 100644 --- a/slab/macos-audio/Sources/ACAudioRoomCLI/main.swift +++ b/slab/macos-audio/Sources/ACAudioRoomCLI/main.swift @@ -24,6 +24,20 @@ private func emit(_ line: String) { FileHandle.standardError.write(Data((line + "\n").utf8)) } +// Room-guest beacon: a receiver announces itself to the LOCAL Menu Band so +// its Juke disc can spin while this Mac carries a channel of another Mac's +// room. Globals because the atexit/signal paths can't capture context. +var roomGuestInfo: (host: String, channel: String)? +func postRoomGuest(active: Bool) { + guard let info = roomGuestInfo else { return } + DistributedNotificationCenter.default().postNotificationName( + NSNotification.Name("computer.aestheticcomputer.menuband.roomGuest"), + object: nil, + userInfo: ["active": active ? "1" : "0", + "host": info.host, "channel": info.channel], + deliverImmediately: true) +} + let args = Array(CommandLine.arguments.dropFirst()) guard let command = args.first else { usage() } let port = UInt16(value(after: "--port", in: args) ?? "") ?? ACRoomWire.defaultPort @@ -44,9 +58,20 @@ do { receiver.onLog = { emit("room receive · \($0)") } receiver.onStarved = { emit("room receive · sender lost — exiting") + postRoomGuest(active: false) exit(0) } try receiver.start() + // Announce to the local Menu Band: start, 30 s heartbeat (covers a + // SIGKILL that skips the goodbye), and a best-effort exit goodbye. + roomGuestInfo = (host.replacingOccurrences(of: ".local", with: ""), + channel.name) + postRoomGuest(active: true) + atexit { postRoomGuest(active: false) } + let heartbeat = DispatchSource.makeTimerSource() + heartbeat.schedule(deadline: .now() + 30, repeating: 30) + heartbeat.setEventHandler { postRoomGuest(active: true) } + heartbeat.resume() dispatchMain() case "send": diff --git a/slab/menuband/Sources/MenuBand/AppDelegate.swift b/slab/menuband/Sources/MenuBand/AppDelegate.swift index 7e3efefb6f..4ad7744cca 100644 --- a/slab/menuband/Sources/MenuBand/AppDelegate.swift +++ b/slab/menuband/Sources/MenuBand/AppDelegate.swift @@ -1393,6 +1393,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate { object: nil ) +#if !MAC_APP_STORE + // A room-audio receiver on THIS Mac (the ac-audio-room CLI, spawned + // by another Mac's Juke) announces itself so the Juke disc spins + // while this machine carries a channel of someone else's room — + // the "part of the performance" light. + DistributedNotificationCenter.default().addObserver( + self, + selector: #selector(handleRoomGuestNotification(_:)), + name: NSNotification.Name("computer.aestheticcomputer.menuband.roomGuest"), + object: nil + ) +#endif + // Live engine: a conductible drone/arp/drum loop that runs // indefinitely and morphs on command (see MenuBandEngine). Four // verbs — start / chord / pattern / stop — let the fleet evolve a @@ -4294,6 +4307,45 @@ final class AppDelegate: NSObject, NSApplicationDelegate { stopScore(broadcast: true) } +#if !MAC_APP_STORE + /// True while the Juke disc is showing a room-guest state (this Mac + /// rendering a channel of another Mac's room) rather than a real local + /// Juke session — so we only ever hide what we ourselves showed. + private var roomGuestShowing = false + private var roomGuestExpiry: Timer? + + @objc private func handleRoomGuestNotification(_ note: Notification) { + let info = note.userInfo as? [String: String] ?? [:] + DispatchQueue.main.async { [weak self] in + guard let self, let item = self.jukeStatusItem else { return } + if info["active"] == "1" { + // Never cover a real Juke display with the guest badge. + guard self.roomGuestShowing || !item.isShowing else { return } + self.roomGuestShowing = true + let channel = info["channel"] ?? "guest" + let host = info["host"] ?? "room" + item.update(title: "room \(channel) ← \(host)", artist: "", + disc: nil, playing: true) + // The receiver heartbeats every 30 s; three missed beats + // (SIGKILL, network gone) and the badge retires itself. + self.roomGuestExpiry?.invalidate() + self.roomGuestExpiry = Timer.scheduledTimer( + withTimeInterval: 90, repeats: false + ) { [weak self] _ in + guard let self, self.roomGuestShowing else { return } + self.roomGuestShowing = false + self.jukeStatusItem?.hide() + } + } else if self.roomGuestShowing { + self.roomGuestShowing = false + self.roomGuestExpiry?.invalidate() + self.roomGuestExpiry = nil + item.hide() + } + } + } +#endif + /// See the `.fluoddity` observer registration for the userInfo contract. /// Order matters: seed/mutate first, then the enable flip, so one posting /// can pick a genome and switch on in the same breath. diff --git a/slab/menuband/Sources/MenuBand/MenuBandJukeStatusItem.swift b/slab/menuband/Sources/MenuBand/MenuBandJukeStatusItem.swift index eeb160b285..faa41fb6d6 100644 --- a/slab/menuband/Sources/MenuBand/MenuBandJukeStatusItem.swift +++ b/slab/menuband/Sources/MenuBand/MenuBandJukeStatusItem.swift @@ -15,6 +15,9 @@ final class MenuBandJukeStatusItem { private let side: CGFloat = 22 private static let maxTitleLength = 34 var onClick: (() -> Void)? + /// Whether the item is currently on the menu bar — lets the room-guest + /// path avoid covering a real Juke display. + var isShowing: Bool { statusItem.isVisible } /// Built fresh on each right-click so it can report what is actually on /// screen right now. var menuProvider: (() -> NSMenu)? -- 2.54.0