Files
wuguanganandClaude Opus 5 11786954cf docs: 功能长图 + 全部截图用演示数据重截
## 长图

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>
2026-08-15 00:04:25 +08:00

43 lines
1.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)")
}