Files
claude-code-skills-reference/capture-screen/scripts/get_window_id.swift
2026-04-04 14:23:37 +08:00

243 lines
8.7 KiB
Swift
Executable File

#!/usr/bin/env swift
//
// get_window_id.swift
// Enumerate on-screen windows and print their Window IDs.
//
// Usage:
// swift scripts/get_window_id.swift # List all windows
// swift scripts/get_window_id.swift Excel # Filter by keyword
// swift scripts/get_window_id.swift "Chrome" # Filter by app name
// swift scripts/get_window_id.swift --permission-hint screen # Print Screen Recording triage
// swift scripts/get_window_id.swift --permission-hint microphone # Print Microphone triage
//
// Output format:
// WID=12345 | App=Microsoft Excel | Title=workbook.xlsx
//
// The WID value is compatible with: screencapture -l <WID> output.png
//
import CoreGraphics
import Foundation
enum PermissionKind: String {
case screen
case microphone
}
let invocationPath = (CommandLine.arguments.first ?? "")
let invokerName = URL(fileURLWithPath: invocationPath).lastPathComponent
let runtimeProcessName = ProcessInfo.processInfo.processName
let invokerIsBundle = invocationPath.hasSuffix(".app") || invocationPath.contains(".app/")
let scriptPath: String? = invocationPath.hasSuffix(".swift") ? invocationPath : nil
let helperBinaries: Set<String> = [
"swift",
"swift-frontend",
"python",
"python3",
"node",
"uv",
"npm",
"bun",
"pnpm",
"yarn",
"bash",
"zsh",
"sh",
"osascript",
"Terminal",
"iTerm2",
"iTerm"
]
let invokerCandidates: [String] = {
var candidates = [String]()
var seen = Set<String>()
func append(_ value: String) {
guard !value.isEmpty, !seen.contains(value) else { return }
seen.insert(value)
candidates.append(value)
}
if let scriptPath = scriptPath {
append(scriptPath)
}
if !invokerName.isEmpty {
append(invokerName)
}
if !runtimeProcessName.isEmpty && runtimeProcessName != invokerName {
append(runtimeProcessName)
}
return candidates
}()
let args = Array(CommandLine.arguments.dropFirst())
var permissionHintTarget: PermissionKind?
var keyword = ""
var expectPermissionTarget = false
func printUsage() {
fputs("Usage:\n", stderr)
fputs(" swift scripts/get_window_id.swift [keyword]\n", stderr)
fputs(" swift scripts/get_window_id.swift --permission-hint [screen|microphone]\n", stderr)
fputs("\n", stderr)
fputs("Options:\n", stderr)
fputs(" --permission-hint [screen|microphone] Print permission triage instructions\n", stderr)
fputs(" -h, --help Show this help\n", stderr)
fputs("\n", stderr)
fputs("Examples:\n", stderr)
fputs(" swift scripts/get_window_id.swift Excel\n", stderr)
fputs(" swift scripts/get_window_id.swift --permission-hint screen\n", stderr)
fputs(" swift scripts/get_window_id.swift --permission-hint microphone\n", stderr)
}
for arg in args {
if expectPermissionTarget {
if let kind = PermissionKind(rawValue: arg.lowercased()) {
permissionHintTarget = kind
expectPermissionTarget = false
continue
}
fputs("Unknown permission target: \(arg)\n", stderr)
printUsage()
exit(2)
}
if arg == "-h" || arg == "--help" {
printUsage()
exit(0)
} else if arg == "--permission-hint" {
permissionHintTarget = .screen
expectPermissionTarget = true
} else if arg.hasPrefix("--permission-hint=") {
let target = String(arg.dropFirst("--permission-hint=".count)).lowercased()
guard let kind = PermissionKind(rawValue: target) else {
fputs("Unknown permission target: \(target)\n", stderr)
printUsage()
exit(2)
}
permissionHintTarget = kind
} else if arg == "--permission-hint-screen" {
permissionHintTarget = .screen
} else if arg == "--permission-hint-microphone" || arg == "--permission-hint-mic" {
permissionHintTarget = .microphone
} else if arg.hasPrefix("-") {
fputs("Unknown option: \(arg)\n", stderr)
printUsage()
exit(2)
} else if keyword.isEmpty {
keyword = arg
}
}
if expectPermissionTarget {
fputs("Missing permission hint target after --permission-hint.\n", stderr)
printUsage()
exit(2)
}
if let kind = permissionHintTarget {
switch kind {
case .screen:
fputs("Screen Recording permission required.\n", stderr)
printCommonPermissionHint(
pane: "Privacy_ScreenCapture",
label: "Screen Recording"
)
printPermissionContextHint()
case .microphone:
fputs("Microphone permission required.\n", stderr)
printCommonPermissionHint(
pane: "Privacy_Microphone",
label: "Microphone"
)
printPermissionContextHint()
}
exit(0)
}
func printCommonPermissionHint(pane: String, label: String, missing: Bool = true) {
let openCommand = "x-apple.systempreferences:com.apple.preference.security?\(pane)"
fputs("Troubleshooting:\n", stderr)
fputs(" - Open Settings: `open \"\(openCommand)\"`\n", stderr)
fputs(" - In Privacy & Security → \(label), enable the target application.\n", stderr)
if missing {
fputs(" - If the target app is not in the list:\n", stderr)
fputs(" - Granting happens by real .app bundle, not by helper/terminal scripts.\n", stderr)
fputs(" - For CLI workflows, grant to the host app you launch from (Terminal, iTerm, iTerm2, Swift, etc.) if no dedicated .app exists yet.\n", stderr)
fputs(" - Click `+` and add the actual `.app` from `/Applications`.\n", stderr)
}
fputs(" - If permission status does not refresh, quit/reopen terminal/app and retry.\n", stderr)
if helperBinaries.contains(invokerName) || helperBinaries.contains(runtimeProcessName) {
fputs(" - Current launcher is a helper/runtime process (`\(runtimeProcessName)`) -> OS may show this entry instead of the tool name.\n", stderr)
} else if invokerIsBundle {
fputs(" - The launcher path looks like a bundled app, which is the preferred state for permissions.\n", stderr)
}
if let scriptPath = scriptPath {
fputs(" - Script entry: `\(scriptPath)`\n", stderr)
}
}
func printPermissionContextHint() {
fputs(" - Invoker path: `\(invocationPath)`\n", stderr)
fputs(" - Runtime process: `\(runtimeProcessName)`\n", stderr)
if !invokerCandidates.isEmpty {
fputs(" - Candidate identities in System Settings:\n", stderr)
for identity in invokerCandidates {
fputs(" - \(identity)\n", stderr)
}
}
if invokerIsBundle {
fputs(" This looks like a bundled app path, so the setting should match the app identity.\n", stderr)
} else {
fputs(" If this is not your final app binary, permissions can be inconsistent.\n", stderr)
}
fputs(" - Recommended for production: keep permission requests inside your signed `.app` process.\n", stderr)
if let scriptPath = scriptPath {
fputs(" - Script entry currently used: `\(scriptPath)`.\n", stderr)
}
}
func printScreenRecordingPermissionHint() {
fputs("Screen Recording permission required.\n", stderr)
fputs("Troubleshooting:\n", stderr)
printCommonPermissionHint(pane: "Privacy_ScreenCapture", label: "Screen Recording")
printPermissionContextHint()
}
guard let windowList = CGWindowListCopyWindowInfo(
.optionOnScreenOnly, kCGNullWindowID
) as? [[String: Any]] else {
fputs("ERROR: Failed to enumerate windows.\n", stderr)
fputs("Possible causes:\n", stderr)
fputs(" - No applications with visible windows are running\n", stderr)
fputs(" - Screen Recording permission not granted (System Settings → Privacy & Security → Screen Recording)\n", stderr)
printScreenRecordingPermissionHint()
exit(1)
}
var found = false
for w in windowList {
let owner = w[kCGWindowOwnerName as String] as? String ?? ""
let name = w[kCGWindowName as String] as? String ?? ""
let wid = w[kCGWindowNumber as String] as? Int ?? 0
// Skip windows without a title (menu bar items, system UI, etc.)
if name.isEmpty && !keyword.isEmpty { continue }
if keyword.isEmpty
|| owner.localizedCaseInsensitiveContains(keyword)
|| name.localizedCaseInsensitiveContains(keyword) {
print("WID=\(wid) | App=\(owner) | Title=\(name)")
found = true
}
}
if !found && !keyword.isEmpty {
fputs("No windows found matching '\(keyword)'\n", stderr)
fputs("Troubleshooting:\n", stderr)
fputs(" - Is the application running? (check: pgrep -i '\(keyword)')\n", stderr)
fputs(" - Is the window visible (not minimized to Dock)?\n", stderr)
fputs(" - Try without keyword to see all windows: swift get_window_id.swift\n", stderr)
exit(1)
}