From acdfc9f13153304c9f8ca1dc9045a67d6539037a Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Sat, 8 Aug 2026 13:49:03 -0700 Subject: [PATCH] Let TrackDrum summon Menu Band with a global command double-tap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The App Store build is sandboxed, so it cannot run an event tap of its own to notice ⌘⌘ while it is not running. TrackDrum can: reading combined-session key state needs no tap and no permission prompt, so promote the shield's exit watchdog into a watchdog that runs for the whole process lifetime. Outside capture, ⌘⌘ summons — activating Menu Band if it is running, otherwise launching it with --focus-on-launch so the gesture lands popover-open and focus-armed rather than merely started. During capture the same gesture keeps its old meaning and tears the shield down; Escape stays exit-only. The summon crosses the localhost bridge as a contactless "summon" frame, and a pending summon is held until the connection is ready so the first tap is never the one that gets dropped. --- slab/menuband-trackpad/Sources/main.swift | 105 ++++++++++++++++-- .../Sources/MenuBand/AppDelegate.swift | 12 ++ .../MenuBandTrackpadPluginClient.swift | 8 ++ 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/slab/menuband-trackpad/Sources/main.swift b/slab/menuband-trackpad/Sources/main.swift index 7c27d265e4..e654e3ee30 100644 --- a/slab/menuband-trackpad/Sources/main.swift +++ b/slab/menuband-trackpad/Sources/main.swift @@ -43,6 +43,7 @@ private final class TrackpadInteractionShield { private var lastCommandTap: CFTimeInterval? var onExitRequested: (() -> Void)? + var onCommandDoubleTap: (() -> Void)? func start() { precondition(Thread.isMainThread) @@ -58,13 +59,10 @@ private final class TrackpadInteractionShield { } else { NSLog("TrackDrum for Menu Band: click/context shield active") } - startExitWatchdog() } func stop() { precondition(Thread.isMainThread) - exitTimer?.invalidate() - exitTimer = nil lastCommandTap = nil commandExitArmed = false stopEventTap() @@ -77,12 +75,11 @@ private final class TrackpadInteractionShield { } } - /// Permission-free emergency exit. Reading combined-session key state - /// does not require a key tap, so Escape and the same double-Command - /// gesture that entered focus can always dismantle the click shield. The - /// watcher first observes a complete release after activation so the - /// entry gesture can never cancel the session it just opened. - private func startExitWatchdog() { + /// Permission-free Command-Command watcher. Reading combined-session key + /// state does not require a key tap, so the companion can summon MenuBand + /// even while the App Store process is not running. While capture is + /// active, the same gesture remains the emergency exit path. + private func startCommandWatchdog() { exitTimer?.invalidate() escapeWasDown = CGEventSource.keyState( .combinedSessionState, key: 53 @@ -120,7 +117,11 @@ private final class TrackpadInteractionShield { self.escapeWasDown = escapeDown self.commandWasDown = commandDown if escapePressed || commandDoubleTapped { - self.onExitRequested?() + if escapePressed { + self.onExitRequested?() + } else { + self.onCommandDoubleTap?() + } } } RunLoop.main.add(timer, forMode: .common) @@ -204,6 +205,18 @@ private final class TrackpadInteractionShield { tapRunLoop = nil } + func startGlobalCommandWatchdog() { + precondition(Thread.isMainThread) + guard exitTimer == nil else { return } + startCommandWatchdog() + } + + func shutdownGlobalCommandWatchdog() { + precondition(Thread.isMainThread) + exitTimer?.invalidate() + exitTimer = nil + } + private func startFallbackPanels() { guard panels.isEmpty else { return } panels = NSScreen.screens.map { screen in @@ -273,6 +286,7 @@ private final class TrackpadBridgeServer { private var client: NWConnection? private var receiveBuffer = Data() private var captureEnabled = false + private var pendingSummon = false private let interactionShield = TrackpadInteractionShield() func start() throws { @@ -283,6 +297,18 @@ private final class TrackpadBridgeServer { self.sendExit() } } + interactionShield.onCommandDoubleTap = { [weak self] in + self?.queue.async { [weak self] in + guard let self else { return } + if self.captureEnabled { + self.setCaptureEnabled(false) + self.sendExit() + } else { + self.requestSummon() + } + } + } + performOnMain { self.interactionShield.startGlobalCommandWatchdog() } let parameters = NWParameters.tcp parameters.requiredLocalEndpoint = .hostPort( host: NWEndpoint.Host("127.0.0.1"), @@ -314,6 +340,7 @@ private final class TrackpadBridgeServer { func stop() { setCaptureEnabled(false) + performOnMain { self.interactionShield.shutdownGlobalCommandWatchdog() } client?.cancel() client = nil listener?.cancel() @@ -330,6 +357,7 @@ private final class TrackpadBridgeServer { switch state { case .ready: self.receive(on: connection) + self.sendPendingSummonIfReady() case .failed, .cancelled: self.client = nil self.setCaptureEnabled(false) @@ -392,6 +420,63 @@ private final class TrackpadBridgeServer { NSLog("TrackDrum for Menu Band: capture %@", captureEnabled ? "on" : "off") } + private func requestSummon() { + queue.async { [weak self] in + guard let self else { return } + self.pendingSummon = true + if self.client != nil { + self.sendPendingSummonIfReady() + } else { + self.launchMenuBandIfNeeded() + } + } + } + + private func sendPendingSummonIfReady() { + guard pendingSummon, let client, + case .ready = client.state else { return } + pendingSummon = false + send(ContactFrame( + version: 1, + timestamp: CACurrentMediaTime(), + contacts: [], + event: "summon" + )) + } + + private func launchMenuBandIfNeeded() { + DispatchQueue.main.async { + let bundleID = "computer.aesthetic.menuband" + if let running = NSRunningApplication.runningApplications( + withBundleIdentifier: bundleID + ).first { + running.activate(options: [.activateAllWindows]) + return + } + guard let url = NSWorkspace.shared.urlForApplication( + withBundleIdentifier: bundleID + ) else { + NSLog("TrackDrum for Menu Band: MenuBand app is not installed") + return + } + let configuration = NSWorkspace.OpenConfiguration() + configuration.activates = true + // The launch itself carries the focus intent, so the first + // global ⌘⌘ is enough even if the localhost bridge is still + // negotiating. The pending summon frame remains as a fallback + // for builds that do not consume this launch argument. + configuration.arguments = ["--focus-on-launch"] + NSWorkspace.shared.openApplication( + at: url, configuration: configuration + ) { _, error in + if let error { + NSLog("TrackDrum for Menu Band: MenuBand launch failed: %@", + error.localizedDescription) + } + } + } + } + private func performOnMain(_ work: @escaping () -> Void) { if Thread.isMainThread { work() diff --git a/slab/menuband/Sources/MenuBand/AppDelegate.swift b/slab/menuband/Sources/MenuBand/AppDelegate.swift index 929a54d7cc..f1b8e1be0a 100644 --- a/slab/menuband/Sources/MenuBand/AppDelegate.swift +++ b/slab/menuband/Sources/MenuBand/AppDelegate.swift @@ -711,6 +711,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate { trackpadPlugin.onExitRequested = { [weak self] in self?.exitPerformanceFocusFromEscape() } + trackpadPlugin.onSummonRequested = { [weak self] in + guard let self else { return } + // TrackDrum owns the global gesture, including while this app was + // not running. Always leave the gesture at the useful endpoint: + // the popover is visible and keyboard/trackpad focus is armed. + if !self.isPopoverPanelShown { + self.showPopover() + } + DispatchQueue.main.async { [weak self] in + self?.beginFocusCaptureFromShortcut(keepPopoverOpen: true) + } + } trackpadPlugin.onFrame = { [weak self] contacts, timestamp, callbackTime in guard let self, self.trackpadPluginCaptureActive else { return } self.handleTrackpadFrame( diff --git a/slab/menuband/Sources/MenuBand/MenuBandTrackpadPluginClient.swift b/slab/menuband/Sources/MenuBand/MenuBandTrackpadPluginClient.swift index b81612bf91..6b46f1d0b0 100644 --- a/slab/menuband/Sources/MenuBand/MenuBandTrackpadPluginClient.swift +++ b/slab/menuband/Sources/MenuBand/MenuBandTrackpadPluginClient.swift @@ -12,6 +12,7 @@ final class MenuBandTrackpadPluginClient { var onConnectionChanged: ((Bool) -> Void)? var onExitRequested: (() -> Void)? + var onSummonRequested: (() -> Void)? var onFrame: (([TrackpadContact], Double, Double) -> Void)? private struct Command: Codable { @@ -219,6 +220,13 @@ final class MenuBandTrackpadPluginClient { } return } + if frame.event == "summon" { + guard frame.contacts.isEmpty else { return } + DispatchQueue.main.async { [weak self] in + self?.onSummonRequested?() + } + return + } guard frame.event == nil else { return } var contacts: [TrackpadContact] = [] contacts.reserveCapacity(frame.contacts.count) -- 2.51.2