-
Notifications
You must be signed in to change notification settings - Fork 0
Add screenshot and beautify scripts for app window captures #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <in.png> <out.png> | ||
|
|
||
| import AppKit | ||
|
|
||
| guard CommandLine.arguments.count == 3 else { | ||
| print("usage: beautify.swift <in.png> <out.png>") | ||
| 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))") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <app name> <out.png> [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 <app name> <out.png> [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)") |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.