Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/yergersnap: colored "snap" pill, hover/press states, click sound

Replaces the SF-Symbol menubar icon with a custom-rendered pill
(red record-dot + "snap" label, sized like MenuBand's piano keys)
that has hover and pressed visual states via NSTrackingArea +
HoverResponder. Click also plays a punctual 220 Hz sine pop via
AVAudioEngine generated buffer. URL-scheme triggers stay silent.

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

+195 -47
+2 -1
slab/yergersnap/build.sh
··· 45 45 46 46 echo "==> compile" 47 47 swiftc -O -o "$APP_BUNDLE/Contents/MacOS/$APP_NAME" tap.swift \ 48 - -framework Cocoa -framework ApplicationServices -framework Carbon 48 + -framework Cocoa -framework ApplicationServices -framework Carbon \ 49 + -framework AVFoundation 49 50 50 51 cp Info.plist "$APP_BUNDLE/Contents/Info.plist" 51 52
+193 -46
slab/yergersnap/tap.swift
··· 1 1 // tap.swift — YergerSnap menubar app. 2 2 // 3 - // Posts a synthetic click on the iPhone Mirroring window's Capture button 4 - // in response to two triggers: 3 + // Posts a synthetic click on the iPhone Mirroring window's Capture 4 + // button. Two triggers: 5 5 // 6 - // 1. Dragonframe Action Script (preferred). DF runs a shell script on 7 - // every event (Preferences → Advanced → Action Script). On SHOOT 8 - // events the script calls `open yergersnap://capture`, which routes 9 - // to this running app via the registered URL scheme. Net result: 10 - // pressing Enter in Dragonframe captures BOTH the DF camera and the 6 + // 1. Dragonframe Action Script (preferred). DF runs a shell script 7 + // on every event (Preferences → Advanced → Action Script). On 8 + // SHOOT events the script calls `open yergersnap://capture`, 9 + // which routes here via the registered URL scheme. So pressing 10 + // Enter in Dragonframe captures BOTH the DF camera and the 11 11 // mirrored iPhone, with no fake keystrokes. 12 12 // 13 - // 2. The menu-bar button (manual / fallback). Left-click taps the 14 - // iPhone immediately. Right-click opens the menu. 13 + // 2. The "snap" menu-bar button (manual). Left-click taps the 14 + // iPhone immediately; right-click opens the menu. 15 15 // 16 16 // First launch on a clean Mac: macOS prompts for Accessibility (TCC). 17 - // That's needed both for AX queries on iPhone Mirroring's window and for 18 - // posting the synthetic mouse click. Grant it once. 17 + // Needed both for AX queries on iPhone Mirroring's window and for 18 + // posting the synthetic mouse click. 19 19 // 20 20 // Calibration: tapX/tapY are window-relative fractions for where to 21 21 // click inside the iPhone Mirroring window. Default = bottom center. 22 - // Tweak and rebuild via ./build.sh. 23 22 24 23 import Cocoa 25 24 import ApplicationServices 26 25 import Carbon.HIToolbox 26 + import AVFoundation 27 27 28 28 // ---- config -------------------------------------------------------------- 29 29 let tapX: CGFloat = 0.50 30 30 let tapY: CGFloat = 0.90 31 31 // -------------------------------------------------------------------------- 32 + 33 + // MARK: - iPhone Mirroring helpers 32 34 33 35 func runningApp(named name: String) -> NSRunningApplication? { 34 36 NSWorkspace.shared.runningApplications.first { $0.localizedName == name } ··· 82 84 prev?.activate(options: []) 83 85 } 84 86 85 - // Action script template installed into ~/Library/Application Support 86 - // and pointed at from Dragonframe Preferences → Advanced → Action Script. 87 - // Dragonframe passes 8 positional args; $4 is the event name. 87 + // MARK: - Dragonframe action script install 88 + 88 89 let actionScriptBody = """ 89 90 #!/usr/bin/env bash 90 91 # YergerSnap — Dragonframe action script. ··· 94 95 # $1 production $2 scene $3 take $4 event 95 96 # $5 frame $6 exposure $7 expName $8 filename 96 97 # 97 - # On SHOOT (the moment the user triggers capture) we ping YergerSnap via 98 - # its URL scheme, which makes the running app tap iPhone Mirroring. 98 + # On SHOOT (the moment the user triggers capture) we ping YergerSnap 99 + # via its URL scheme, which makes the running app tap iPhone Mirroring. 99 100 case "$4" in 100 101 SHOOT) 101 102 /usr/bin/open "yergersnap://capture" >/dev/null 2>&1 ··· 126 127 } 127 128 } 128 129 130 + // MARK: - Icon renderer 131 + 132 + enum IconState { case idle, hover, pressed } 133 + 134 + enum IconRenderer { 135 + static let height: CGFloat = 22 136 + static let leftPad: CGFloat = 5 137 + static let dotSize: CGFloat = 10 138 + static let ringPad: CGFloat = 2 139 + static let gap: CGFloat = 4 140 + static let rightPad: CGFloat = 6 141 + static let labelText = "snap" 142 + static let font = NSFont.systemFont(ofSize: 11.5, weight: .semibold) 143 + 144 + static var labelSize: NSSize { 145 + NSAttributedString(string: labelText, attributes: [.font: font]).size() 146 + } 147 + 148 + static var imageSize: NSSize { 149 + let w = leftPad + (dotSize + ringPad * 2) + gap + ceil(labelSize.width) + rightPad 150 + return NSSize(width: ceil(w), height: height) 151 + } 152 + 153 + static func image(_ state: IconState) -> NSImage { 154 + let size = imageSize 155 + let img = NSImage(size: size) 156 + img.lockFocus() 157 + defer { img.unlockFocus() } 158 + 159 + // Hover / pressed background pill 160 + if state != .idle { 161 + let alpha: CGFloat = state == .pressed ? 0.28 : 0.12 162 + NSColor.white.withAlphaComponent(alpha).setFill() 163 + let pill = NSRect(x: 1, y: 1, width: size.width - 2, height: size.height - 2) 164 + NSBezierPath(roundedRect: pill, xRadius: 4.5, yRadius: 4.5).fill() 165 + } 166 + 167 + // Dot + ring 168 + let dotY = (size.height - dotSize) / 2 169 + let dotX = leftPad + ringPad 170 + let dotRect = NSRect(x: dotX, y: dotY, width: dotSize, height: dotSize) 171 + let ringRect = dotRect.insetBy(dx: -ringPad, dy: -ringPad) 172 + 173 + // Outer ring 174 + let ringColor: NSColor = .white.withAlphaComponent(state == .pressed ? 1.0 : 0.85) 175 + ringColor.setStroke() 176 + let ring = NSBezierPath(ovalIn: ringRect) 177 + ring.lineWidth = 1.3 178 + ring.stroke() 179 + 180 + // Inner dot — red, brighter on hover, white on press. 181 + let dotColor: NSColor = { 182 + switch state { 183 + case .idle: return NSColor(calibratedRed: 0.92, green: 0.22, blue: 0.22, alpha: 1) 184 + case .hover: return NSColor(calibratedRed: 1.00, green: 0.32, blue: 0.32, alpha: 1) 185 + case .pressed: return NSColor.white 186 + } 187 + }() 188 + dotColor.setFill() 189 + NSBezierPath(ovalIn: dotRect).fill() 190 + 191 + // "snap" label 192 + let textColor: NSColor = state == .pressed 193 + ? NSColor.white 194 + : NSColor.white.withAlphaComponent(0.92) 195 + let textX = leftPad + dotSize + ringPad * 2 + gap 196 + let textY = (size.height - labelSize.height) / 2 - 1 197 + labelText.draw(at: NSPoint(x: textX, y: textY), 198 + withAttributes: [.font: font, .foregroundColor: textColor]) 199 + 200 + return img 201 + } 202 + } 203 + 204 + // MARK: - Click sound — low sine with a tiny front-edge pop. 205 + 206 + final class ClickSound { 207 + private let engine = AVAudioEngine() 208 + private let player = AVAudioPlayerNode() 209 + private var buffer: AVAudioPCMBuffer? 210 + 211 + init() { 212 + engine.attach(player) 213 + let format = engine.mainMixerNode.outputFormat(forBus: 0) 214 + engine.connect(player, to: engine.mainMixerNode, format: format) 215 + buffer = makeBuffer(format: format) 216 + do { try engine.start() } 217 + catch { NSLog("YergerSnap: audio engine start failed — \(error)") } 218 + } 219 + 220 + private func makeBuffer(format: AVAudioFormat) -> AVAudioPCMBuffer? { 221 + let sr = format.sampleRate 222 + let durationSec = 0.085 223 + let frameCount = AVAudioFrameCount(sr * durationSec) 224 + guard let buf = AVAudioPCMBuffer(pcmFormat: format, 225 + frameCapacity: frameCount) 226 + else { return nil } 227 + buf.frameLength = frameCount 228 + let channels = Int(format.channelCount) 229 + let f0 = 220.0 230 + for ch in 0..<channels { 231 + guard let data = buf.floatChannelData?[ch] else { continue } 232 + for i in 0..<Int(frameCount) { 233 + let t = Double(i) / sr 234 + let env: Double 235 + if t < 0.004 { env = t / 0.004 } 236 + else if t < durationSec - 0.030 { env = 1.0 } 237 + else { env = max(0, (durationSec - t) / 0.030) } 238 + let body = sin(2 * .pi * f0 * t) 239 + let click = (t < 0.0025) 240 + ? sin(2 * .pi * 2400 * t) * (1.0 - t / 0.0025) 241 + : 0 242 + data[i] = Float((body * 0.6 + click * 0.4) * env * 0.20) 243 + } 244 + } 245 + return buf 246 + } 247 + 248 + func play() { 249 + guard let buf = buffer else { return } 250 + if !engine.isRunning { try? engine.start() } 251 + player.scheduleBuffer(buf, at: nil, options: .interrupts, 252 + completionHandler: nil) 253 + if !player.isPlaying { player.play() } 254 + } 255 + } 256 + 257 + // MARK: - Hover responder (NSResponder shim for tracking-area events) 258 + 259 + final class HoverResponder: NSResponder { 260 + var onEnter: (() -> Void)? 261 + var onExit: (() -> Void)? 262 + override func mouseEntered(with event: NSEvent) { onEnter?() } 263 + override func mouseExited(with event: NSEvent) { onExit?() } 264 + } 265 + 266 + // MARK: - AppDelegate 267 + 129 268 class AppDelegate: NSObject, NSApplicationDelegate { 130 269 var statusItem: NSStatusItem! 131 270 var menu: NSMenu! 132 - var idleImage: NSImage? 133 - var firingImage: NSImage? 271 + let sound = ClickSound() 272 + let hover = HoverResponder() 273 + var isHovered = false 134 274 135 275 func applicationDidFinishLaunching(_ note: Notification) { 136 276 let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 137 277 _ = AXIsProcessTrustedWithOptions([promptKey: true] as CFDictionary) 138 278 139 - idleImage = NSImage(systemSymbolName: "camera.aperture", 140 - accessibilityDescription: "YergerSnap idle") 141 - firingImage = NSImage(systemSymbolName: "circle.inset.filled", 142 - accessibilityDescription: "YergerSnap firing") 143 - 144 - statusItem = NSStatusBar.system.statusItem( 145 - withLength: NSStatusItem.variableLength) 279 + let size = IconRenderer.imageSize 280 + statusItem = NSStatusBar.system.statusItem(withLength: size.width) 146 281 if let btn = statusItem.button { 147 - btn.image = idleImage 282 + btn.image = IconRenderer.image(.idle) 148 283 btn.imagePosition = .imageOnly 149 284 btn.target = self 150 285 btn.action = #selector(handleClick(_:)) 151 - btn.sendAction(on: [.leftMouseUp, .rightMouseUp]) 286 + btn.sendAction(on: [.leftMouseDown, .rightMouseDown]) 152 287 btn.toolTip = "YergerSnap — click to tap iPhone Mirroring" 288 + 289 + hover.onEnter = { [weak self] in self?.setHovered(true) } 290 + hover.onExit = { [weak self] in self?.setHovered(false) } 291 + let area = NSTrackingArea( 292 + rect: btn.bounds, 293 + options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], 294 + owner: hover, userInfo: nil) 295 + btn.addTrackingArea(area) 153 296 } 154 297 155 298 menu = NSMenu() ··· 157 300 header.isEnabled = false 158 301 menu.addItem(header) 159 302 menu.addItem(NSMenuItem.separator()) 160 - 161 303 let tapItem = NSMenuItem(title: "Tap iPhone Now", 162 304 action: #selector(tapFromMenu), 163 305 keyEquivalent: "") 164 306 tapItem.target = self 165 307 menu.addItem(tapItem) 166 - 167 308 menu.addItem(NSMenuItem.separator()) 168 309 let installItem = NSMenuItem(title: "Install Dragonframe Hook…", 169 310 action: #selector(installAndReveal), 170 311 keyEquivalent: "") 171 312 installItem.target = self 172 313 menu.addItem(installItem) 173 - 174 314 menu.addItem(NSMenuItem.separator()) 175 315 menu.addItem(NSMenuItem(title: "Quit YergerSnap", 176 316 action: #selector(NSApplication.terminate(_:)), 177 317 keyEquivalent: "q")) 178 318 } 179 319 180 - // URL scheme handler — Dragonframe's action script calls this via 181 - // `open yergersnap://capture` to ask us to tap iPhone Mirroring. 320 + func setHovered(_ hovered: Bool) { 321 + if isHovered == hovered { return } 322 + isHovered = hovered 323 + statusItem.button?.image = IconRenderer.image(hovered ? .hover : .idle) 324 + } 325 + 326 + // URL scheme handler — Dragonframe action script calls this via 327 + // `open yergersnap://capture` to request a tap. 182 328 func application(_ app: NSApplication, open urls: [URL]) { 183 329 for url in urls where url.scheme == "yergersnap" { 184 - let host = url.host?.lowercased() ?? url.path.replacingOccurrences( 185 - of: "/", with: "") 330 + let host = url.host?.lowercased() 331 + ?? url.path.replacingOccurrences(of: "/", with: "") 186 332 switch host { 187 333 case "capture", "tap": 188 - flashIcon() 334 + flashClick(playSound: false) // remote trigger: silent 189 335 tapPhone() 190 336 default: 191 337 NSLog("YergerSnap: unknown URL \(url)") ··· 195 341 196 342 @objc func handleClick(_ sender: Any?) { 197 343 let event = NSApp.currentEvent 198 - let isRight = event?.type == .rightMouseUp 344 + let isRight = event?.type == .rightMouseDown 199 345 let isCtrl = event?.modifierFlags.contains(.control) ?? false 200 346 if isRight || isCtrl { showMenu(); return } 201 - flashIcon() 347 + flashClick(playSound: true) 202 348 tapPhone() 203 349 } 204 350 ··· 208 354 DispatchQueue.main.async { self.statusItem.menu = nil } 209 355 } 210 356 211 - func flashIcon() { 212 - statusItem.button?.image = firingImage 213 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { [weak self] in 214 - self?.statusItem.button?.image = self?.idleImage 357 + func flashClick(playSound: Bool) { 358 + if playSound { sound.play() } 359 + statusItem.button?.image = IconRenderer.image(.pressed) 360 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.13) { [weak self] in 361 + guard let self else { return } 362 + self.statusItem.button?.image = 363 + IconRenderer.image(self.isHovered ? .hover : .idle) 215 364 } 216 365 } 217 366 218 - @objc func tapFromMenu() { flashIcon(); tapPhone() } 219 - 367 + @objc func tapFromMenu() { flashClick(playSound: true); tapPhone() } 220 368 @objc func installAndReveal() { 221 369 guard let path = installActionScript() else { 222 370 let a = NSAlert() ··· 227 375 } 228 376 NSPasteboard.general.clearContents() 229 377 NSPasteboard.general.setString(path, forType: .string) 230 - NSWorkspace.shared.activateFileViewerSelecting( 231 - [URL(fileURLWithPath: path)]) 378 + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: path)]) 232 379 233 380 NSApp.activate(ignoringOtherApps: true) 234 381 let a = NSAlert()