From 421cd822476e6419fa54293b248e60dd09f9b2b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 23:55:27 +0000 Subject: [PATCH] Add window screenshot capture + beautify scripts for site images scripts/screenshot.swift finds an app's on-screen window (optionally by title substring) via CGWindowListCopyWindowInfo and shells out to screencapture -l, keeping the macOS corner radius and drop shadow in the PNG alpha. scripts/beautify.swift composites that capture onto the site's brand gradient with proportional padding. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YB7DWdZjLUzBvjaemnSBk4 --- scripts/beautify.swift | 57 +++++++++++++++++++++++++ scripts/screenshot.swift | 92 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 scripts/beautify.swift create mode 100644 scripts/screenshot.swift diff --git a/scripts/beautify.swift b/scripts/beautify.swift new file mode 100644 index 0000000..4a4bfa7 --- /dev/null +++ b/scripts/beautify.swift @@ -0,0 +1,57 @@ +#!/usr/bin/env swift +// Composites a window screenshot (with alpha shadow) onto a gradient backdrop — +// the "nice screenshot" treatment for site images. Capture the input with +// scripts/screenshot.swift, which preserves the shadow alpha this expects. +// Usage: swift scripts/beautify.swift + +import AppKit + +guard CommandLine.arguments.count == 3 else { + print("usage: beautify.swift ") + exit(1) +} +let inputURL = URL(fileURLWithPath: CommandLine.arguments[1]) +let outputURL = URL(fileURLWithPath: CommandLine.arguments[2]) + +guard let image = NSImage(contentsOf: inputURL), + let rep = image.representations.first as? NSBitmapImageRep +else { + print("could not read \(inputURL.path)") + exit(1) +} +let shotW = rep.pixelsWide +let shotH = rep.pixelsHigh + +// Padding proportional to the shot, capped for consistency across sizes. +let pad = min(max(Int(Double(min(shotW, shotH)) * 0.07), 72), 140) +let outW = shotW + pad * 2 +let outH = shotH + pad * 2 + +let out = NSBitmapImageRep( + bitmapDataPlanes: nil, pixelsWide: outW, pixelsHigh: outH, + bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, + colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)! +NSGraphicsContext.saveGraphicsState() +NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: out) + +// Brand gradient (matches the site's accent colors), angled. +let gradient = NSGradient(colors: [ + NSColor(calibratedRed: 0.24, green: 0.36, blue: 0.86, alpha: 1), + NSColor(calibratedRed: 0.42, green: 0.23, blue: 0.86, alpha: 1), + NSColor(calibratedRed: 0.58, green: 0.25, blue: 0.78, alpha: 1), +])! +gradient.draw(in: NSRect(x: 0, y: 0, width: outW, height: outH), angle: -35) + +// The captured window already carries macOS corner radius + shadow in its alpha. +let dest = NSRect(x: pad, y: pad, width: shotW, height: shotH) +rep.draw( + in: dest, from: .zero, operation: .sourceOver, fraction: 1.0, + respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue]) + +NSGraphicsContext.restoreGraphicsState() +guard let data = out.representation(using: .png, properties: [:]) else { + print("encode failed") + exit(1) +} +try! data.write(to: outputURL) +print("wrote \(outputURL.path) (\(outW)x\(outH))") diff --git a/scripts/screenshot.swift b/scripts/screenshot.swift new file mode 100644 index 0000000..d7d9361 --- /dev/null +++ b/scripts/screenshot.swift @@ -0,0 +1,92 @@ +#!/usr/bin/env swift +// Captures a single app window to PNG with the macOS corner radius + drop +// shadow preserved in the alpha channel — exactly the input +// scripts/beautify.swift composites onto the gradient backdrop. +// +// Needs Screen Recording permission (System Settings → Privacy & Security); +// screencapture prompts for it on first use. +// +// Usage: swift scripts/screenshot.swift [window title substring] +// swift scripts/screenshot.swift Blurt /tmp/blurt.png +// swift scripts/screenshot.swift Blurt /tmp/settings.png Settings + +import AppKit +import CoreGraphics + +let args = CommandLine.arguments +guard args.count == 3 || args.count == 4 else { + print("usage: screenshot.swift [window title substring]") + exit(1) +} +let appName = args[1] +let outputPath = args[2] +let titleFilter = args.count == 4 ? args[3] : nil + +// On-screen windows, front to back. Layer 0 keeps ordinary document/settings +// windows and drops menu bar extras, overlays, and the Dock; the size floor +// drops helper popovers. +guard + let windowList = CGWindowListCopyWindowInfo( + [.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] +else { + print("could not read the window list") + exit(1) +} + +struct Candidate { + let id: Int + let title: String + let width: Int + let height: Int +} + +let candidates: [Candidate] = windowList.compactMap { info in + guard let owner = info[kCGWindowOwnerName as String] as? String, owner == appName, + let layer = info[kCGWindowLayer as String] as? Int, layer == 0, + let id = info[kCGWindowNumber as String] as? Int, + let bounds = info[kCGWindowBounds as String] as? [String: Any], + let width = bounds["Width"] as? Double, let height = bounds["Height"] as? Double, + width >= 64, height >= 64 + else { return nil } + // Window titles are only visible here once Screen Recording is granted — + // the same permission screencapture itself needs below. + let title = info[kCGWindowName as String] as? String ?? "" + return Candidate(id: id, title: title, width: Int(width), height: Int(height)) +} + +let matches: [Candidate] +if let titleFilter { + matches = candidates.filter { $0.title.localizedCaseInsensitiveContains(titleFilter) } +} else { + matches = candidates +} + +guard let window = matches.first else { + let filterNote = titleFilter.map { " with title containing \"\($0)\"" } ?? "" + print("no on-screen window matched app \"\(appName)\"\(filterNote)") + for c in candidates { + let title = c.title.isEmpty ? "(untitled — grant Screen Recording to see titles)" : c.title + print(" [\(c.id)] \(title) \(c.width)x\(c.height)") + } + exit(1) +} + +// screencapture keeps the window's rounded corners and drop shadow in the +// PNG's alpha channel by default (-o would strip the shadow, which beautify +// relies on); -x mutes the shutter sound. +let task = Process() +task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture") +task.arguments = ["-x", "-t", "png", "-l", String(window.id), outputPath] +do { + try task.run() +} catch { + print("could not launch screencapture: \(error.localizedDescription)") + exit(1) +} +task.waitUntilExit() +guard task.terminationStatus == 0, FileManager.default.fileExists(atPath: outputPath) else { + print("screencapture failed — is Screen Recording granted in System Settings?") + exit(1) +} +let titleNote = window.title.isEmpty ? "" : " — \(window.title)" +print("captured \"\(appName)\"\(titleNote) (\(window.width)x\(window.height)) to \(outputPath)")