Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/yergersnap: rename MirrorTap → YergerSnap, add app icon

URL scheme, bundle ID, executable, action-script install path all
move to yergersnap/yergersnap://. Adds make-icon.swift which renders
a camera-aperture shutter on a violet→navy gradient and compiles to
AppIcon.icns; bundle now ships with the icon embedded.

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

+531 -388
-3
slab/iphone-mirror-tap/.gitignore
··· 1 - build/ 2 - MirrorTap.app/ 3 - *.zip
+9 -7
slab/iphone-mirror-tap/Info.plist slab/yergersnap/Info.plist
··· 5 5 <key>CFBundleDevelopmentRegion</key> 6 6 <string>en</string> 7 7 <key>CFBundleExecutable</key> 8 - <string>MirrorTap</string> 8 + <string>YergerSnap</string> 9 9 <key>CFBundleIdentifier</key> 10 - <string>computer.aesthetic.mirrortap</string> 10 + <string>computer.aesthetic.yergersnap</string> 11 11 <key>CFBundleInfoDictionaryVersion</key> 12 12 <string>6.0</string> 13 13 <key>CFBundleName</key> 14 - <string>MirrorTap</string> 14 + <string>YergerSnap</string> 15 15 <key>CFBundleDisplayName</key> 16 - <string>MirrorTap</string> 16 + <string>YergerSnap</string> 17 17 <key>CFBundlePackageType</key> 18 18 <string>APPL</string> 19 19 <key>CFBundleShortVersionString</key> 20 20 <string>0.1</string> 21 21 <key>CFBundleVersion</key> 22 22 <string>1</string> 23 + <key>CFBundleIconFile</key> 24 + <string>AppIcon</string> 23 25 <key>LSMinimumSystemVersion</key> 24 26 <string>13.0</string> 25 27 <key>LSUIElement</key> ··· 27 29 <key>NSHighResolutionCapable</key> 28 30 <true/> 29 31 <key>NSAppleEventsUsageDescription</key> 30 - <string>MirrorTap needs to interact with iPhone Mirroring to send a tap.</string> 32 + <string>YergerSnap needs to interact with iPhone Mirroring to send a tap.</string> 31 33 <key>CFBundleURLTypes</key> 32 34 <array> 33 35 <dict> 34 36 <key>CFBundleURLName</key> 35 - <string>computer.aesthetic.mirrortap</string> 37 + <string>computer.aesthetic.yergersnap</string> 36 38 <key>CFBundleURLSchemes</key> 37 39 <array> 38 - <string>mirrortap</string> 40 + <string>yergersnap</string> 39 41 </array> 40 42 </dict> 41 43 </array>
slab/iphone-mirror-tap/MirrorTap.entitlements slab/yergersnap/YergerSnap.entitlements
-125
slab/iphone-mirror-tap/build.sh
··· 1 - #!/usr/bin/env bash 2 - # build.sh — compile, bundle, sign, notarize, staple → MirrorTap.app 3 - # 4 - # Pulls the Developer ID cert + notarization creds from the vault at 5 - # aesthetic-computer-vault/ac-electron/.env. The .p12 is imported into a 6 - # build-only keychain on first run so we don't pollute the login keychain. 7 - # 8 - # Usage: 9 - # ./build.sh # build + sign + notarize + staple 10 - # ./build.sh --no-notary # build + sign only (faster; opens with right-click) 11 - # 12 - # Output: MirrorTap.app in this directory. 13 - 14 - set -euo pipefail 15 - 16 - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 17 - cd "$SCRIPT_DIR" 18 - 19 - VAULT_ENV="$(cd "$SCRIPT_DIR/../../aesthetic-computer-vault/ac-electron" && pwd)/.env" 20 - NOTARIZE=1 21 - [[ "${1:-}" == "--no-notary" ]] && NOTARIZE=0 22 - 23 - if [[ ! -f "$VAULT_ENV" ]]; then 24 - echo "vault env missing at $VAULT_ENV" >&2 25 - exit 1 26 - fi 27 - # shellcheck disable=SC1090 28 - source "$VAULT_ENV" 29 - : "${CSC_KEY_PASSWORD:?missing CSC_KEY_PASSWORD}" 30 - : "${APPLE_ID:?missing APPLE_ID}" 31 - : "${APPLE_APP_SPECIFIC_PASSWORD:?missing APPLE_APP_SPECIFIC_PASSWORD}" 32 - : "${APPLE_TEAM_ID:?missing APPLE_TEAM_ID}" 33 - 34 - P12="$(cd "$(dirname "$VAULT_ENV")" && pwd)/mac-developer-id.p12" 35 - [[ -f "$P12" ]] || { echo "missing $P12" >&2; exit 1; } 36 - 37 - APP_NAME="MirrorTap" 38 - APP_BUNDLE="$SCRIPT_DIR/$APP_NAME.app" 39 - BUILD_DIR="$SCRIPT_DIR/build" 40 - KEYCHAIN="$BUILD_DIR/build.keychain" 41 - KEYCHAIN_PASS="mirrortap-build" 42 - 43 - rm -rf "$BUILD_DIR" "$APP_BUNDLE" 44 - mkdir -p "$BUILD_DIR" "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources" 45 - 46 - echo "==> compile" 47 - swiftc -O -o "$APP_BUNDLE/Contents/MacOS/$APP_NAME" tap.swift \ 48 - -framework Cocoa -framework ApplicationServices -framework Carbon 49 - 50 - cp Info.plist "$APP_BUNDLE/Contents/Info.plist" 51 - 52 - echo "==> import cert into ephemeral keychain" 53 - # Capture the original search list before we touch anything so we can 54 - # always restore it. If a stale build keychain is registered, drop it. 55 - ORIG_SEARCH=$(security list-keychains -d user \ 56 - | sed -e 's/"//g' -e 's/^[[:space:]]*//' \ 57 - | grep -v 'iphone-mirror-tap/build/build.keychain' || true) 58 - security delete-keychain "$KEYCHAIN" 2>/dev/null || true 59 - 60 - security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" 61 - security set-keychain-settings -lut 21600 "$KEYCHAIN" 62 - security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" 63 - security import "$P12" -k "$KEYCHAIN" -P "$CSC_KEY_PASSWORD" -A >/dev/null 64 - security set-key-partition-list -S apple-tool:,apple:,codesign: \ 65 - -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" >/dev/null 66 - 67 - # The .p12 holds only the leaf — fetch Apple's Developer ID G2 intermediate 68 - # so the chain validates. Cached after the first build. 69 - G2_CA="$BUILD_DIR/DeveloperIDG2CA.cer" 70 - if [[ ! -f "$G2_CA" ]]; then 71 - curl -fsSL -o "$G2_CA" \ 72 - https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer 73 - fi 74 - security import "$G2_CA" -k "$KEYCHAIN" >/dev/null 75 - 76 - # Prepend the build keychain to the search list so codesign can find it. 77 - security list-keychains -d user -s "$KEYCHAIN" $ORIG_SEARCH 78 - 79 - IDENTITY=$(security find-identity -p codesigning -v "$KEYCHAIN" \ 80 - | grep -E 'Developer ID Application' | head -1 \ 81 - | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/') 82 - [[ -n "$IDENTITY" ]] || { echo "no Developer ID Application identity found" >&2; exit 1; } 83 - echo " identity: $IDENTITY" 84 - 85 - echo "==> codesign (hardened runtime)" 86 - codesign --force --options runtime --timestamp \ 87 - --entitlements MirrorTap.entitlements \ 88 - --sign "$IDENTITY" --keychain "$KEYCHAIN" \ 89 - "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 90 - codesign --force --options runtime --timestamp \ 91 - --entitlements MirrorTap.entitlements \ 92 - --sign "$IDENTITY" --keychain "$KEYCHAIN" \ 93 - "$APP_BUNDLE" 94 - 95 - echo "==> verify signature" 96 - codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" 97 - 98 - if [[ "$NOTARIZE" == "1" ]]; then 99 - ZIP="$BUILD_DIR/$APP_NAME.zip" 100 - echo "==> zip for notarization" 101 - /usr/bin/ditto -c -k --keepParent "$APP_BUNDLE" "$ZIP" 102 - 103 - echo "==> submit to notary (this can take a minute or three)" 104 - xcrun notarytool submit "$ZIP" \ 105 - --apple-id "$APPLE_ID" \ 106 - --team-id "$APPLE_TEAM_ID" \ 107 - --password "$APPLE_APP_SPECIFIC_PASSWORD" \ 108 - --wait 109 - 110 - echo "==> staple" 111 - xcrun stapler staple "$APP_BUNDLE" 112 - spctl -a -vv "$APP_BUNDLE" || true 113 - fi 114 - 115 - # Restore original keychain search list (don't leave the build keychain pinned). 116 - security list-keychains -d user -s $ORIG_SEARCH 117 - security delete-keychain "$KEYCHAIN" 2>/dev/null || true 118 - 119 - echo 120 - echo "✓ built $APP_BUNDLE" 121 - if [[ "$NOTARIZE" == "1" ]]; then 122 - echo " (signed + notarized — Gatekeeper will allow on any Mac)" 123 - else 124 - echo " (signed only — first launch needs right-click → Open)" 125 - fi
slab/iphone-mirror-tap/shoot.sh slab/yergersnap/shoot.sh
-253
slab/iphone-mirror-tap/tap.swift
··· 1 - // tap.swift — MirrorTap menubar app. 2 - // 3 - // Posts a synthetic click on the iPhone Mirroring window's Capture button 4 - // in response to two triggers: 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 mirrortap://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 11 - // mirrored iPhone, with no fake keystrokes. 12 - // 13 - // 2. The menu-bar button (manual / fallback). Left-click taps the 14 - // iPhone immediately. Right-click opens the menu. 15 - // 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. 19 - // 20 - // Calibration: tapX/tapY are window-relative fractions for where to 21 - // click inside the iPhone Mirroring window. Default = bottom center. 22 - // Tweak and rebuild via ./build.sh. 23 - 24 - import Cocoa 25 - import ApplicationServices 26 - import Carbon.HIToolbox 27 - 28 - // ---- config -------------------------------------------------------------- 29 - let tapX: CGFloat = 0.50 30 - let tapY: CGFloat = 0.90 31 - // -------------------------------------------------------------------------- 32 - 33 - func runningApp(named name: String) -> NSRunningApplication? { 34 - NSWorkspace.shared.runningApplications.first { $0.localizedName == name } 35 - } 36 - 37 - func iPhoneMirroringFrame() -> CGRect? { 38 - guard let pid = runningApp(named: "iPhone Mirroring")?.processIdentifier 39 - else { return nil } 40 - let app = AXUIElementCreateApplication(pid) 41 - var windowsRef: CFTypeRef? 42 - guard AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, 43 - &windowsRef) == .success, 44 - let windows = windowsRef as? [AXUIElement], 45 - let win = windows.first else { return nil } 46 - var posRef: CFTypeRef? 47 - var sizeRef: CFTypeRef? 48 - guard AXUIElementCopyAttributeValue(win, kAXPositionAttribute as CFString, 49 - &posRef) == .success, 50 - AXUIElementCopyAttributeValue(win, kAXSizeAttribute as CFString, 51 - &sizeRef) == .success 52 - else { return nil } 53 - var pos = CGPoint.zero, size = CGSize.zero 54 - AXValueGetValue(posRef as! AXValue, .cgPoint, &pos) 55 - AXValueGetValue(sizeRef as! AXValue, .cgSize, &size) 56 - return CGRect(origin: pos, size: size) 57 - } 58 - 59 - func clickAt(_ point: CGPoint) { 60 - let saved = CGEvent(source: nil)?.location 61 - let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, 62 - mouseCursorPosition: point, mouseButton: .left) 63 - let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, 64 - mouseCursorPosition: point, mouseButton: .left) 65 - down?.post(tap: .cghidEventTap) 66 - up?.post(tap: .cghidEventTap) 67 - if let saved = saved { CGWarpMouseCursorPosition(saved) } 68 - } 69 - 70 - func tapPhone() { 71 - guard let frame = iPhoneMirroringFrame() else { 72 - NSLog("MirrorTap: iPhone Mirroring window not found.") 73 - return 74 - } 75 - let target = CGPoint(x: frame.minX + frame.width * tapX, 76 - y: frame.minY + frame.height * tapY) 77 - let prev = NSWorkspace.shared.frontmostApplication 78 - runningApp(named: "iPhone Mirroring")?.activate(options: []) 79 - usleep(80_000) 80 - clickAt(target) 81 - usleep(40_000) 82 - prev?.activate(options: []) 83 - } 84 - 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. 88 - let actionScriptBody = """ 89 - #!/usr/bin/env bash 90 - # MirrorTap — Dragonframe action script. 91 - # Configured via Dragonframe Preferences → Advanced → Action Script. 92 - # 93 - # Args from Dragonframe: 94 - # $1 production $2 scene $3 take $4 event 95 - # $5 frame $6 exposure $7 expName $8 filename 96 - # 97 - # On SHOOT (the moment the user triggers capture) we ping MirrorTap via 98 - # its URL scheme, which makes the running app tap iPhone Mirroring. 99 - case "$4" in 100 - SHOOT) 101 - /usr/bin/open "mirrortap://capture" >/dev/null 2>&1 102 - ;; 103 - esac 104 - exit 0 105 - """ 106 - 107 - func actionScriptPath() -> String { 108 - let support = (NSSearchPathForDirectoriesInDomains( 109 - .applicationSupportDirectory, .userDomainMask, true).first ?? "") 110 - return support + "/MirrorTap/dragonframe-action.sh" 111 - } 112 - 113 - func installActionScript() -> String? { 114 - let path = actionScriptPath() 115 - let dir = (path as NSString).deletingLastPathComponent 116 - do { 117 - try FileManager.default.createDirectory(atPath: dir, 118 - withIntermediateDirectories: true) 119 - try actionScriptBody.write(toFile: path, atomically: true, encoding: .utf8) 120 - try FileManager.default.setAttributes( 121 - [.posixPermissions: NSNumber(value: 0o755)], ofItemAtPath: path) 122 - return path 123 - } catch { 124 - NSLog("MirrorTap: install failed — \(error)") 125 - return nil 126 - } 127 - } 128 - 129 - class AppDelegate: NSObject, NSApplicationDelegate { 130 - var statusItem: NSStatusItem! 131 - var menu: NSMenu! 132 - var idleImage: NSImage? 133 - var firingImage: NSImage? 134 - 135 - func applicationDidFinishLaunching(_ note: Notification) { 136 - let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 137 - _ = AXIsProcessTrustedWithOptions([promptKey: true] as CFDictionary) 138 - 139 - idleImage = NSImage(systemSymbolName: "camera.aperture", 140 - accessibilityDescription: "MirrorTap idle") 141 - firingImage = NSImage(systemSymbolName: "circle.inset.filled", 142 - accessibilityDescription: "MirrorTap firing") 143 - 144 - statusItem = NSStatusBar.system.statusItem( 145 - withLength: NSStatusItem.variableLength) 146 - if let btn = statusItem.button { 147 - btn.image = idleImage 148 - btn.imagePosition = .imageOnly 149 - btn.target = self 150 - btn.action = #selector(handleClick(_:)) 151 - btn.sendAction(on: [.leftMouseUp, .rightMouseUp]) 152 - btn.toolTip = "MirrorTap — click to tap iPhone Mirroring" 153 - } 154 - 155 - menu = NSMenu() 156 - let header = NSMenuItem(title: "MirrorTap", action: nil, keyEquivalent: "") 157 - header.isEnabled = false 158 - menu.addItem(header) 159 - menu.addItem(NSMenuItem.separator()) 160 - 161 - let tapItem = NSMenuItem(title: "Tap iPhone Now", 162 - action: #selector(tapFromMenu), 163 - keyEquivalent: "") 164 - tapItem.target = self 165 - menu.addItem(tapItem) 166 - 167 - menu.addItem(NSMenuItem.separator()) 168 - let installItem = NSMenuItem(title: "Install Dragonframe Hook…", 169 - action: #selector(installAndReveal), 170 - keyEquivalent: "") 171 - installItem.target = self 172 - menu.addItem(installItem) 173 - 174 - menu.addItem(NSMenuItem.separator()) 175 - menu.addItem(NSMenuItem(title: "Quit MirrorTap", 176 - action: #selector(NSApplication.terminate(_:)), 177 - keyEquivalent: "q")) 178 - } 179 - 180 - // URL scheme handler — Dragonframe's action script calls this via 181 - // `open mirrortap://capture` to ask us to tap iPhone Mirroring. 182 - func application(_ app: NSApplication, open urls: [URL]) { 183 - for url in urls where url.scheme == "mirrortap" { 184 - let host = url.host?.lowercased() ?? url.path.replacingOccurrences( 185 - of: "/", with: "") 186 - switch host { 187 - case "capture", "tap": 188 - flashIcon() 189 - tapPhone() 190 - default: 191 - NSLog("MirrorTap: unknown URL \(url)") 192 - } 193 - } 194 - } 195 - 196 - @objc func handleClick(_ sender: Any?) { 197 - let event = NSApp.currentEvent 198 - let isRight = event?.type == .rightMouseUp 199 - let isCtrl = event?.modifierFlags.contains(.control) ?? false 200 - if isRight || isCtrl { showMenu(); return } 201 - flashIcon() 202 - tapPhone() 203 - } 204 - 205 - func showMenu() { 206 - statusItem.menu = menu 207 - statusItem.button?.performClick(nil) 208 - DispatchQueue.main.async { self.statusItem.menu = nil } 209 - } 210 - 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 215 - } 216 - } 217 - 218 - @objc func tapFromMenu() { flashIcon(); tapPhone() } 219 - 220 - @objc func installAndReveal() { 221 - guard let path = installActionScript() else { 222 - let a = NSAlert() 223 - a.messageText = "Couldn't write the action script." 224 - a.informativeText = "Check ~/Library/Application Support permissions." 225 - a.runModal() 226 - return 227 - } 228 - NSPasteboard.general.clearContents() 229 - NSPasteboard.general.setString(path, forType: .string) 230 - NSWorkspace.shared.activateFileViewerSelecting( 231 - [URL(fileURLWithPath: path)]) 232 - 233 - NSApp.activate(ignoringOtherApps: true) 234 - let a = NSAlert() 235 - a.messageText = "Dragonframe hook installed" 236 - a.informativeText = """ 237 - Path (also copied to clipboard): 238 - \(path) 239 - 240 - Next: in Dragonframe → Preferences → Advanced → Action Script, 241 - choose this file. From then on, every SHOOT in Dragonframe 242 - will also tap iPhone Mirroring. 243 - """ 244 - a.addButton(withTitle: "OK") 245 - a.runModal() 246 - } 247 - } 248 - 249 - let app = NSApplication.shared 250 - let delegate = AppDelegate() 251 - app.delegate = delegate 252 - app.setActivationPolicy(.accessory) 253 - app.run()
+4
slab/yergersnap/.gitignore
··· 1 + build/ 2 + YergerSnap.app/ 3 + icon.iconset/ 4 + *.zip
slab/yergersnap/AppIcon.icns

