mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-02-28 00:22:41 -08:00
update
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$(uname)" != "Darwin" ]]; then
|
||||
echo "ensure_macos_permissions.sh only supports macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v swift >/dev/null 2>&1; then
|
||||
echo "swift is required to check macOS screen capture permissions" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PERM_SWIFT="$SCRIPT_DIR/macos_permissions.swift"
|
||||
MODULE_CACHE="${TMPDIR:-/tmp}/codex-swift-module-cache"
|
||||
mkdir -p "$MODULE_CACHE"
|
||||
|
||||
screen_capture_status() {
|
||||
local json
|
||||
json="$(swift -module-cache-path "$MODULE_CACHE" "$PERM_SWIFT" "$@")"
|
||||
python3 -c 'import json, sys; data=json.loads(sys.argv[1]); print("1" if data.get("screenCapture") else "0")' "$json"
|
||||
}
|
||||
|
||||
if [[ -n "${CODEX_SANDBOX:-}" ]]; then
|
||||
echo "Screen capture checks are blocked in the sandbox; rerun with escalated permissions." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ "$(screen_capture_status)" == "1" ]]; then
|
||||
echo "Screen Recording permission already granted."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<'MSG'
|
||||
This workflow needs macOS Screen Recording permission to capture screenshots.
|
||||
macOS will show a single system prompt for Screen Recording. Approve it, then
|
||||
return here. If macOS opens System Settings instead of prompting, enable Screen
|
||||
Recording for your terminal and rerun the command.
|
||||
MSG
|
||||
|
||||
# Request permission once after explaining why it is needed.
|
||||
screen_capture_status --request >/dev/null || true
|
||||
|
||||
if [[ "$(screen_capture_status)" != "1" ]]; then
|
||||
cat <<'MSG'
|
||||
Screen Recording is still not granted.
|
||||
Open System Settings > Privacy & Security > Screen Recording and enable it for
|
||||
your terminal (and Codex if needed), then rerun your screenshot command.
|
||||
MSG
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Screen Recording permission granted."
|
||||
22
.agents/skills/screenshot/scripts/macos_display_info.swift
Normal file
22
.agents/skills/screenshot/scripts/macos_display_info.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
struct Response: Encodable {
|
||||
let count: Int
|
||||
let displays: [Int]
|
||||
}
|
||||
|
||||
let count = max(NSScreen.screens.count, 1)
|
||||
let displays = Array(1...count)
|
||||
|
||||
let response = Response(count: count, displays: displays)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
|
||||
if let data = try? encoder.encode(response),
|
||||
let json = String(data: data, encoding: .utf8) {
|
||||
print(json)
|
||||
} else {
|
||||
fputs("{\"count\":\(count)}\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
40
.agents/skills/screenshot/scripts/macos_permissions.swift
Normal file
40
.agents/skills/screenshot/scripts/macos_permissions.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
struct Status: Encodable {
|
||||
let screenCapture: Bool
|
||||
let requested: Bool
|
||||
}
|
||||
|
||||
let shouldRequest = CommandLine.arguments.contains("--request")
|
||||
|
||||
@available(macOS 10.15, *)
|
||||
func screenCaptureGranted(request: Bool) -> Bool {
|
||||
if CGPreflightScreenCaptureAccess() {
|
||||
return true
|
||||
}
|
||||
if request {
|
||||
_ = CGRequestScreenCaptureAccess()
|
||||
return CGPreflightScreenCaptureAccess()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let granted: Bool
|
||||
if #available(macOS 10.15, *) {
|
||||
granted = screenCaptureGranted(request: shouldRequest)
|
||||
} else {
|
||||
granted = true
|
||||
}
|
||||
|
||||
let status = Status(screenCapture: granted, requested: shouldRequest)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
|
||||
if let data = try? encoder.encode(status),
|
||||
let json = String(data: data, encoding: .utf8) {
|
||||
print(json)
|
||||
} else {
|
||||
fputs("{\"requested\":\(shouldRequest),\"screenCapture\":\(granted)}\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
126
.agents/skills/screenshot/scripts/macos_window_info.swift
Normal file
126
.agents/skills/screenshot/scripts/macos_window_info.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
struct Bounds: Encodable {
|
||||
let x: Int
|
||||
let y: Int
|
||||
let width: Int
|
||||
let height: Int
|
||||
}
|
||||
|
||||
struct WindowInfo: Encodable {
|
||||
let id: Int
|
||||
let owner: String
|
||||
let name: String
|
||||
let layer: Int
|
||||
let bounds: Bounds
|
||||
let area: Int
|
||||
}
|
||||
|
||||
struct Response: Encodable {
|
||||
let count: Int
|
||||
let selected: WindowInfo?
|
||||
let windows: [WindowInfo]?
|
||||
}
|
||||
|
||||
func value(for flag: String) -> String? {
|
||||
guard let idx = CommandLine.arguments.firstIndex(of: flag) else {
|
||||
return nil
|
||||
}
|
||||
let next = CommandLine.arguments.index(after: idx)
|
||||
guard next < CommandLine.arguments.endIndex else {
|
||||
return nil
|
||||
}
|
||||
return CommandLine.arguments[next]
|
||||
}
|
||||
|
||||
let frontmostFlag = CommandLine.arguments.contains("--frontmost")
|
||||
let explicitApp = value(for: "--app")
|
||||
let frontmostName = frontmostFlag ? NSWorkspace.shared.frontmostApplication?.localizedName : nil
|
||||
if frontmostFlag && frontmostName == nil {
|
||||
fputs("{\"count\":0}\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
let appFilter = (explicitApp ?? frontmostName)?.lowercased()
|
||||
let nameFilter = value(for: "--window-name")?.lowercased()
|
||||
let includeList = CommandLine.arguments.contains("--list")
|
||||
|
||||
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
||||
guard let raw = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
|
||||
fputs("{\"count\":0}\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
var exactMatches: [WindowInfo] = []
|
||||
var partialMatches: [WindowInfo] = []
|
||||
exactMatches.reserveCapacity(raw.count)
|
||||
partialMatches.reserveCapacity(raw.count)
|
||||
|
||||
for entry in raw {
|
||||
guard let owner = entry[kCGWindowOwnerName as String] as? String else { continue }
|
||||
let ownerLower = owner.lowercased()
|
||||
if let appFilter, !ownerLower.contains(appFilter) { continue }
|
||||
|
||||
let name = (entry[kCGWindowName as String] as? String) ?? ""
|
||||
if let nameFilter, !name.lowercased().contains(nameFilter) { continue }
|
||||
|
||||
guard let number = entry[kCGWindowNumber as String] as? Int else { continue }
|
||||
let layer = (entry[kCGWindowLayer as String] as? Int) ?? 0
|
||||
|
||||
guard let boundsDict = entry[kCGWindowBounds as String] as? [String: Any] else { continue }
|
||||
let x = Int((boundsDict["X"] as? Double) ?? 0)
|
||||
let y = Int((boundsDict["Y"] as? Double) ?? 0)
|
||||
let width = Int((boundsDict["Width"] as? Double) ?? 0)
|
||||
let height = Int((boundsDict["Height"] as? Double) ?? 0)
|
||||
if width <= 0 || height <= 0 { continue }
|
||||
|
||||
let bounds = Bounds(x: x, y: y, width: width, height: height)
|
||||
let area = width * height
|
||||
let info = WindowInfo(id: number, owner: owner, name: name, layer: layer, bounds: bounds, area: area)
|
||||
if let appFilter, ownerLower == appFilter {
|
||||
exactMatches.append(info)
|
||||
} else {
|
||||
partialMatches.append(info)
|
||||
}
|
||||
}
|
||||
|
||||
let windows: [WindowInfo]
|
||||
if appFilter != nil && !exactMatches.isEmpty {
|
||||
windows = exactMatches
|
||||
} else {
|
||||
windows = partialMatches
|
||||
}
|
||||
|
||||
func rank(_ window: WindowInfo) -> (Int, Int) {
|
||||
// Prefer normal-layer windows, then larger area.
|
||||
let layerScore = window.layer == 0 ? 0 : 1
|
||||
return (layerScore, -window.area)
|
||||
}
|
||||
|
||||
let ordered: [WindowInfo]
|
||||
if frontmostFlag {
|
||||
ordered = windows
|
||||
} else {
|
||||
ordered = windows.sorted { rank($0) < rank($1) }
|
||||
}
|
||||
let selected = ordered.first
|
||||
|
||||
let list: [WindowInfo]?
|
||||
if includeList {
|
||||
list = ordered
|
||||
} else {
|
||||
list = nil
|
||||
}
|
||||
|
||||
let response = Response(count: windows.count, selected: selected, windows: list)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys]
|
||||
|
||||
if let data = try? encoder.encode(response),
|
||||
let json = String(data: data, encoding: .utf8) {
|
||||
print(json)
|
||||
} else {
|
||||
fputs("{\"count\":\(windows.count)}\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
163
.agents/skills/screenshot/scripts/take_screenshot.ps1
Normal file
163
.agents/skills/screenshot/scripts/take_screenshot.ps1
Normal file
@@ -0,0 +1,163 @@
|
||||
param(
|
||||
[string]$Path,
|
||||
[ValidateSet("default", "temp")][string]$Mode = "default",
|
||||
[string]$Format = "png",
|
||||
[string]$Region,
|
||||
[switch]$ActiveWindow,
|
||||
[int]$WindowHandle
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-Timestamp {
|
||||
Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
|
||||
}
|
||||
|
||||
function Get-DefaultDirectory {
|
||||
$home = [Environment]::GetFolderPath("UserProfile")
|
||||
$pictures = Join-Path $home "Pictures"
|
||||
$screenshots = Join-Path $pictures "Screenshots"
|
||||
if (Test-Path $screenshots) { return $screenshots }
|
||||
if (Test-Path $pictures) { return $pictures }
|
||||
return $home
|
||||
}
|
||||
|
||||
function New-DefaultFilename {
|
||||
param([string]$Prefix)
|
||||
if (-not $Prefix) { $Prefix = "screenshot" }
|
||||
"$Prefix-$(Get-Timestamp).$Format"
|
||||
}
|
||||
|
||||
function Resolve-OutputPath {
|
||||
if ($Path) {
|
||||
$expanded = [Environment]::ExpandEnvironmentVariables($Path)
|
||||
$homeDir = [Environment]::GetFolderPath("UserProfile")
|
||||
if ($expanded -eq "~") {
|
||||
$expanded = $homeDir
|
||||
} elseif ($expanded.StartsWith("~/") -or $expanded.StartsWith("~\\")) {
|
||||
$expanded = Join-Path $homeDir $expanded.Substring(2)
|
||||
}
|
||||
$full = [System.IO.Path]::GetFullPath($expanded)
|
||||
if ((Test-Path $full) -and (Get-Item $full).PSIsContainer) {
|
||||
$full = Join-Path $full (New-DefaultFilename "")
|
||||
} elseif (($expanded.EndsWith("\") -or $expanded.EndsWith("/")) -and -not (Test-Path $full)) {
|
||||
New-Item -ItemType Directory -Path $full -Force | Out-Null
|
||||
$full = Join-Path $full (New-DefaultFilename "")
|
||||
} elseif ([System.IO.Path]::GetExtension($full) -eq "") {
|
||||
$full = "$full.$Format"
|
||||
}
|
||||
$parent = Split-Path -Parent $full
|
||||
if ($parent) {
|
||||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
||||
}
|
||||
return $full
|
||||
}
|
||||
|
||||
if ($Mode -eq "temp") {
|
||||
$tmp = [System.IO.Path]::GetTempPath()
|
||||
return Join-Path $tmp (New-DefaultFilename "codex-shot")
|
||||
}
|
||||
|
||||
$dest = Get-DefaultDirectory
|
||||
return Join-Path $dest (New-DefaultFilename "")
|
||||
}
|
||||
|
||||
function Parse-Region {
|
||||
if (-not $Region) { return $null }
|
||||
$parts = $Region.Split(",") | ForEach-Object { $_.Trim() }
|
||||
if ($parts.Length -ne 4) {
|
||||
throw "Region must be x,y,w,h"
|
||||
}
|
||||
$values = $parts | ForEach-Object {
|
||||
$out = 0
|
||||
if (-not [int]::TryParse($_, [ref]$out)) {
|
||||
throw "Region values must be integers"
|
||||
}
|
||||
$out
|
||||
}
|
||||
if ($values[2] -le 0 -or $values[3] -le 0) {
|
||||
throw "Region width and height must be positive"
|
||||
}
|
||||
return $values
|
||||
}
|
||||
|
||||
if ($Region -and $ActiveWindow) {
|
||||
throw "Choose either -Region or -ActiveWindow"
|
||||
}
|
||||
if ($Region -and $WindowHandle) {
|
||||
throw "Choose either -Region or -WindowHandle"
|
||||
}
|
||||
if ($ActiveWindow -and $WindowHandle) {
|
||||
throw "Choose either -ActiveWindow or -WindowHandle"
|
||||
}
|
||||
|
||||
$regionValues = Parse-Region
|
||||
$outputPath = Resolve-OutputPath
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$imageFormat = switch ($Format.ToLowerInvariant()) {
|
||||
"png" { [System.Drawing.Imaging.ImageFormat]::Png }
|
||||
"jpg" { [System.Drawing.Imaging.ImageFormat]::Jpeg }
|
||||
"jpeg" { [System.Drawing.Imaging.ImageFormat]::Jpeg }
|
||||
"bmp" { [System.Drawing.Imaging.ImageFormat]::Bmp }
|
||||
default { throw "Unsupported format: $Format" }
|
||||
}
|
||||
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class NativeMethods {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
}
|
||||
"@
|
||||
|
||||
if ($regionValues) {
|
||||
$x = $regionValues[0]
|
||||
$y = $regionValues[1]
|
||||
$w = $regionValues[2]
|
||||
$h = $regionValues[3]
|
||||
$bounds = New-Object System.Drawing.Rectangle($x, $y, $w, $h)
|
||||
} elseif ($ActiveWindow -or $WindowHandle) {
|
||||
$handle = if ($WindowHandle) { [IntPtr]$WindowHandle } else { [NativeMethods]::GetForegroundWindow() }
|
||||
$rect = New-Object NativeMethods+RECT
|
||||
if (-not [NativeMethods]::GetWindowRect($handle, [ref]$rect)) {
|
||||
throw "Failed to get window bounds"
|
||||
}
|
||||
$width = $rect.Right - $rect.Left
|
||||
$height = $rect.Bottom - $rect.Top
|
||||
$bounds = New-Object System.Drawing.Rectangle($rect.Left, $rect.Top, $width, $height)
|
||||
} else {
|
||||
$vs = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
||||
$bounds = New-Object System.Drawing.Rectangle($vs.Left, $vs.Top, $vs.Width, $vs.Height)
|
||||
}
|
||||
|
||||
$bitmap = New-Object System.Drawing.Bitmap($bounds.Width, $bounds.Height)
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
|
||||
try {
|
||||
$source = New-Object System.Drawing.Point($bounds.Left, $bounds.Top)
|
||||
$target = [System.Drawing.Point]::Empty
|
||||
$size = New-Object System.Drawing.Size($bounds.Width, $bounds.Height)
|
||||
$graphics.CopyFromScreen($source, $target, $size)
|
||||
$bitmap.Save($outputPath, $imageFormat)
|
||||
} finally {
|
||||
$graphics.Dispose()
|
||||
$bitmap.Dispose()
|
||||
}
|
||||
|
||||
Write-Output $outputPath
|
||||
585
.agents/skills/screenshot/scripts/take_screenshot.py
Normal file
585
.agents/skills/screenshot/scripts/take_screenshot.py
Normal file
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-platform screenshot helper for Codex skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
MAC_PERM_SCRIPT = SCRIPT_DIR / "macos_permissions.swift"
|
||||
MAC_PERM_HELPER = SCRIPT_DIR / "ensure_macos_permissions.sh"
|
||||
MAC_WINDOW_SCRIPT = SCRIPT_DIR / "macos_window_info.swift"
|
||||
MAC_DISPLAY_SCRIPT = SCRIPT_DIR / "macos_display_info.swift"
|
||||
TEST_MODE_ENV = "CODEX_SCREENSHOT_TEST_MODE"
|
||||
TEST_PLATFORM_ENV = "CODEX_SCREENSHOT_TEST_PLATFORM"
|
||||
TEST_WINDOWS_ENV = "CODEX_SCREENSHOT_TEST_WINDOWS"
|
||||
TEST_DISPLAYS_ENV = "CODEX_SCREENSHOT_TEST_DISPLAYS"
|
||||
TEST_PNG = (
|
||||
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
||||
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x0cIDAT\x08\xd7c"
|
||||
b"\xf8\xff\xff?\x00\x05\xfe\x02\xfeA\xad\x1c\x1c\x00\x00\x00\x00IEND"
|
||||
b"\xaeB`\x82"
|
||||
)
|
||||
|
||||
|
||||
def parse_region(value: str) -> tuple[int, int, int, int]:
|
||||
parts = [p.strip() for p in value.split(",")]
|
||||
if len(parts) != 4:
|
||||
raise argparse.ArgumentTypeError("region must be x,y,w,h")
|
||||
try:
|
||||
x, y, w, h = (int(p) for p in parts)
|
||||
except ValueError as exc:
|
||||
raise argparse.ArgumentTypeError("region values must be integers") from exc
|
||||
if w <= 0 or h <= 0:
|
||||
raise argparse.ArgumentTypeError("region width and height must be positive")
|
||||
return x, y, w, h
|
||||
|
||||
|
||||
def test_mode_enabled() -> bool:
|
||||
value = os.environ.get(TEST_MODE_ENV, "")
|
||||
return value.lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def normalize_platform(value: str) -> str:
|
||||
lowered = value.strip().lower()
|
||||
if lowered in {"darwin", "mac", "macos", "osx"}:
|
||||
return "Darwin"
|
||||
if lowered in {"linux", "ubuntu"}:
|
||||
return "Linux"
|
||||
if lowered in {"windows", "win"}:
|
||||
return "Windows"
|
||||
return value
|
||||
|
||||
|
||||
def test_platform_override() -> str | None:
|
||||
value = os.environ.get(TEST_PLATFORM_ENV)
|
||||
if value:
|
||||
return normalize_platform(value)
|
||||
return None
|
||||
|
||||
|
||||
def parse_int_list(value: str) -> list[int]:
|
||||
results: list[int] = []
|
||||
for part in value.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
try:
|
||||
results.append(int(part))
|
||||
except ValueError:
|
||||
continue
|
||||
return results
|
||||
|
||||
|
||||
def test_window_ids() -> list[int]:
|
||||
value = os.environ.get(TEST_WINDOWS_ENV, "101,102")
|
||||
ids = parse_int_list(value)
|
||||
return ids or [101]
|
||||
|
||||
|
||||
def test_display_ids() -> list[int]:
|
||||
value = os.environ.get(TEST_DISPLAYS_ENV, "1,2")
|
||||
ids = parse_int_list(value)
|
||||
return ids or [1]
|
||||
|
||||
|
||||
def write_test_png(path: Path) -> None:
|
||||
ensure_parent(path)
|
||||
path.write_bytes(TEST_PNG)
|
||||
|
||||
|
||||
def timestamp() -> str:
|
||||
return dt.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
|
||||
def default_filename(fmt: str, prefix: str = "screenshot") -> str:
|
||||
return f"{prefix}-{timestamp()}.{fmt}"
|
||||
|
||||
|
||||
def mac_default_dir() -> Path:
|
||||
desktop = Path.home() / "Desktop"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["defaults", "read", "com.apple.screencapture", "location"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
location = proc.stdout.strip()
|
||||
if location:
|
||||
return Path(location).expanduser()
|
||||
except OSError:
|
||||
pass
|
||||
return desktop
|
||||
|
||||
|
||||
def default_dir(system: str) -> Path:
|
||||
home = Path.home()
|
||||
if system == "Darwin":
|
||||
return mac_default_dir()
|
||||
if system == "Windows":
|
||||
pictures = home / "Pictures"
|
||||
screenshots = pictures / "Screenshots"
|
||||
if screenshots.exists():
|
||||
return screenshots
|
||||
if pictures.exists():
|
||||
return pictures
|
||||
return home
|
||||
pictures = home / "Pictures"
|
||||
screenshots = pictures / "Screenshots"
|
||||
if screenshots.exists():
|
||||
return screenshots
|
||||
if pictures.exists():
|
||||
return pictures
|
||||
return home
|
||||
|
||||
|
||||
def ensure_parent(path: Path) -> None:
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except OSError:
|
||||
# Fall back to letting the capture command report a clearer error.
|
||||
pass
|
||||
|
||||
|
||||
def resolve_output_path(
|
||||
requested_path: str | None, mode: str, fmt: str, system: str
|
||||
) -> Path:
|
||||
if requested_path:
|
||||
path = Path(requested_path).expanduser()
|
||||
if path.exists() and path.is_dir():
|
||||
path = path / default_filename(fmt)
|
||||
elif requested_path.endswith(("/", "\\")) and not path.exists():
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
path = path / default_filename(fmt)
|
||||
elif path.suffix == "":
|
||||
path = path.with_suffix(f".{fmt}")
|
||||
ensure_parent(path)
|
||||
return path
|
||||
|
||||
if mode == "temp":
|
||||
tmp_dir = Path(tempfile.gettempdir())
|
||||
tmp_path = tmp_dir / default_filename(fmt, prefix="codex-shot")
|
||||
ensure_parent(tmp_path)
|
||||
return tmp_path
|
||||
|
||||
dest_dir = default_dir(system)
|
||||
dest_path = dest_dir / default_filename(fmt)
|
||||
ensure_parent(dest_path)
|
||||
return dest_path
|
||||
|
||||
|
||||
def multi_output_paths(base: Path, suffixes: list[str]) -> list[Path]:
|
||||
if len(suffixes) <= 1:
|
||||
return [base]
|
||||
paths: list[Path] = []
|
||||
for suffix in suffixes:
|
||||
candidate = base.with_name(f"{base.stem}-{suffix}{base.suffix}")
|
||||
ensure_parent(candidate)
|
||||
paths.append(candidate)
|
||||
return paths
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> None:
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except FileNotFoundError as exc:
|
||||
raise SystemExit(f"required command not found: {cmd[0]}") from exc
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise SystemExit(f"command failed ({exc.returncode}): {' '.join(cmd)}") from exc
|
||||
|
||||
|
||||
def swift_json(script: Path, extra_args: list[str] | None = None) -> dict:
|
||||
module_cache = Path(tempfile.gettempdir()) / "codex-swift-module-cache"
|
||||
module_cache.mkdir(parents=True, exist_ok=True)
|
||||
cmd = ["swift", "-module-cache-path", str(module_cache), str(script)]
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
try:
|
||||
proc = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
except FileNotFoundError as exc:
|
||||
raise SystemExit("swift not found; install Xcode command line tools") from exc
|
||||
except subprocess.CalledProcessError as exc:
|
||||
stderr = (exc.stderr or "").strip()
|
||||
if "ModuleCache" in stderr and "Operation not permitted" in stderr:
|
||||
raise SystemExit(
|
||||
"swift needs module-cache access; rerun with escalated permissions"
|
||||
) from exc
|
||||
msg = stderr or (exc.stdout or "").strip() or "swift helper failed"
|
||||
raise SystemExit(msg) from exc
|
||||
try:
|
||||
return json.loads(proc.stdout)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit(f"swift helper returned invalid JSON: {proc.stdout.strip()}") from exc
|
||||
|
||||
|
||||
def macos_screen_capture_granted(request: bool = False) -> bool:
|
||||
args = ["--request"] if request else []
|
||||
payload = swift_json(MAC_PERM_SCRIPT, args)
|
||||
return bool(payload.get("screenCapture"))
|
||||
|
||||
|
||||
def ensure_macos_permissions() -> None:
|
||||
if os.environ.get("CODEX_SANDBOX"):
|
||||
raise SystemExit(
|
||||
"screen capture checks are blocked in the sandbox; rerun with escalated permissions"
|
||||
)
|
||||
if macos_screen_capture_granted():
|
||||
return
|
||||
subprocess.run(["bash", str(MAC_PERM_HELPER)], check=False)
|
||||
if not macos_screen_capture_granted():
|
||||
raise SystemExit(
|
||||
"Screen Recording permission is required; enable it in System Settings and retry"
|
||||
)
|
||||
|
||||
|
||||
def activate_app(app: str) -> None:
|
||||
safe_app = app.replace('"', '\\"')
|
||||
script = f'tell application "{safe_app}" to activate'
|
||||
subprocess.run(["osascript", "-e", script], check=False, capture_output=True, text=True)
|
||||
|
||||
|
||||
def macos_window_payload(args: argparse.Namespace, frontmost: bool, include_list: bool) -> dict:
|
||||
flags: list[str] = []
|
||||
if frontmost:
|
||||
flags.append("--frontmost")
|
||||
if args.app:
|
||||
flags.extend(["--app", args.app])
|
||||
if args.window_name:
|
||||
flags.extend(["--window-name", args.window_name])
|
||||
if include_list:
|
||||
flags.append("--list")
|
||||
return swift_json(MAC_WINDOW_SCRIPT, flags)
|
||||
|
||||
|
||||
def macos_display_indexes() -> list[int]:
|
||||
payload = swift_json(MAC_DISPLAY_SCRIPT)
|
||||
displays = payload.get("displays") or []
|
||||
indexes: list[int] = []
|
||||
for item in displays:
|
||||
try:
|
||||
value = int(item)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if value > 0:
|
||||
indexes.append(value)
|
||||
return indexes or [1]
|
||||
|
||||
|
||||
def macos_window_ids(args: argparse.Namespace, capture_all: bool) -> list[int]:
|
||||
payload = macos_window_payload(
|
||||
args,
|
||||
frontmost=args.active_window,
|
||||
include_list=capture_all,
|
||||
)
|
||||
if capture_all:
|
||||
windows = payload.get("windows") or []
|
||||
ids: list[int] = []
|
||||
for item in windows:
|
||||
win_id = item.get("id")
|
||||
if win_id is None:
|
||||
continue
|
||||
try:
|
||||
ids.append(int(win_id))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if ids:
|
||||
return ids
|
||||
selected = payload.get("selected") or {}
|
||||
win_id = selected.get("id")
|
||||
if win_id is not None:
|
||||
try:
|
||||
return [int(win_id)]
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
raise SystemExit("no matching macOS window found; try --list-windows to inspect ids")
|
||||
|
||||
|
||||
def list_macos_windows(args: argparse.Namespace) -> None:
|
||||
payload = macos_window_payload(args, frontmost=args.active_window, include_list=True)
|
||||
windows = payload.get("windows") or []
|
||||
if not windows:
|
||||
print("no matching windows found")
|
||||
return
|
||||
for item in windows:
|
||||
bounds = item.get("bounds") or {}
|
||||
name = item.get("name") or ""
|
||||
width = bounds.get("width", 0)
|
||||
height = bounds.get("height", 0)
|
||||
x = bounds.get("x", 0)
|
||||
y = bounds.get("y", 0)
|
||||
print(f"{item.get('id')}\t{item.get('owner')}\t{name}\t{width}x{height}+{x}+{y}")
|
||||
|
||||
|
||||
def list_test_macos_windows(args: argparse.Namespace) -> None:
|
||||
owner = args.app or "TestApp"
|
||||
name = args.window_name or ""
|
||||
ids = test_window_ids()
|
||||
if args.active_window and ids:
|
||||
ids = [ids[0]]
|
||||
for idx, win_id in enumerate(ids, start=1):
|
||||
window_name = name or f"Window {idx}"
|
||||
print(f"{win_id}\t{owner}\t{window_name}\t800x600+0+0")
|
||||
|
||||
|
||||
def resolve_macos_windows(args: argparse.Namespace) -> list[int]:
|
||||
if args.app:
|
||||
activate_app(args.app)
|
||||
capture_all = not args.active_window
|
||||
return macos_window_ids(args, capture_all=capture_all)
|
||||
|
||||
|
||||
def resolve_test_macos_windows(args: argparse.Namespace) -> list[int]:
|
||||
ids = test_window_ids()
|
||||
if args.active_window and ids:
|
||||
return [ids[0]]
|
||||
return ids
|
||||
|
||||
|
||||
def capture_macos(
|
||||
args: argparse.Namespace,
|
||||
output: Path,
|
||||
*,
|
||||
window_id: int | None = None,
|
||||
display: int | None = None,
|
||||
) -> None:
|
||||
cmd = ["screencapture", "-x", f"-t{args.format}"]
|
||||
if args.interactive:
|
||||
cmd.append("-i")
|
||||
if display is not None:
|
||||
cmd.append(f"-D{display}")
|
||||
effective_window_id = window_id if window_id is not None else args.window_id
|
||||
if effective_window_id is not None:
|
||||
cmd.append(f"-l{effective_window_id}")
|
||||
elif args.region is not None:
|
||||
x, y, w, h = args.region
|
||||
cmd.append(f"-R{x},{y},{w},{h}")
|
||||
cmd.append(str(output))
|
||||
run(cmd)
|
||||
|
||||
|
||||
def capture_linux(args: argparse.Namespace, output: Path) -> None:
|
||||
scrot = shutil.which("scrot")
|
||||
gnome = shutil.which("gnome-screenshot")
|
||||
imagemagick = shutil.which("import")
|
||||
xdotool = shutil.which("xdotool")
|
||||
|
||||
if args.region is not None:
|
||||
x, y, w, h = args.region
|
||||
if scrot:
|
||||
run(["scrot", "-a", f"{x},{y},{w},{h}", str(output)])
|
||||
return
|
||||
if imagemagick:
|
||||
geometry = f"{w}x{h}+{x}+{y}"
|
||||
run(["import", "-window", "root", "-crop", geometry, str(output)])
|
||||
return
|
||||
raise SystemExit("region capture requires scrot or ImageMagick (import)")
|
||||
|
||||
if args.window_id is not None:
|
||||
if imagemagick:
|
||||
run(["import", "-window", str(args.window_id), str(output)])
|
||||
return
|
||||
raise SystemExit("window-id capture requires ImageMagick (import)")
|
||||
|
||||
if args.active_window:
|
||||
if scrot:
|
||||
run(["scrot", "-u", str(output)])
|
||||
return
|
||||
if gnome:
|
||||
run(["gnome-screenshot", "-w", "-f", str(output)])
|
||||
return
|
||||
if imagemagick and xdotool:
|
||||
win_id = (
|
||||
subprocess.check_output(["xdotool", "getactivewindow"], text=True)
|
||||
.strip()
|
||||
)
|
||||
run(["import", "-window", win_id, str(output)])
|
||||
return
|
||||
raise SystemExit("active-window capture requires scrot, gnome-screenshot, or import+xdotool")
|
||||
|
||||
if scrot:
|
||||
run(["scrot", str(output)])
|
||||
return
|
||||
if gnome:
|
||||
run(["gnome-screenshot", "-f", str(output)])
|
||||
return
|
||||
if imagemagick:
|
||||
run(["import", "-window", "root", str(output)])
|
||||
return
|
||||
raise SystemExit("no supported screenshot tool found (scrot, gnome-screenshot, or import)")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
help="output file path or directory; overrides --mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("default", "temp"),
|
||||
default="default",
|
||||
help="default saves to the OS screenshot location; temp saves to the temp dir",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
default="png",
|
||||
help="image format/extension (default: png)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--app",
|
||||
help="macOS only: capture all matching on-screen windows for this app name",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-name",
|
||||
help="macOS only: substring match for a window title (optionally scoped by --app)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-windows",
|
||||
action="store_true",
|
||||
help="macOS only: list matching window ids instead of capturing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--region",
|
||||
type=parse_region,
|
||||
help="capture region as x,y,w,h (pixel coordinates)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-id",
|
||||
type=int,
|
||||
help="capture a specific window id when supported",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--active-window",
|
||||
action="store_true",
|
||||
help="capture the focused/active window only when supported",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--interactive",
|
||||
action="store_true",
|
||||
help="use interactive selection where the OS tool supports it",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.region and args.window_id is not None:
|
||||
raise SystemExit("choose either --region or --window-id, not both")
|
||||
if args.region and args.active_window:
|
||||
raise SystemExit("choose either --region or --active-window, not both")
|
||||
if args.window_id is not None and args.active_window:
|
||||
raise SystemExit("choose either --window-id or --active-window, not both")
|
||||
if args.app and args.window_id is not None:
|
||||
raise SystemExit("choose either --app or --window-id, not both")
|
||||
if args.region and args.app:
|
||||
raise SystemExit("choose either --region or --app, not both")
|
||||
if args.region and args.window_name:
|
||||
raise SystemExit("choose either --region or --window-name, not both")
|
||||
if args.interactive and args.app:
|
||||
raise SystemExit("choose either --interactive or --app, not both")
|
||||
if args.interactive and args.window_name:
|
||||
raise SystemExit("choose either --interactive or --window-name, not both")
|
||||
if args.interactive and args.window_id is not None:
|
||||
raise SystemExit("choose either --interactive or --window-id, not both")
|
||||
if args.interactive and args.active_window:
|
||||
raise SystemExit("choose either --interactive or --active-window, not both")
|
||||
if args.list_windows and (args.region or args.window_id is not None or args.interactive):
|
||||
raise SystemExit("--list-windows only supports --app, --window-name, and --active-window")
|
||||
|
||||
test_mode = test_mode_enabled()
|
||||
system = platform.system()
|
||||
if test_mode:
|
||||
override = test_platform_override()
|
||||
if override:
|
||||
system = override
|
||||
window_ids: list[int] = []
|
||||
display_ids: list[int] = []
|
||||
|
||||
if system != "Darwin" and (args.app or args.window_name or args.list_windows):
|
||||
raise SystemExit("--app/--window-name/--list-windows are supported on macOS only")
|
||||
|
||||
if system == "Darwin":
|
||||
if test_mode:
|
||||
if args.list_windows:
|
||||
list_test_macos_windows(args)
|
||||
return
|
||||
if args.window_id is not None:
|
||||
window_ids = [args.window_id]
|
||||
elif args.app or args.window_name or args.active_window:
|
||||
window_ids = resolve_test_macos_windows(args)
|
||||
elif args.region is None and not args.interactive:
|
||||
display_ids = test_display_ids()
|
||||
else:
|
||||
ensure_macos_permissions()
|
||||
if args.list_windows:
|
||||
list_macos_windows(args)
|
||||
return
|
||||
if args.window_id is not None:
|
||||
window_ids = [args.window_id]
|
||||
elif args.app or args.window_name or args.active_window:
|
||||
window_ids = resolve_macos_windows(args)
|
||||
elif args.region is None and not args.interactive:
|
||||
display_ids = macos_display_indexes()
|
||||
|
||||
output = resolve_output_path(args.path, args.mode, args.format, system)
|
||||
|
||||
if test_mode:
|
||||
if system == "Darwin":
|
||||
if window_ids:
|
||||
suffixes = [f"w{wid}" for wid in window_ids]
|
||||
paths = multi_output_paths(output, suffixes)
|
||||
for path in paths:
|
||||
write_test_png(path)
|
||||
for path in paths:
|
||||
print(path)
|
||||
return
|
||||
if len(display_ids) > 1:
|
||||
suffixes = [f"d{did}" for did in display_ids]
|
||||
paths = multi_output_paths(output, suffixes)
|
||||
for path in paths:
|
||||
write_test_png(path)
|
||||
for path in paths:
|
||||
print(path)
|
||||
return
|
||||
write_test_png(output)
|
||||
print(output)
|
||||
return
|
||||
|
||||
if system == "Darwin":
|
||||
if window_ids:
|
||||
suffixes = [f"w{wid}" for wid in window_ids]
|
||||
paths = multi_output_paths(output, suffixes)
|
||||
for wid, path in zip(window_ids, paths):
|
||||
capture_macos(args, path, window_id=wid)
|
||||
for path in paths:
|
||||
print(path)
|
||||
return
|
||||
if len(display_ids) > 1:
|
||||
suffixes = [f"d{did}" for did in display_ids]
|
||||
paths = multi_output_paths(output, suffixes)
|
||||
for did, path in zip(display_ids, paths):
|
||||
capture_macos(args, path, display=did)
|
||||
for path in paths:
|
||||
print(path)
|
||||
return
|
||||
capture_macos(args, output)
|
||||
elif system == "Linux":
|
||||
capture_linux(args, output)
|
||||
elif system == "Windows":
|
||||
raise SystemExit(
|
||||
"Windows support lives in scripts/take_screenshot.ps1; run it with PowerShell"
|
||||
)
|
||||
else:
|
||||
raise SystemExit(f"unsupported platform: {system}")
|
||||
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user