## 长图 docs/poster.png:六段式,一张图讲完这个工具是什么。 生成脚本 Resources/make-poster.swift,改文案或调顺序改那个数组即可。 修了个坑:NSImage.size 返回的是**点**不是像素,这批截图是 2x 抓的, 1880px 的图它报 940,于是大图被当小图处理,第一版只占了半幅。 ## 截图 scripts/shoot-screenshots.sh:用 HUB_DEMO=1 + HUB_ISLAND_STATE 把形态钉死, 自动截 + 自动裁掉透明留白,五种形态全自动。 之前那批是改名前截的,岛头还写着旧名。重截之后顺带修好三件事: - 演示工作区路径 claude-hub-demo → vibe-foreman-demo - 新图带上了「在问你」横幅和状态徽章,比旧图更能说明问题 - 删掉 04-main-window.png:验收页本来就带主窗口侧边栏,一张顶两张, 而且那张的标题栏是旧名 两个配套小工具: - scripts/find-window.swift 拿窗口 id 给 screencapture -l 用。 CGWindowList 报的是显示名不是可执行名,写死 ClaudeHub 一个都找不到。 - scripts/crop-alpha.swift 裁到不透明内容的外接矩形。岛画在 640×720 的 透明容器窗口里,不裁的话拼进长图就是一大块空白。 截图脚本踩了 NOTES.md 里记过的那个 bash 坑:变量后面紧跟中文标点 (`$state)`)会被算进变量名,set -u 下直接报未定义。加花括号。 窗口探测改成轮询而不是固定 sleep —— 实测 4 秒对 hover 够、对 expanded 不够。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
43 lines
1.8 KiB
Swift
43 lines
1.8 KiB
Swift
import CoreGraphics
|
||
import Foundation
|
||
|
||
// 找出灵动岛那个窗口的 id,给 `screencapture -l` 用。
|
||
//
|
||
// 为什么不用 AppleScript:岛是个 `.nonactivatingPanel` 的无边框 NSPanel,
|
||
// System Events 那套 accessibility 接口对它基本是瞎的(拿不到 window 1),
|
||
// 而且走那条路要 Accessibility 授权。CGWindowList 只需要屏幕录制权限,
|
||
// 而截图本来就要那个权限,不多欠一份。
|
||
//
|
||
// 输出:`<窗口id> <宽> <高>`,按面积从大到小。最大的那个就是岛
|
||
//(app 还会有几个 1×1 的辅助窗口)。
|
||
|
||
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
||
guard let windows = CGWindowListCopyWindowInfo(options, kCGNullWindowID)
|
||
as? [[String: Any]] else { exit(1) }
|
||
|
||
struct Found {
|
||
let id: Int
|
||
let width: Int
|
||
let height: Int
|
||
var area: Int { width * height }
|
||
}
|
||
|
||
let found: [Found] = windows.compactMap { window in
|
||
guard let owner = window[kCGWindowOwnerName as String] as? String,
|
||
// CGWindowList 报的是**显示名**(CFBundleName),不是可执行名。
|
||
// 写死 "ClaudeHub" 会一个窗口都找不到 —— 第一版就是这么空手而归的。
|
||
owner.contains("Foreman") || owner == "ClaudeHub",
|
||
let id = window[kCGWindowNumber as String] as? Int,
|
||
let bounds = window[kCGWindowBounds as String] as? [String: Any],
|
||
let width = bounds["Width"] as? Double,
|
||
let height = bounds["Height"] as? Double,
|
||
// 1×1 的辅助窗口和 0 尺寸的占位窗口全部排掉。
|
||
width > 40, height > 40
|
||
else { return nil }
|
||
return Found(id: id, width: Int(width), height: Int(height))
|
||
}
|
||
|
||
for window in found.sorted(by: { $0.area > $1.area }) {
|
||
print("\(window.id) \(window.width) \(window.height)")
|
||
}
|