This is a binary file and will not be displayed.

+131
slab/yergersnap/build.sh
··· 1 + #!/usr/bin/env bash 2 + # build.sh — compile, bundle, sign, notarize, staple → YergerSnap.app 3 + # 4 + # Pulls the Developer ID cert + notarization creds from the vault at 5 + # aesthetic-computer-vault/ac-electron/.env. The .p12 is imported into a 6 + # build-only keychain on first run so we don't pollute the login keychain. 7 + # 8 + # Usage: 9 + # ./build.sh # build + sign + notarize + staple 10 + # ./build.sh --no-notary # build + sign only (faster; opens with right-click) 11 + # 12 + # Output: YergerSnap.app in this directory. 13 + 14 + set -euo pipefail 15 + 16 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 17 + cd "$SCRIPT_DIR" 18 + 19 + VAULT_ENV="$(cd "$SCRIPT_DIR/../../aesthetic-computer-vault/ac-electron" && pwd)/.env" 20 + NOTARIZE=1 21 + [[ "${1:-}" == "--no-notary" ]] && NOTARIZE=0 22 + 23 + if [[ ! -f "$VAULT_ENV" ]]; then 24 + echo "vault env missing at $VAULT_ENV" >&2 25 + exit 1 26 + fi 27 + # shellcheck disable=SC1090 28 + source "$VAULT_ENV" 29 + : "${CSC_KEY_PASSWORD:?missing CSC_KEY_PASSWORD}" 30 + : "${APPLE_ID:?missing APPLE_ID}" 31 + : "${APPLE_APP_SPECIFIC_PASSWORD:?missing APPLE_APP_SPECIFIC_PASSWORD}" 32 + : "${APPLE_TEAM_ID:?missing APPLE_TEAM_ID}" 33 + 34 + P12="$(cd "$(dirname "$VAULT_ENV")" && pwd)/mac-developer-id.p12" 35 + [[ -f "$P12" ]] || { echo "missing $P12" >&2; exit 1; } 36 + 37 + APP_NAME="YergerSnap" 38 + APP_BUNDLE="$SCRIPT_DIR/$APP_NAME.app" 39 + BUILD_DIR="$SCRIPT_DIR/build" 40 + KEYCHAIN="$BUILD_DIR/build.keychain" 41 + KEYCHAIN_PASS="yergersnap-build" 42 + 43 + rm -rf "$BUILD_DIR" "$APP_BUNDLE" 44 + mkdir -p "$BUILD_DIR" "$APP_BUNDLE/Contents/MacOS" "$APP_BUNDLE/Contents/Resources" 45 + 46 + echo "==> compile" 47 + swiftc -O -o "$APP_BUNDLE/Contents/MacOS/$APP_NAME" tap.swift \ 48 + -framework Cocoa -framework ApplicationServices -framework Carbon 49 + 50 + cp Info.plist "$APP_BUNDLE/Contents/Info.plist" 51 + 52 + if [[ -f AppIcon.icns ]]; then 53 + cp AppIcon.icns "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 54 + elif [[ -f icon.iconset ]]; then 55 + iconutil -c icns -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" icon.iconset 56 + fi 57 + 58 + echo "==> import cert into ephemeral keychain" 59 + # Capture the original search list before we touch anything so we can 60 + # always restore it. If a stale build keychain is registered, drop it. 61 + ORIG_SEARCH=$(security list-keychains -d user \ 62 + | sed -e 's/"//g' -e 's/^[[:space:]]*//' \ 63 + | grep -v 'yergersnap/build/build.keychain' || true) 64 + security delete-keychain "$KEYCHAIN" 2>/dev/null || true 65 + 66 + security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" 67 + security set-keychain-settings -lut 21600 "$KEYCHAIN" 68 + security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" 69 + security import "$P12" -k "$KEYCHAIN" -P "$CSC_KEY_PASSWORD" -A >/dev/null 70 + security set-key-partition-list -S apple-tool:,apple:,codesign: \ 71 + -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" >/dev/null 72 + 73 + # The .p12 holds only the leaf — fetch Apple's Developer ID G2 intermediate 74 + # so the chain validates. Cached after the first build. 75 + G2_CA="$BUILD_DIR/DeveloperIDG2CA.cer" 76 + if [[ ! -f "$G2_CA" ]]; then 77 + curl -fsSL -o "$G2_CA" \ 78 + https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer 79 + fi 80 + security import "$G2_CA" -k "$KEYCHAIN" >/dev/null 81 + 82 + # Prepend the build keychain to the search list so codesign can find it. 83 + security list-keychains -d user -s "$KEYCHAIN" $ORIG_SEARCH 84 + 85 + IDENTITY=$(security find-identity -p codesigning -v "$KEYCHAIN" \ 86 + | grep -E 'Developer ID Application' | head -1 \ 87 + | sed -E 's/.*"(Developer ID Application: [^"]+)".*/\1/') 88 + [[ -n "$IDENTITY" ]] || { echo "no Developer ID Application identity found" >&2; exit 1; } 89 + echo " identity: $IDENTITY" 90 + 91 + echo "==> codesign (hardened runtime)" 92 + codesign --force --options runtime --timestamp \ 93 + --entitlements YergerSnap.entitlements \ 94 + --sign "$IDENTITY" --keychain "$KEYCHAIN" \ 95 + "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 96 + codesign --force --options runtime --timestamp \ 97 + --entitlements YergerSnap.entitlements \ 98 + --sign "$IDENTITY" --keychain "$KEYCHAIN" \ 99 + "$APP_BUNDLE" 100 + 101 + echo "==> verify signature" 102 + codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE" 103 + 104 + if [[ "$NOTARIZE" == "1" ]]; then 105 + ZIP="$BUILD_DIR/$APP_NAME.zip" 106 + echo "==> zip for notarization" 107 + /usr/bin/ditto -c -k --keepParent "$APP_BUNDLE" "$ZIP" 108 + 109 + echo "==> submit to notary (this can take a minute or three)" 110 + xcrun notarytool submit "$ZIP" \ 111 + --apple-id "$APPLE_ID" \ 112 + --team-id "$APPLE_TEAM_ID" \ 113 + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ 114 + --wait 115 + 116 + echo "==> staple" 117 + xcrun stapler staple "$APP_BUNDLE" 118 + spctl -a -vv "$APP_BUNDLE" || true 119 + fi 120 + 121 + # Restore original keychain search list (don't leave the build keychain pinned). 122 + security list-keychains -d user -s $ORIG_SEARCH 123 + security delete-keychain "$KEYCHAIN" 2>/dev/null || true 124 + 125 + echo 126 + echo "✓ built $APP_BUNDLE" 127 + if [[ "$NOTARIZE" == "1" ]]; then 128 + echo " (signed + notarized — Gatekeeper will allow on any Mac)" 129 + else 130 + echo " (signed only — first launch needs right-click → Open)" 131 + fi
+134
slab/yergersnap/make-icon.swift
··· 1 + #!/usr/bin/env swift 2 + // 3 + // make-icon.swift — generate AppIcon.icns from scratch. 4 + // Produces icon.iconset/ + AppIcon.icns next to this file. 5 + // 6 + // Run with: swift make-icon.swift 7 + // 8 + // Design: a camera-aperture shutter centered on a deep purple→navy 9 + // gradient, with two faint concentric ripples around it suggesting 10 + // the "snap" tap. 11 + 12 + import Cocoa 13 + 14 + let canvas: CGFloat = 1024 15 + let outIconset = "icon.iconset" 16 + let outIcns = "AppIcon.icns" 17 + 18 + // ---- render the master 1024x1024 image ---------------------------------- 19 + let master = NSImage(size: NSSize(width: canvas, height: canvas)) 20 + master.lockFocus() 21 + let ctx = NSGraphicsContext.current!.cgContext 22 + 23 + // Rounded-rect clip (macOS app-icon corner radius is roughly 22.5%). 24 + let bounds = NSRect(x: 0, y: 0, width: canvas, height: canvas) 25 + let cornerRadius = canvas * 0.2237 26 + NSBezierPath(roundedRect: bounds, 27 + xRadius: cornerRadius, 28 + yRadius: cornerRadius).addClip() 29 + 30 + // Background gradient — deep violet to near-black. 31 + let bg = NSGradient(colors: [ 32 + NSColor(calibratedRed: 0.18, green: 0.10, blue: 0.34, alpha: 1.0), 33 + NSColor(calibratedRed: 0.05, green: 0.03, blue: 0.13, alpha: 1.0), 34 + ])! 35 + bg.draw(in: bounds, angle: 270) 36 + 37 + // Soft top-side highlight so it doesn't read as flat. 38 + let highlight = NSGradient(colors: [ 39 + NSColor.white.withAlphaComponent(0.10), 40 + NSColor.clear, 41 + ])! 42 + highlight.draw( 43 + fromCenter: NSPoint(x: canvas * 0.5, y: canvas * 0.66), radius: 0, 44 + toCenter: NSPoint(x: canvas * 0.5, y: canvas * 0.66), radius: canvas * 0.55, 45 + options: []) 46 + 47 + // Shutter — render SF Symbol "camera.aperture" big and tint white. 48 + let symConfig = NSImage.SymbolConfiguration(pointSize: 560, weight: .light) 49 + let symbol = NSImage(systemSymbolName: "camera.aperture", 50 + accessibilityDescription: nil)! 51 + .withSymbolConfiguration(symConfig)! 52 + let symSize = symbol.size 53 + 54 + let tinted = NSImage(size: symSize) 55 + tinted.lockFocus() 56 + NSColor.white.setFill() 57 + NSRect(origin: .zero, size: symSize).fill() 58 + symbol.draw(in: NSRect(origin: .zero, size: symSize), 59 + from: .zero, operation: .destinationIn, fraction: 1) 60 + tinted.unlockFocus() 61 + 62 + let symFrame = NSRect( 63 + x: (canvas - symSize.width) / 2, 64 + y: (canvas - symSize.height) / 2, 65 + width: symSize.width, height: symSize.height) 66 + tinted.draw(in: symFrame, from: .zero, operation: .sourceOver, fraction: 0.96) 67 + 68 + // Tap ripples — two thin concentric rings outside the shutter. 69 + ctx.setLineCap(.round) 70 + let center = NSPoint(x: canvas * 0.5, y: canvas * 0.5) 71 + for (radius, alpha, lineWidth) in [ 72 + (canvas * 0.430, CGFloat(0.18), CGFloat(4.0)), 73 + (canvas * 0.482, CGFloat(0.10), CGFloat(3.0)), 74 + ] { 75 + NSColor.white.withAlphaComponent(alpha).setStroke() 76 + let ring = NSBezierPath(ovalIn: NSRect( 77 + x: center.x - radius, y: center.y - radius, 78 + width: radius * 2, height: radius * 2)) 79 + ring.lineWidth = lineWidth 80 + ring.stroke() 81 + } 82 + 83 + master.unlockFocus() 84 + 85 + // ---- slice into the 10 iconutil sizes ----------------------------------- 86 + let fm = FileManager.default 87 + try? fm.removeItem(atPath: outIconset) 88 + try fm.createDirectory(atPath: outIconset, withIntermediateDirectories: true) 89 + 90 + let slices: [(String, Int)] = [ 91 + ("icon_16x16", 16), 92 + ("icon_16x16@2x", 32), 93 + ("icon_32x32", 32), 94 + ("icon_32x32@2x", 64), 95 + ("icon_128x128", 128), 96 + ("icon_128x128@2x", 256), 97 + ("icon_256x256", 256), 98 + ("icon_256x256@2x", 512), 99 + ("icon_512x512", 512), 100 + ("icon_512x512@2x", 1024), 101 + ] 102 + 103 + for (name, px) in slices { 104 + let scaled = NSImage(size: NSSize(width: px, height: px)) 105 + scaled.lockFocus() 106 + NSGraphicsContext.current?.imageInterpolation = .high 107 + master.draw(in: NSRect(x: 0, y: 0, width: px, height: px), 108 + from: .zero, operation: .sourceOver, fraction: 1) 109 + scaled.unlockFocus() 110 + guard let cg = scaled.cgImage(forProposedRect: nil, context: nil, 111 + hints: nil) else { 112 + fputs("failed to cgImage \(name)\n", stderr) 113 + exit(1) 114 + } 115 + let bm = NSBitmapImageRep(cgImage: cg) 116 + guard let data = bm.representation(using: .png, properties: [:]) else { 117 + fputs("failed to encode \(name)\n", stderr) 118 + exit(1) 119 + } 120 + try data.write(to: URL(fileURLWithPath: "\(outIconset)/\(name).png")) 121 + } 122 + 123 + // ---- compile to .icns --------------------------------------------------- 124 + let task = Process() 125 + task.executableURL = URL(fileURLWithPath: "/usr/bin/iconutil") 126 + task.arguments = ["-c", "icns", "-o", outIcns, outIconset] 127 + try task.run() 128 + task.waitUntilExit() 129 + if task.terminationStatus != 0 { 130 + fputs("iconutil failed: \(task.terminationStatus)\n", stderr) 131 + exit(1) 132 + } 133 + 134 + print("✓ wrote \(outIcns) (also kept \(outIconset)/ for inspection)")
+253
slab/yergersnap/tap.swift
··· 1 + // tap.swift — YergerSnap menubar app. 2 + // 3 + // Posts a synthetic click on the iPhone Mirroring window's Capture button 4 + // in response to two triggers: 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 11 + // mirrored iPhone, with no fake keystrokes. 12 + // 13 + // 2. The menu-bar button (manual / fallback). Left-click taps the 14 + // iPhone immediately. Right-click opens the menu. 15 + // 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. 19 + // 20 + // Calibration: tapX/tapY are window-relative fractions for where to 21 + // click inside the iPhone Mirroring window. Default = bottom center. 22 + // Tweak and rebuild via ./build.sh. 23 + 24 + import Cocoa 25 + import ApplicationServices 26 + import Carbon.HIToolbox 27 + 28 + // ---- config -------------------------------------------------------------- 29 + let tapX: CGFloat = 0.50 30 + let tapY: CGFloat = 0.90 31 + // -------------------------------------------------------------------------- 32 + 33 + func runningApp(named name: String) -> NSRunningApplication? { 34 + NSWorkspace.shared.runningApplications.first { $0.localizedName == name } 35 + } 36 + 37 + func iPhoneMirroringFrame() -> CGRect? { 38 + guard let pid = runningApp(named: "iPhone Mirroring")?.processIdentifier 39 + else { return nil } 40 + let app = AXUIElementCreateApplication(pid) 41 + var windowsRef: CFTypeRef? 42 + guard AXUIElementCopyAttributeValue(app, kAXWindowsAttribute as CFString, 43 + &windowsRef) == .success, 44 + let windows = windowsRef as? [AXUIElement], 45 + let win = windows.first else { return nil } 46 + var posRef: CFTypeRef? 47 + var sizeRef: CFTypeRef? 48 + guard AXUIElementCopyAttributeValue(win, kAXPositionAttribute as CFString, 49 + &posRef) == .success, 50 + AXUIElementCopyAttributeValue(win, kAXSizeAttribute as CFString, 51 + &sizeRef) == .success 52 + else { return nil } 53 + var pos = CGPoint.zero, size = CGSize.zero 54 + AXValueGetValue(posRef as! AXValue, .cgPoint, &pos) 55 + AXValueGetValue(sizeRef as! AXValue, .cgSize, &size) 56 + return CGRect(origin: pos, size: size) 57 + } 58 + 59 + func clickAt(_ point: CGPoint) { 60 + let saved = CGEvent(source: nil)?.location 61 + let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, 62 + mouseCursorPosition: point, mouseButton: .left) 63 + let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, 64 + mouseCursorPosition: point, mouseButton: .left) 65 + down?.post(tap: .cghidEventTap) 66 + up?.post(tap: .cghidEventTap) 67 + if let saved = saved { CGWarpMouseCursorPosition(saved) } 68 + } 69 + 70 + func tapPhone() { 71 + guard let frame = iPhoneMirroringFrame() else { 72 + NSLog("YergerSnap: iPhone Mirroring window not found.") 73 + return 74 + } 75 + let target = CGPoint(x: frame.minX + frame.width * tapX, 76 + y: frame.minY + frame.height * tapY) 77 + let prev = NSWorkspace.shared.frontmostApplication 78 + runningApp(named: "iPhone Mirroring")?.activate(options: []) 79 + usleep(80_000) 80 + clickAt(target) 81 + usleep(40_000) 82 + prev?.activate(options: []) 83 + } 84 + 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. 88 + let actionScriptBody = """ 89 + #!/usr/bin/env bash 90 + # YergerSnap — Dragonframe action script. 91 + # Configured via Dragonframe Preferences → Advanced → Action Script. 92 + # 93 + # Args from Dragonframe: 94 + # $1 production $2 scene $3 take $4 event 95 + # $5 frame $6 exposure $7 expName $8 filename 96 + # 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. 99 + case "$4" in 100 + SHOOT) 101 + /usr/bin/open "yergersnap://capture" >/dev/null 2>&1 102 + ;; 103 + esac 104 + exit 0 105 + """ 106 + 107 + func actionScriptPath() -> String { 108 + let support = (NSSearchPathForDirectoriesInDomains( 109 + .applicationSupportDirectory, .userDomainMask, true).first ?? "") 110 + return support + "/YergerSnap/dragonframe-action.sh" 111 + } 112 + 113 + func installActionScript() -> String? { 114 + let path = actionScriptPath() 115 + let dir = (path as NSString).deletingLastPathComponent 116 + do { 117 + try FileManager.default.createDirectory(atPath: dir, 118 + withIntermediateDirectories: true) 119 + try actionScriptBody.write(toFile: path, atomically: true, encoding: .utf8) 120 + try FileManager.default.setAttributes( 121 + [.posixPermissions: NSNumber(value: 0o755)], ofItemAtPath: path) 122 + return path 123 + } catch { 124 + NSLog("YergerSnap: install failed — \(error)") 125 + return nil 126 + } 127 + } 128 + 129 + class AppDelegate: NSObject, NSApplicationDelegate { 130 + var statusItem: NSStatusItem! 131 + var menu: NSMenu! 132 + var idleImage: NSImage? 133 + var firingImage: NSImage? 134 + 135 + func applicationDidFinishLaunching(_ note: Notification) { 136 + let promptKey = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String 137 + _ = AXIsProcessTrustedWithOptions([promptKey: true] as CFDictionary) 138 + 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) 146 + if let btn = statusItem.button { 147 + btn.image = idleImage 148 + btn.imagePosition = .imageOnly 149 + btn.target = self 150 + btn.action = #selector(handleClick(_:)) 151 + btn.sendAction(on: [.leftMouseUp, .rightMouseUp]) 152 + btn.toolTip = "YergerSnap — click to tap iPhone Mirroring" 153 + } 154 + 155 + menu = NSMenu() 156 + let header = NSMenuItem(title: "YergerSnap", action: nil, keyEquivalent: "") 157 + header.isEnabled = false 158 + menu.addItem(header) 159 + menu.addItem(NSMenuItem.separator()) 160 + 161 + let tapItem = NSMenuItem(title: "Tap iPhone Now", 162 + action: #selector(tapFromMenu), 163 + keyEquivalent: "") 164 + tapItem.target = self 165 + menu.addItem(tapItem) 166 + 167 + menu.addItem(NSMenuItem.separator()) 168 + let installItem = NSMenuItem(title: "Install Dragonframe Hook…", 169 + action: #selector(installAndReveal), 170 + keyEquivalent: "") 171 + installItem.target = self 172 + menu.addItem(installItem) 173 + 174 + menu.addItem(NSMenuItem.separator()) 175 + menu.addItem(NSMenuItem(title: "Quit YergerSnap", 176 + action: #selector(NSApplication.terminate(_:)), 177 + keyEquivalent: "q")) 178 + } 179 + 180 + // URL scheme handler — Dragonframe's action script calls this via 181 + // `open yergersnap://capture` to ask us to tap iPhone Mirroring. 182 + func application(_ app: NSApplication, open urls: [URL]) { 183 + for url in urls where url.scheme == "yergersnap" { 184 + let host = url.host?.lowercased() ?? url.path.replacingOccurrences( 185 + of: "/", with: "") 186 + switch host { 187 + case "capture", "tap": 188 + flashIcon() 189 + tapPhone() 190 + default: 191 + NSLog("YergerSnap: unknown URL \(url)") 192 + } 193 + } 194 + } 195 + 196 + @objc func handleClick(_ sender: Any?) { 197 + let event = NSApp.currentEvent 198 + let isRight = event?.type == .rightMouseUp 199 + let isCtrl = event?.modifierFlags.contains(.control) ?? false 200 + if isRight || isCtrl { showMenu(); return } 201 + flashIcon() 202 + tapPhone() 203 + } 204 + 205 + func showMenu() { 206 + statusItem.menu = menu 207 + statusItem.button?.performClick(nil) 208 + DispatchQueue.main.async { self.statusItem.menu = nil } 209 + } 210 + 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 215 + } 216 + } 217 + 218 + @objc func tapFromMenu() { flashIcon(); tapPhone() } 219 + 220 + @objc func installAndReveal() { 221 + guard let path = installActionScript() else { 222 + let a = NSAlert() 223 + a.messageText = "Couldn't write the action script." 224 + a.informativeText = "Check ~/Library/Application Support permissions." 225 + a.runModal() 226 + return 227 + } 228 + NSPasteboard.general.clearContents() 229 + NSPasteboard.general.setString(path, forType: .string) 230 + NSWorkspace.shared.activateFileViewerSelecting( 231 + [URL(fileURLWithPath: path)]) 232 + 233 + NSApp.activate(ignoringOtherApps: true) 234 + let a = NSAlert() 235 + a.messageText = "Dragonframe hook installed" 236 + a.informativeText = """ 237 + Path (also copied to clipboard): 238 + \(path) 239 + 240 + Next: in Dragonframe → Preferences → Advanced → Action Script, 241 + choose this file. From then on, every SHOOT in Dragonframe 242 + will also tap iPhone Mirroring. 243 + """ 244 + a.addButton(withTitle: "OK") 245 + a.runModal() 246 + } 247 + } 248 + 249 + let app = NSApplication.shared 250 + let delegate = AppDelegate() 251 + app.delegate = delegate 252 + app.setActivationPolicy(.accessory) 253 + app.run()