--- name: gui-debug description: Verify a rendering or window-chrome change in any macOS GUI app unattended — find its CGWindowID via JXA, capture that window alone (with per-pixel alpha) using screencapture -l, and read exact RGBA via NSBitmapImageRep. Use for blur/opacity/corner-radius/glyph/color rendering bugs where a screenshot or pixel value is the ground truth. No Python and no Accessibility permission needed. --- # macOS GUI debugging Verify a rendering or window-chrome change by launching the app, capturing **just its window**, and probing pixels. None of this needs Accessibility permission. (System Events / keystroke injection _does_ need it and is usually blocked — don't plan on typing into the window unattended.) Screen Recording permission is required for screenshots: if `screencapture -x /tmp/full.png` produces a non-black image, you have it. Throughout, replace `MyApp` with the app's window-owner name (what shows in the menu bar / Activity Monitor) and `myapp` with the process name for `pkill -x`. ## Launch → window id → capture ```sh ./path/to/MyApp 2>/tmp/app.log & sleep 4 # or: open -a MyApp WID=$(osascript -l JavaScript -e ' ObjC.import("CoreGraphics"); const list = ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, $.kCGNullWindowID)); const arr = ObjC.deepUnwrap(list); const w = arr.filter(x => x.kCGWindowOwnerName === "MyApp")[0]; w ? w.kCGWindowNumber : "none";') screencapture -o -l$WID /tmp/app-win.png # window only, no drop shadow ``` - `ObjC.castRefToObject` is required — `ObjC.deepUnwrap` on the raw `CFArrayRef` fails with "Ref has incompatible type". - `screencapture -o -l` captures the window's own composited content **with per-pixel alpha**: a translucent background shows its real alpha (e.g. 153 for `opacity = 0.6`), and rounded-corner pixels read fully transparent. This is the ground truth for transparency / corner-clip checks — a full-screen capture is not. - A tiling WM (or the app itself) may resize the window after launch — re-read `kCGWindowBounds` instead of assuming a fixed size. The post-resize capture also catches relayout bugs (AppKit reorders sublayers on the first layout pass). ## Pixel probe — exact RGBA, no Python ```sh osascript -l JavaScript -e ' ObjC.import("AppKit"); const rep = $.NSBitmapImageRep.imageRepWithContentsOfFile("/tmp/app-win.png"); function p(x,y){const c=rep.colorAtXY(x,y);return [c.redComponent,c.greenComponent,c.blueComponent,c.alphaComponent].map(v=>Math.round(v*255)).join(",");} const h = rep.pixelsHigh*1, w = rep.pixelsWide*1; JSON.stringify({w:w,h:h,center:p(Math.floor(w/2),Math.floor(h/2)),tl:p(2,2),bl:p(2,h-3),br:p(w-3,h-3)});' ``` - Corner pixels reading `0,0,0,0` = the rounded clip works. - A frosted-glass / vibrancy background (`NSVisualEffectView`) behind a Metal/CALayer reads centre alpha `255` even when "transparent" — that's the opaque blur material, not a transparency failure. Test raw alpha with blur disabled. ## Process hygiene - `pkill -x myapp` — match the **exact** process name. Never `pkill -f path/to/MyApp`, which also matches helper processes (renderers, daemons) whose argv contains that path and can kill unrelated user sessions. - Comparing against an older build: use a separate build dir / git worktree so the two binaries don't clobber each other's artifacts. ## Multi-display gotchas - `screencapture -x -R` fails with "could not create image from rect" across display boundaries. Capture per display (`-D 1`, `-D 2`) or stick to window captures (`-l`), which are display-agnostic.