Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menubar-swift: native appkit rewrite with sf-symbol icons + passphrase modal

replaces the rumps/python menubar with a swift binary. icon is an sf
symbol that shifts by state: stack-fill when working, moon-zzz when
lid-closed-napping, triangle-warning when lid-closed-not-napping,
waveform-path-ecg when ambient.

passphrase modal ships here instead of being deferred — unix socket
server at ~/.ac-daemon.sock with an in-memory ttl cache, native
NSAlert + NSSecureTextField for entry.

install.sh builds release mode, copies the binary to
~/.local/bin/slab-menubar, rewrites the launchd plist, and bounces
the agent. idempotent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+935
+5
slab/menubar-swift/.gitignore
··· 1 + .build/ 2 + .swiftpm/ 3 + Package.resolved 4 + DerivedData/ 5 + *.xcodeproj/
+13
slab/menubar-swift/Package.swift
··· 1 + // swift-tools-version:5.9 2 + import PackageDescription 3 + 4 + let package = Package( 5 + name: "slab-menubar-swift", 6 + platforms: [.macOS(.v11)], 7 + targets: [ 8 + .executableTarget( 9 + name: "slab-menubar-swift", 10 + path: "Sources/SlabMenubar" 11 + ), 12 + ] 13 + )
+118
slab/menubar-swift/README.md
··· 1 + # slab-menubar (Swift) 2 + 3 + Native macOS menu-bar app for the slab workflow — replaces `slab/bin/slab-menubar.py` (rumps/Python). 4 + 5 + ## Why swift 6 + 7 + - One language across the whole slab stack (the lid-ambient synth is already Swift). 8 + - No Python venv → no venv breakage, no numpy/sounddevice install fragility on a fresh Mac. 9 + - Faster cold start, lower memory, no GIL. 10 + - SF Symbols + native `NSStatusItem` give dynamic, template-aware icons that adapt to light/dark menu bars. 11 + - The passphrase-modal feature (deferred in Python) ships here. 12 + 13 + ## What's in the menu bar 14 + 15 + The status-item icon changes by state: 16 + 17 + | State | SF Symbol | 18 + | -------------------------------------------------- | ----------------------------------- | 19 + | idle (no work) | `square.stack.3d.up` (outline) | 20 + | N active, lid open | `square.stack.3d.up.fill` + `N` | 21 + | N active, lid closed, sleep disabled (working) | `moon.zzz.fill` + `N` | 22 + | N active, lid closed, sleep NOT disabled (⚠ drift) | `exclamationmark.triangle.fill` + N | 23 + | ambient synth running | `waveform.path.ecg` | 24 + 25 + Menu contents are parity with the Python version: 26 + 27 + - status line + active-prompt / active-subagent counts 28 + - Tailnet submenu (click peer → opens `ssh <host>` in Terminal.app) 29 + - Mail submenu (`mbsync` + `mu index`, open sync log) 30 + - Stay-awake toggle / Sleep now 31 + - Open daemon log / Open sounds folder 32 + - Reload daemon / Quit menu bar 33 + 34 + Refresh cadence: 2 s. Mail unread count refreshes every 30 s (15 ticks). 35 + 36 + ## Passphrase modal (IPC server) 37 + 38 + The app listens on a Unix domain socket at `~/.ac-daemon.sock` (mode 0600). Any script on this user's account can request a passphrase. 39 + 40 + ### Request / response 41 + 42 + ```json 43 + → {"op":"passphrase","label":"vault-ssh","timeout":600} 44 + ← {"ok":true,"secret":"…","cached":false} 45 + ``` 46 + 47 + - `timeout` is the in-memory cache TTL in seconds (default 600). 48 + - Repeated requests for the same `label` return the cached secret with `"cached":true` until TTL expires. 49 + - Cancel: `← {"ok":false,"cancelled":true}`. 50 + - Forget one: `→ {"op":"forget","label":"vault-ssh"}`. Forget all: omit `label`. 51 + - Ping: `→ {"op":"ping"} → {"ok":true,"pong":true}`. 52 + 53 + ### Fish caller example 54 + 55 + ```fish 56 + set req '{"op":"passphrase","label":"vault","timeout":600}' 57 + set resp (echo $req | nc -U ~/.ac-daemon.sock) 58 + set phrase (echo $resp | jq -r '.secret // empty') 59 + ``` 60 + 61 + The modal is a native `NSAlert` + `NSSecureTextField`, brought to the front with `NSApp.activate(ignoringOtherApps:)`. 62 + 63 + ## Layout 64 + 65 + ``` 66 + menubar-swift/ 67 + ├── Package.swift 68 + ├── install.sh # build + install + launchctl 69 + ├── computer.slab.menubar.plist.tmpl # launchd agent template (@HOME@ is substituted) 70 + └── Sources/SlabMenubar/ 71 + ├── main.swift # NSApplication bootstrap 72 + ├── AppDelegate.swift # status item, timer, menu actions 73 + ├── Paths.swift # all path + tool-location constants 74 + ├── ShellRunner.swift # Process wrapper (sync + async) 75 + ├── StateSnapshot.swift # polls lid/pmset/prompt dirs/ambient flag 76 + ├── TailnetPeer.swift # `tailscale status --json` parser 77 + ├── IconRenderer.swift # SF Symbol picker per state 78 + ├── MenuBuilder.swift # NSMenu construction 79 + ├── PassphraseModal.swift # NSAlert + NSSecureTextField 80 + └── PassphraseServer.swift # Unix-socket JSON server with in-memory cache 81 + ``` 82 + 83 + ## Build & install 84 + 85 + ``` 86 + ./install.sh 87 + ``` 88 + 89 + Idempotent. Re-run after edits to rebuild, replace the binary, and bounce the launch agent. 90 + 91 + ### Manual 92 + 93 + ``` 94 + swift build -c release 95 + cp .build/release/slab-menubar-swift ~/.local/bin/slab-menubar 96 + sed "s|@HOME@|$HOME|g" computer.slab.menubar.plist.tmpl > ~/Library/LaunchAgents/computer.slab.menubar.plist 97 + launchctl unload ~/Library/LaunchAgents/computer.slab.menubar.plist 2>/dev/null || true 98 + launchctl load ~/Library/LaunchAgents/computer.slab.menubar.plist 99 + ``` 100 + 101 + Logs: `/tmp/slab-menubar.out`, `/tmp/slab-menubar.err`. 102 + 103 + ## Relation to slab/install.sh 104 + 105 + The Swift menubar replaces the Python menubar — `slab/install.sh` still sets up the daemon, hooks, sounds, and sudoers. Running `menubar-swift/install.sh` after that repoints the menubar plist at the compiled binary. Run order for a fresh Mac: 106 + 107 + ``` 108 + slab/install.sh # daemon + hooks + sounds + python menubar 109 + slab/menubar-swift/install.sh # overlay: replace python menubar with swift 110 + ``` 111 + 112 + Uninstall is handled by `slab/uninstall.sh`, which removes `computer.slab.menubar.plist` regardless of which binary it points to. 113 + 114 + ## Future 115 + 116 + - Hot reload via [Inject](https://github.com/krzysztofzablocki/Inject): split into host exec + dylib so edits land in-process. 117 + - First-class callers: `devault.fish`, `lith/deploy.fish`, npm session scripts. 118 + - Richer status: lid sensor sparkline, ambient-synth peak, SSH session count.
+158
slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift
··· 1 + import AppKit 2 + 3 + final class AppDelegate: NSObject, NSApplicationDelegate { 4 + private var statusItem: NSStatusItem! 5 + private var refreshTimer: Timer? 6 + private var mailTickCount = 0 7 + private var mailPending = false 8 + private var mailSyncing = false 9 + private var mailStatus = "—" 10 + private var state = StateSnapshot() 11 + private let passphraseServer = PassphraseServer() 12 + 13 + func applicationDidFinishLaunching(_ notification: Notification) { 14 + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 15 + statusItem.button?.imagePosition = .imageLeading 16 + 17 + do { 18 + try passphraseServer.start() 19 + } catch { 20 + NSLog("slab passphrase server failed to start: \(error)") 21 + } 22 + 23 + refresh() 24 + let timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in 25 + self?.refresh() 26 + } 27 + refreshTimer = timer 28 + RunLoop.main.add(timer, forMode: .common) 29 + } 30 + 31 + func applicationWillTerminate(_ notification: Notification) { 32 + passphraseServer.stop() 33 + } 34 + 35 + // MARK: - Refresh 36 + 37 + private func refresh() { 38 + state = StateSnapshot.gather() 39 + 40 + if let button = statusItem.button { 41 + button.image = IconRenderer.image(for: state) 42 + button.title = state.totalActive > 0 ? " \(state.totalActive)" : "" 43 + } 44 + 45 + mailTickCount += 1 46 + if mailTickCount >= 15 && !mailPending && !mailSyncing { 47 + mailTickCount = 0 48 + refreshMailCount() 49 + } 50 + 51 + statusItem.menu = MenuBuilder.build(state: state, mailStatus: mailStatus, target: self) 52 + } 53 + 54 + private func refreshMailCount() { 55 + guard let mu = Tools.resolve("mu") else { 56 + mailStatus = "mu not found" 57 + return 58 + } 59 + mailPending = true 60 + DispatchQueue.global(qos: .utility).async { [weak self] in 61 + let out = ShellRunner.output(mu, args: ["find", "flag:unread", "AND", "NOT", "flag:trashed"], timeout: 5) ?? "" 62 + let count = out.split(separator: "\n").filter { !$0.isEmpty }.count 63 + let status = count > 0 ? "\(count) unread" : "no unread" 64 + DispatchQueue.main.async { 65 + self?.mailStatus = status 66 + self?.mailPending = false 67 + } 68 + } 69 + } 70 + 71 + // MARK: - Menu actions 72 + 73 + @objc func openDaemonLog() { 74 + ShellRunner.run("/usr/bin/open", args: ["-a", "Console", Paths.lidLog]) 75 + } 76 + 77 + @objc func openSoundsFolder() { 78 + ShellRunner.run("/usr/bin/open", args: [Paths.soundsDir]) 79 + } 80 + 81 + @objc func reloadDaemon() { 82 + DispatchQueue.global(qos: .userInitiated).async { 83 + ShellRunner.run("/bin/launchctl", args: ["unload", Paths.daemonPlist]) 84 + ShellRunner.run("/bin/launchctl", args: ["load", Paths.daemonPlist]) 85 + } 86 + } 87 + 88 + @objc func quitMenubar() { 89 + DispatchQueue.global(qos: .userInitiated).async { 90 + ShellRunner.run("/bin/launchctl", args: ["unload", Paths.menubarPlist]) 91 + DispatchQueue.main.async { 92 + NSApp.terminate(nil) 93 + } 94 + } 95 + } 96 + 97 + @objc func toggleStayAwake() { 98 + let arg = state.sleepDisabled ? "auto" : "awake" 99 + ShellRunner.runAsync(Paths.claudeSleep, args: [arg]) { [weak self] in 100 + DispatchQueue.main.async { self?.refresh() } 101 + } 102 + } 103 + 104 + @objc func sleepNow() { 105 + ShellRunner.runAsync(Paths.claudeSleep, args: ["now"]) 106 + } 107 + 108 + @objc func syncBoth() { syncMail(account: nil) } 109 + @objc func syncAcMail() { syncMail(account: "ac-mail") } 110 + @objc func syncJasMail() { syncMail(account: "jas-mail") } 111 + 112 + @objc func openSyncLog() { 113 + if FileManager.default.fileExists(atPath: Paths.mailSyncLog) { 114 + ShellRunner.run("/usr/bin/open", args: ["-a", "Console", Paths.mailSyncLog]) 115 + } else { 116 + notify(title: "slab", subtitle: "Mail sync", body: "No sync log yet.") 117 + } 118 + } 119 + 120 + @objc func sshPeer(_ sender: NSMenuItem) { 121 + guard let host = sender.representedObject as? String else { return } 122 + let script = "tell application \"Terminal\" to do script \"ssh \(host)\"" 123 + ShellRunner.runAsync("/usr/bin/osascript", args: ["-e", script]) 124 + } 125 + 126 + private func syncMail(account: String?) { 127 + mailSyncing = true 128 + mailStatus = "syncing…" 129 + statusItem.menu = MenuBuilder.build(state: state, mailStatus: mailStatus, target: self) 130 + 131 + let target = account ?? "-a" 132 + let log = Paths.mailSyncLog.replacingOccurrences(of: "\"", with: "\\\"") 133 + let logDir = (Paths.mailSyncLog as NSString).deletingLastPathComponent 134 + try? FileManager.default.createDirectory(atPath: logDir, withIntermediateDirectories: true) 135 + 136 + let command = "mbsync \(target) >> \"\(log)\" 2>&1 && mu index --quiet >> \"\(log)\" 2>&1" 137 + ShellRunner.runShellAsync(command) { [weak self] in 138 + DispatchQueue.main.async { 139 + self?.mailSyncing = false 140 + self?.refreshMailCount() 141 + } 142 + } 143 + } 144 + 145 + private func notify(title: String, subtitle: String?, body: String) { 146 + let alert = NSAlert() 147 + alert.messageText = title 148 + var info = body 149 + if let subtitle = subtitle, !subtitle.isEmpty { 150 + info = "\(subtitle)\n\n\(body)" 151 + } 152 + alert.informativeText = info 153 + alert.alertStyle = .informational 154 + alert.addButton(withTitle: "OK") 155 + NSApp.activate(ignoringOtherApps: true) 156 + alert.runModal() 157 + } 158 + }
+44
slab/menubar-swift/Sources/SlabMenubar/IconRenderer.swift
··· 1 + import AppKit 2 + 3 + enum IconRenderer { 4 + static func image(for state: StateSnapshot) -> NSImage { 5 + let name: String 6 + let weight: NSFont.Weight 7 + 8 + if state.ambientActive { 9 + name = "waveform.path.ecg" 10 + weight = .medium 11 + } else if state.hasWork && state.lidClosed && !state.sleepDisabled { 12 + name = "exclamationmark.triangle.fill" 13 + weight = .semibold 14 + } else if state.hasWork && state.lidClosed { 15 + name = "moon.zzz.fill" 16 + weight = .semibold 17 + } else if state.hasWork { 18 + name = "square.stack.3d.up.fill" 19 + weight = .semibold 20 + } else { 21 + name = "square.stack.3d.up" 22 + weight = .regular 23 + } 24 + 25 + let config = NSImage.SymbolConfiguration(pointSize: 14, weight: weight, scale: .medium) 26 + let base = NSImage(systemSymbolName: name, accessibilityDescription: "slab: \(state.statusLine)") 27 + ?? fallbackImage() 28 + let configured = base.withSymbolConfiguration(config) ?? base 29 + configured.isTemplate = true 30 + return configured 31 + } 32 + 33 + private static func fallbackImage() -> NSImage { 34 + let size = NSSize(width: 16, height: 16) 35 + let img = NSImage(size: size) 36 + img.lockFocus() 37 + NSColor.labelColor.setStroke() 38 + let path = NSBezierPath(ovalIn: NSRect(x: 3, y: 3, width: 10, height: 10)) 39 + path.lineWidth = 1.5 40 + path.stroke() 41 + img.unlockFocus() 42 + return img 43 + } 44 + }
+91
slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift
··· 1 + import AppKit 2 + 3 + enum MenuBuilder { 4 + static func build(state: StateSnapshot, mailStatus: String, target: AppDelegate) -> NSMenu { 5 + let menu = NSMenu() 6 + menu.autoenablesItems = false 7 + 8 + menu.addItem(info("Status: \(state.statusLine)")) 9 + menu.addItem(.separator()) 10 + 11 + menu.addItem(info("Prompts in flight: \(state.activePrompts)")) 12 + menu.addItem(info("Subagents in flight: \(state.activeSubagents)")) 13 + menu.addItem(.separator()) 14 + 15 + menu.addItem(buildTailnet(state: state, target: target)) 16 + menu.addItem(buildMail(status: mailStatus, target: target)) 17 + menu.addItem(.separator()) 18 + 19 + let stayAwake = item("Stay awake (lid closed)", selector: #selector(AppDelegate.toggleStayAwake), target: target) 20 + stayAwake.state = state.sleepDisabled ? .on : .off 21 + menu.addItem(stayAwake) 22 + menu.addItem(item("Sleep now", selector: #selector(AppDelegate.sleepNow), target: target)) 23 + menu.addItem(.separator()) 24 + 25 + menu.addItem(item("Open daemon log", selector: #selector(AppDelegate.openDaemonLog), target: target)) 26 + menu.addItem(item("Open sounds folder", selector: #selector(AppDelegate.openSoundsFolder), target: target)) 27 + menu.addItem(.separator()) 28 + 29 + menu.addItem(item("Reload daemon", selector: #selector(AppDelegate.reloadDaemon), target: target)) 30 + menu.addItem(item("Quit menu bar", selector: #selector(AppDelegate.quitMenubar), target: target)) 31 + 32 + return menu 33 + } 34 + 35 + private static func info(_ title: String) -> NSMenuItem { 36 + let it = NSMenuItem(title: title, action: nil, keyEquivalent: "") 37 + it.isEnabled = false 38 + return it 39 + } 40 + 41 + private static func item(_ title: String, selector: Selector, target: AnyObject) -> NSMenuItem { 42 + let it = NSMenuItem(title: title, action: selector, keyEquivalent: "") 43 + it.target = target 44 + it.isEnabled = true 45 + return it 46 + } 47 + 48 + private static func buildTailnet(state: StateSnapshot, target: AppDelegate) -> NSMenuItem { 49 + let online = state.tailnetPeers.filter { $0.online }.count 50 + let total = state.tailnetPeers.count 51 + let label: String 52 + if total == 0 { 53 + label = "Tailnet: —" 54 + } else { 55 + label = "Tailnet: \(online)/\(total) online" 56 + } 57 + let parent = NSMenuItem(title: label, action: nil, keyEquivalent: "") 58 + let sub = NSMenu() 59 + 60 + if state.tailnetPeers.isEmpty { 61 + sub.addItem(info("(no peers)")) 62 + } else { 63 + for peer in state.tailnetPeers { 64 + let marker = peer.online ? "●" : "○" 65 + let entry = NSMenuItem( 66 + title: "\(marker) \(peer.hostname)", 67 + action: #selector(AppDelegate.sshPeer(_:)), 68 + keyEquivalent: "" 69 + ) 70 + entry.target = target 71 + entry.representedObject = peer.hostname 72 + entry.isEnabled = true 73 + sub.addItem(entry) 74 + } 75 + } 76 + parent.submenu = sub 77 + return parent 78 + } 79 + 80 + private static func buildMail(status: String, target: AppDelegate) -> NSMenuItem { 81 + let parent = NSMenuItem(title: "Mail: \(status)", action: nil, keyEquivalent: "") 82 + let sub = NSMenu() 83 + sub.addItem(item("Sync both", selector: #selector(AppDelegate.syncBoth), target: target)) 84 + sub.addItem(item("Sync ac-mail", selector: #selector(AppDelegate.syncAcMail), target: target)) 85 + sub.addItem(item("Sync jas-mail", selector: #selector(AppDelegate.syncJasMail), target: target)) 86 + sub.addItem(.separator()) 87 + sub.addItem(item("Open sync log", selector: #selector(AppDelegate.openSyncLog), target: target)) 88 + parent.submenu = sub 89 + return parent 90 + } 91 + }
+26
slab/menubar-swift/Sources/SlabMenubar/PassphraseModal.swift
··· 1 + import AppKit 2 + 3 + enum PassphraseModal { 4 + static func prompt(label: String) -> String? { 5 + assert(Thread.isMainThread, "PassphraseModal must run on the main thread") 6 + 7 + let alert = NSAlert() 8 + alert.messageText = "Passphrase required" 9 + alert.informativeText = "slab needs your passphrase for “\(label)”." 10 + alert.alertStyle = .informational 11 + alert.addButton(withTitle: "Unlock") 12 + alert.addButton(withTitle: "Cancel") 13 + 14 + let field = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 280, height: 24)) 15 + field.placeholderString = "passphrase" 16 + alert.accessoryView = field 17 + 18 + NSApp.activate(ignoringOtherApps: true) 19 + alert.window.initialFirstResponder = field 20 + 21 + let response = alert.runModal() 22 + guard response == .alertFirstButtonReturn else { return nil } 23 + let value = field.stringValue 24 + return value.isEmpty ? nil : value 25 + } 26 + }
+156
slab/menubar-swift/Sources/SlabMenubar/PassphraseServer.swift
··· 1 + import Foundation 2 + import Darwin 3 + 4 + final class PassphraseServer { 5 + private var listenFd: Int32 = -1 6 + private var cache: [String: CacheEntry] = [:] 7 + private let cacheQueue = DispatchQueue(label: "slab.passphrase.cache") 8 + 9 + private struct CacheEntry { 10 + let secret: String 11 + let expiry: Date 12 + } 13 + 14 + func start() throws { 15 + let sockPath = Paths.passphraseSocket 16 + unlink(sockPath) 17 + 18 + listenFd = socket(AF_UNIX, SOCK_STREAM, 0) 19 + if listenFd < 0 { throw NSError(domain: "slab.passphrase", code: Int(errno)) } 20 + 21 + var addr = sockaddr_un() 22 + addr.sun_family = sa_family_t(AF_UNIX) 23 + let pathBytes = Array(sockPath.utf8) 24 + let maxPath = MemoryLayout.size(ofValue: addr.sun_path) - 1 25 + precondition(pathBytes.count <= maxPath, "socket path too long") 26 + 27 + withUnsafeMutableBytes(of: &addr.sun_path) { raw in 28 + let ptr = raw.baseAddress!.assumingMemoryBound(to: UInt8.self) 29 + for i in 0..<pathBytes.count { ptr[i] = pathBytes[i] } 30 + ptr[pathBytes.count] = 0 31 + } 32 + 33 + let addrLen = socklen_t(MemoryLayout<sockaddr_un>.size) 34 + let bindResult = withUnsafePointer(to: &addr) { ap -> Int32 in 35 + ap.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in 36 + Darwin.bind(listenFd, sa, addrLen) 37 + } 38 + } 39 + if bindResult < 0 { 40 + let e = errno 41 + close(listenFd); listenFd = -1 42 + throw NSError(domain: "slab.passphrase.bind", code: Int(e)) 43 + } 44 + chmod(sockPath, 0o600) 45 + 46 + if listen(listenFd, 8) < 0 { 47 + let e = errno 48 + close(listenFd); listenFd = -1 49 + throw NSError(domain: "slab.passphrase.listen", code: Int(e)) 50 + } 51 + 52 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in 53 + self?.acceptLoop() 54 + } 55 + } 56 + 57 + func stop() { 58 + if listenFd >= 0 { 59 + close(listenFd) 60 + listenFd = -1 61 + } 62 + unlink(Paths.passphraseSocket) 63 + } 64 + 65 + private func acceptLoop() { 66 + while listenFd >= 0 { 67 + let fd = accept(listenFd, nil, nil) 68 + if fd < 0 { continue } 69 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in 70 + self?.handleClient(fd) 71 + } 72 + } 73 + } 74 + 75 + private func handleClient(_ fd: Int32) { 76 + defer { close(fd) } 77 + 78 + var buf = [UInt8](repeating: 0, count: 8192) 79 + let n = read(fd, &buf, buf.count) 80 + guard n > 0 else { return } 81 + let data = Data(bytes: buf, count: n) 82 + 83 + guard let obj = try? JSONSerialization.jsonObject(with: data), 84 + let json = obj as? [String: Any] else { 85 + writeJson(fd: fd, ["ok": false, "err": "bad_json"]) 86 + return 87 + } 88 + let op = (json["op"] as? String) ?? "" 89 + 90 + switch op { 91 + case "passphrase": 92 + let label = (json["label"] as? String) ?? "vault" 93 + let timeout = (json["timeout"] as? Int) ?? 600 94 + handlePassphrase(fd: fd, label: label, ttl: TimeInterval(timeout)) 95 + case "forget": 96 + let label = json["label"] as? String 97 + cacheQueue.sync { 98 + if let label = label { 99 + cache.removeValue(forKey: label) 100 + } else { 101 + cache.removeAll() 102 + } 103 + } 104 + writeJson(fd: fd, ["ok": true]) 105 + case "ping": 106 + writeJson(fd: fd, ["ok": true, "pong": true]) 107 + default: 108 + writeJson(fd: fd, ["ok": false, "err": "unknown_op"]) 109 + } 110 + } 111 + 112 + private func handlePassphrase(fd: Int32, label: String, ttl: TimeInterval) { 113 + let now = Date() 114 + let cached: String? = cacheQueue.sync { 115 + if let entry = cache[label], entry.expiry > now { 116 + return entry.secret 117 + } 118 + cache.removeValue(forKey: label) 119 + return nil 120 + } 121 + if let secret = cached { 122 + writeJson(fd: fd, ["ok": true, "secret": secret, "cached": true]) 123 + return 124 + } 125 + 126 + let sem = DispatchSemaphore(value: 0) 127 + var typed: String? 128 + DispatchQueue.main.async { 129 + typed = PassphraseModal.prompt(label: label) 130 + sem.signal() 131 + } 132 + sem.wait() 133 + 134 + guard let secret = typed else { 135 + writeJson(fd: fd, ["ok": false, "cancelled": true]) 136 + return 137 + } 138 + 139 + let expiry = Date().addingTimeInterval(ttl) 140 + cacheQueue.sync { cache[label] = CacheEntry(secret: secret, expiry: expiry) } 141 + writeJson(fd: fd, ["ok": true, "secret": secret, "cached": false]) 142 + } 143 + 144 + private func writeJson(fd: Int32, _ dict: [String: Any]) { 145 + guard let data = try? JSONSerialization.data(withJSONObject: dict, options: []) else { return } 146 + var remaining = data 147 + while !remaining.isEmpty { 148 + let wrote = remaining.withUnsafeBytes { raw -> Int in 149 + guard let base = raw.baseAddress else { return 0 } 150 + return Darwin.write(fd, base, remaining.count) 151 + } 152 + if wrote <= 0 { break } 153 + remaining = remaining.advanced(by: wrote) 154 + } 155 + } 156 + }
+55
slab/menubar-swift/Sources/SlabMenubar/Paths.swift
··· 1 + import Foundation 2 + 3 + enum Paths { 4 + static let home = ProcessInfo.processInfo.environment["HOME"] ?? NSHomeDirectory() 5 + 6 + static var slabHome: String { 7 + ProcessInfo.processInfo.environment["SLAB_HOME"] ?? "\(home)/.local/share/slab" 8 + } 9 + 10 + static var slabBin: String { 11 + ProcessInfo.processInfo.environment["SLAB_BIN"] ?? "\(home)/.local/bin" 12 + } 13 + 14 + static var activePromptsDir: String { "\(slabHome)/state/active-prompts" } 15 + static var activeSubagentsDir: String { "\(slabHome)/state/active-subagents" } 16 + static var soundsDir: String { "\(slabHome)/sounds" } 17 + static var lidLog: String { "\(slabHome)/logs/lidalive.log" } 18 + 19 + static var mailDir: String { 20 + ProcessInfo.processInfo.environment["AC_MAIL_MAILDIR"] ?? "\(home)/.mail-all" 21 + } 22 + static var mailSyncLog: String { "\(mailDir)/sync.log" } 23 + 24 + static var daemonPlist: String { "\(home)/Library/LaunchAgents/computer.slab.daemon.plist" } 25 + static var menubarPlist: String { "\(home)/Library/LaunchAgents/computer.slab.menubar.plist" } 26 + static var claudeSleep: String { "\(slabBin)/claude-sleep" } 27 + 28 + static var passphraseSocket: String { "\(home)/.ac-daemon.sock" } 29 + 30 + static var ambientFlag: String { "/tmp/slab-ambient-active" } 31 + } 32 + 33 + enum Tools { 34 + static let candidates: [String: [String]] = [ 35 + "tailscale": [ 36 + "/opt/homebrew/bin/tailscale", 37 + "/usr/local/bin/tailscale", 38 + "/Applications/Tailscale.app/Contents/MacOS/Tailscale", 39 + ], 40 + "mbsync": ["/opt/homebrew/bin/mbsync", "/usr/local/bin/mbsync"], 41 + "mu": ["/opt/homebrew/bin/mu", "/usr/local/bin/mu"], 42 + ] 43 + 44 + static func resolve(_ name: String) -> String? { 45 + for path in candidates[name] ?? [] { 46 + if FileManager.default.isExecutableFile(atPath: path) { return path } 47 + } 48 + let pathEnv = ProcessInfo.processInfo.environment["PATH"] ?? "" 49 + for dir in pathEnv.split(separator: ":") { 50 + let candidate = "\(dir)/\(name)" 51 + if FileManager.default.isExecutableFile(atPath: candidate) { return candidate } 52 + } 53 + return nil 54 + } 55 + }
+53
slab/menubar-swift/Sources/SlabMenubar/ShellRunner.swift
··· 1 + import Foundation 2 + 3 + enum ShellRunner { 4 + @discardableResult 5 + static func run(_ path: String, args: [String], timeout: TimeInterval? = nil) -> (status: Int32, output: String) { 6 + let proc = Process() 7 + proc.executableURL = URL(fileURLWithPath: path) 8 + proc.arguments = args 9 + 10 + let outPipe = Pipe() 11 + let errPipe = Pipe() 12 + proc.standardOutput = outPipe 13 + proc.standardError = errPipe 14 + 15 + do { 16 + try proc.run() 17 + } catch { 18 + return (-1, "") 19 + } 20 + 21 + var timedOut = false 22 + if let timeout = timeout { 23 + DispatchQueue.global().asyncAfter(deadline: .now() + timeout) { 24 + if proc.isRunning { 25 + timedOut = true 26 + proc.terminate() 27 + } 28 + } 29 + } 30 + 31 + proc.waitUntilExit() 32 + let data = outPipe.fileHandleForReading.readDataToEndOfFile() 33 + _ = errPipe.fileHandleForReading.readDataToEndOfFile() 34 + let status = timedOut ? Int32(-2) : proc.terminationStatus 35 + return (status, String(data: data, encoding: .utf8) ?? "") 36 + } 37 + 38 + static func output(_ path: String, args: [String], timeout: TimeInterval? = nil) -> String? { 39 + let result = run(path, args: args, timeout: timeout) 40 + return result.status == 0 ? result.output : nil 41 + } 42 + 43 + static func runAsync(_ path: String, args: [String], completion: (() -> Void)? = nil) { 44 + DispatchQueue.global(qos: .utility).async { 45 + _ = run(path, args: args) 46 + completion?() 47 + } 48 + } 49 + 50 + static func runShellAsync(_ command: String, completion: (() -> Void)? = nil) { 51 + runAsync("/bin/sh", args: ["-c", command], completion: completion) 52 + } 53 + }
+63
slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift
··· 1 + import Foundation 2 + 3 + struct StateSnapshot { 4 + var lidClosed: Bool = false 5 + var sleepDisabled: Bool = false 6 + var activePrompts: Int = 0 7 + var activeSubagents: Int = 0 8 + var ambientActive: Bool = false 9 + var tailnetPeers: [TailnetPeer] = [] 10 + 11 + var totalActive: Int { activePrompts + activeSubagents } 12 + var hasWork: Bool { totalActive > 0 } 13 + 14 + var statusLine: String { 15 + if ambientActive && hasWork { return "ambient — \(totalActive) active" } 16 + if ambientActive { return "ambient" } 17 + if !hasWork { return "idle" } 18 + if lidClosed && !sleepDisabled { return "\(totalActive) active · sleep not disabled" } 19 + if lidClosed { return "\(totalActive) active · lid closed" } 20 + return "\(totalActive) active" 21 + } 22 + 23 + static func gather() -> StateSnapshot { 24 + var s = StateSnapshot() 25 + s.lidClosed = parseLidState() 26 + s.sleepDisabled = parseSleepDisabled() 27 + s.activePrompts = countFiles(in: Paths.activePromptsDir) 28 + s.activeSubagents = countFiles(in: Paths.activeSubagentsDir) 29 + s.ambientActive = FileManager.default.fileExists(atPath: Paths.ambientFlag) 30 + s.tailnetPeers = TailnetPeer.query() 31 + return s 32 + } 33 + 34 + private static func parseLidState() -> Bool { 35 + guard let out = ShellRunner.output( 36 + "/usr/sbin/ioreg", 37 + args: ["-r", "-k", "AppleClamshellState", "-d", "4"], 38 + timeout: 2 39 + ) else { return false } 40 + for line in out.split(separator: "\n") { 41 + if line.contains("AppleClamshellState") && !line.contains("Change") { 42 + return line.contains("Yes") 43 + } 44 + } 45 + return false 46 + } 47 + 48 + private static func parseSleepDisabled() -> Bool { 49 + guard let out = ShellRunner.output("/usr/bin/pmset", args: ["-g"], timeout: 2) else { return false } 50 + for rawLine in out.split(separator: "\n") { 51 + let line = rawLine.trimmingCharacters(in: .whitespaces) 52 + if line.hasPrefix("SleepDisabled") { 53 + return line.contains("1") 54 + } 55 + } 56 + return false 57 + } 58 + 59 + private static func countFiles(in dir: String) -> Int { 60 + guard let contents = try? FileManager.default.contentsOfDirectory(atPath: dir) else { return 0 } 61 + return contents.filter { !$0.hasPrefix(".") }.count 62 + } 63 + }
+28
slab/menubar-swift/Sources/SlabMenubar/TailnetPeer.swift
··· 1 + import Foundation 2 + 3 + struct TailnetPeer { 4 + let hostname: String 5 + let online: Bool 6 + let ip: String 7 + 8 + static func query() -> [TailnetPeer] { 9 + guard let ts = Tools.resolve("tailscale") else { return [] } 10 + guard let out = ShellRunner.output(ts, args: ["status", "--json"], timeout: 2) else { return [] } 11 + guard let data = out.data(using: .utf8), 12 + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], 13 + let peers = json["Peer"] as? [String: [String: Any]] 14 + else { return [] } 15 + 16 + var result: [TailnetPeer] = [] 17 + for (_, p) in peers { 18 + guard let host = p["HostName"] as? String else { continue } 19 + let online = (p["Online"] as? Bool) ?? false 20 + let ips = (p["TailscaleIPs"] as? [String]) ?? [] 21 + result.append(TailnetPeer(hostname: host, online: online, ip: ips.first ?? "")) 22 + } 23 + return result.sorted { lhs, rhs in 24 + if lhs.online != rhs.online { return lhs.online } 25 + return lhs.hostname < rhs.hostname 26 + } 27 + } 28 + }
+7
slab/menubar-swift/Sources/SlabMenubar/main.swift
··· 1 + import AppKit 2 + 3 + let app = NSApplication.shared 4 + app.setActivationPolicy(.accessory) 5 + let delegate = AppDelegate() 6 + app.delegate = delegate 7 + app.run()
+33
slab/menubar-swift/computer.slab.menubar.plist.tmpl
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 + <plist version="1.0"> 4 + <dict> 5 + <key>Label</key> 6 + <string>computer.slab.menubar</string> 7 + <key>ProgramArguments</key> 8 + <array> 9 + <string>@HOME@/.local/bin/slab-menubar</string> 10 + </array> 11 + <key>EnvironmentVariables</key> 12 + <dict> 13 + <key>HOME</key> 14 + <string>@HOME@</string> 15 + <key>PATH</key> 16 + <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string> 17 + <key>SLAB_HOME</key> 18 + <string>@HOME@/.local/share/slab</string> 19 + <key>SLAB_BIN</key> 20 + <string>@HOME@/.local/bin</string> 21 + </dict> 22 + <key>RunAtLoad</key> 23 + <true/> 24 + <key>KeepAlive</key> 25 + <true/> 26 + <key>ThrottleInterval</key> 27 + <integer>5</integer> 28 + <key>StandardOutPath</key> 29 + <string>/tmp/slab-menubar.out</string> 30 + <key>StandardErrorPath</key> 31 + <string>/tmp/slab-menubar.err</string> 32 + </dict> 33 + </plist>
+85
slab/menubar-swift/install.sh
··· 1 + #!/usr/bin/env bash 2 + # install.sh — build and install the Swift slab menubar, replacing the Python one. 3 + # 4 + # Steps: 5 + # 1. swift build -c release 6 + # 2. Unload the existing python menubar (if loaded) 7 + # 3. Copy the compiled binary to ~/.local/bin/slab-menubar 8 + # 4. Install the launchd plist pointing at the new binary 9 + # 5. launchctl load 10 + # 11 + # Idempotent. Safe to re-run after edits. 12 + 13 + set -euo pipefail 14 + 15 + BOLD=$'\033[1m' 16 + CYAN=$'\033[1;36m' 17 + GREEN=$'\033[1;32m' 18 + YELLOW=$'\033[1;33m' 19 + RESET=$'\033[0m' 20 + 21 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 22 + REPO_HOME="${HOME}" 23 + LAUNCH_AGENTS="${REPO_HOME}/Library/LaunchAgents" 24 + PLIST_PATH="${LAUNCH_AGENTS}/computer.slab.menubar.plist" 25 + PLIST_TMPL="${SCRIPT_DIR}/computer.slab.menubar.plist.tmpl" 26 + BIN_DIR="${REPO_HOME}/.local/bin" 27 + BIN_PATH="${BIN_DIR}/slab-menubar" 28 + 29 + say() { printf "%s• %s%s\n" "$CYAN" "$1" "$RESET"; } 30 + ok() { printf "%s✓ %s%s\n" "$GREEN" "$1" "$RESET"; } 31 + warn(){ printf "%s! %s%s\n" "$YELLOW" "$1" "$RESET"; } 32 + 33 + command -v swift >/dev/null 2>&1 || { 34 + echo "swift not found — install Xcode Command Line Tools first:" 35 + echo " xcode-select --install" 36 + exit 1 37 + } 38 + 39 + say "building slab-menubar (swift build -c release)" 40 + cd "${SCRIPT_DIR}" 41 + swift build -c release >/dev/null 42 + 43 + BUILT="$(swift build -c release --show-bin-path)/slab-menubar-swift" 44 + if [[ ! -x "${BUILT}" ]]; then 45 + echo "build succeeded but binary not found at ${BUILT}" 46 + exit 1 47 + fi 48 + ok "built: ${BUILT}" 49 + 50 + say "unloading any existing menubar launch agent" 51 + if launchctl list | grep -q computer.slab.menubar; then 52 + launchctl unload "${PLIST_PATH}" 2>/dev/null || true 53 + ok "unloaded computer.slab.menubar" 54 + else 55 + warn "no running menubar to unload — skipping" 56 + fi 57 + 58 + say "installing binary → ${BIN_PATH}" 59 + mkdir -p "${BIN_DIR}" 60 + cp "${BUILT}" "${BIN_PATH}" 61 + chmod +x "${BIN_PATH}" 62 + ok "binary installed" 63 + 64 + say "writing launchd plist → ${PLIST_PATH}" 65 + mkdir -p "${LAUNCH_AGENTS}" 66 + sed "s|@HOME@|${REPO_HOME}|g" "${PLIST_TMPL}" > "${PLIST_PATH}" 67 + ok "plist written" 68 + 69 + say "loading launch agent" 70 + launchctl load "${PLIST_PATH}" 71 + sleep 1 72 + if launchctl list | grep -q computer.slab.menubar; then 73 + ok "computer.slab.menubar is running" 74 + else 75 + warn "launchctl did not register the agent — check /tmp/slab-menubar.err" 76 + fi 77 + 78 + printf "\n%sdone.%s\n" "${BOLD}" "${RESET}" 79 + echo " binary: ${BIN_PATH}" 80 + echo " plist: ${PLIST_PATH}" 81 + echo " stdout: /tmp/slab-menubar.out" 82 + echo " stderr: /tmp/slab-menubar.err" 83 + echo 84 + echo " rebuild+reload after edits: $(basename "$0")" 85 + echo " tail logs: tail -f /tmp/slab-menubar.err"