🧹 Simplified: Removed redundant basic version, kept enhanced as main

The enhanced version has all features (multi-window, app discovery, etc.)
No need for two separate scripts - one powerful tool is better.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-05-22 18:42:30 +02:00
parent e234e192b3
commit 4c1c255b7c
3 changed files with 359 additions and 728 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,17 +1,18 @@
#!/usr/bin/osascript
--------------------------------------------------------------------------------
-- peekaboo.scpt - v1.0.0 "Peekaboo! 👀 → 📸 → 💾"
-- Unattended screenshot capture with app targeting and location specification
-- peekaboo_enhanced.scpt - v1.0.0 "Peekaboo Pro! 👀 → 📸 → 💾"
-- Enhanced screenshot capture with multi-window support and app discovery
-- Peekaboo—screenshot got you! Now you see it, now it's saved.
--------------------------------------------------------------------------------
--#region Configuration Properties
property scriptInfoPrefix : "Peekaboo 👀: "
property defaultScreenshotFormat : "png"
property captureDelay : 1.0 -- Delay after bringing app to front before capture
property windowActivationDelay : 0.5 -- Delay for window activation
property captureDelay : 1.0
property windowActivationDelay : 0.5
property enhancedErrorReporting : true
property verboseLogging : false
property maxWindowTitleLength : 50
--#endregion Configuration Properties
--#region Helper Functions
@ -43,6 +44,32 @@ on ensureDirectoryExists(dirPath)
end try
end ensureDirectoryExists
on sanitizeFilename(filename)
-- Replace problematic characters for filenames
set filename to my replaceText(filename, "/", "_")
set filename to my replaceText(filename, ":", "_")
set filename to my replaceText(filename, "*", "_")
set filename to my replaceText(filename, "?", "_")
set filename to my replaceText(filename, "\"", "_")
set filename to my replaceText(filename, "<", "_")
set filename to my replaceText(filename, ">", "_")
set filename to my replaceText(filename, "|", "_")
if (length of filename) > maxWindowTitleLength then
set filename to text 1 thru maxWindowTitleLength of filename
end if
return filename
end sanitizeFilename
on replaceText(theText, searchStr, replaceStr)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to searchStr
set textItems to text items of theText
set AppleScript's text item delimiters to replaceStr
set newText to textItems as text
set AppleScript's text item delimiters to oldDelims
return newText
end replaceText
on formatErrorMessage(errorType, errorMsg, context)
if enhancedErrorReporting then
set formattedMsg to scriptInfoPrefix & errorType & ": " & errorMsg
@ -82,6 +109,74 @@ on trimWhitespace(theText)
end trimWhitespace
--#endregion Helper Functions
--#region App Discovery Functions
on listRunningApps()
set appList to {}
try
tell application "System Events"
repeat with proc in (every application process whose background only is false)
try
set appName to name of proc
set bundleID to bundle identifier of proc
set windowCount to count of windows of proc
set windowTitles to {}
if windowCount > 0 then
repeat with win in windows of proc
try
set winTitle to title of win
if winTitle is not "" then
set end of windowTitles to winTitle
end if
on error
-- Skip windows without accessible titles
end try
end repeat
end if
set end of appList to {appName:appName, bundleID:bundleID, windowCount:windowCount, windowTitles:windowTitles}
on error
-- Skip apps we can't access
end try
end repeat
end tell
on error errMsg
return my formatErrorMessage("Discovery Error", "Failed to enumerate running applications: " & errMsg, "app enumeration")
end try
return appList
end listRunningApps
on formatAppList(appList)
if appList starts with scriptInfoPrefix then
return appList -- Error message
end if
set output to scriptInfoPrefix & "Running Applications:" & linefeed & linefeed
repeat with appInfo in appList
set appName to appName of appInfo
set bundleID to bundleID of appInfo
set windowCount to windowCount of appInfo
set windowTitles to windowTitles of appInfo
set output to output & "• " & appName & " (" & bundleID & ")" & linefeed
set output to output & " Windows: " & windowCount
if windowCount > 0 and (count of windowTitles) > 0 then
set output to output & linefeed
repeat with winTitle in windowTitles
set output to output & " - \"" & winTitle & "\"" & linefeed
end repeat
else
set output to output & linefeed
end if
set output to output & linefeed
end repeat
return output
end formatAppList
--#endregion App Discovery Functions
--#region App Resolution Functions
on resolveAppIdentifier(appIdentifier)
my logVerbose("Resolving app identifier: " & appIdentifier)
@ -160,6 +255,64 @@ on resolveAppIdentifier(appIdentifier)
return missing value
end resolveAppIdentifier
on getAppWindows(appName)
set windowInfo to {}
set windowCount to 0
set accessibleWindows to 0
try
tell application "System Events"
tell process appName
set windowCount to count of windows
repeat with i from 1 to windowCount
try
set win to window i
set winTitle to title of win
if winTitle is "" then set winTitle to "Untitled Window " & i
set end of windowInfo to {winTitle, i}
set accessibleWindows to accessibleWindows + 1
on error
set end of windowInfo to {("Window " & i), i}
end try
end repeat
end tell
end tell
on error errMsg
my logVerbose("Failed to get windows for " & appName & ": " & errMsg)
return {windowInfo:windowInfo, totalWindows:0, accessibleWindows:0, errorMsg:errMsg}
end try
return {windowInfo:windowInfo, totalWindows:windowCount, accessibleWindows:accessibleWindows, errorMsg:""}
end getAppWindows
on getAppWindowStatus(appName)
set windowStatus to my getAppWindows(appName)
set windowInfo to windowInfo of windowStatus
set totalWindows to totalWindows of windowStatus
set accessibleWindows to accessibleWindows of windowStatus
set windowError to errorMsg of windowStatus
if windowError is not "" then
return my formatErrorMessage("Window Access Error", "Cannot access windows for app '" & appName & "': " & windowError & ". The app may not be running or may not have accessibility permissions.", "window enumeration")
end if
if totalWindows = 0 then
return my formatErrorMessage("No Windows Error", "App '" & appName & "' is running but has no windows open. Peekaboo needs at least one window to capture. Please open a window in " & appName & " and try again.", "zero windows")
end if
if accessibleWindows = 0 and totalWindows > 0 then
return my formatErrorMessage("Window Access Error", "App '" & appName & "' has " & totalWindows & " window(s) but none are accessible. This may require accessibility permissions in System Preferences > Security & Privacy > Accessibility.", "accessibility required")
end if
-- Success case
set statusMsg to "App '" & appName & "' has " & totalWindows & " window(s)"
if accessibleWindows < totalWindows then
set statusMsg to statusMsg & " (" & accessibleWindows & " accessible)"
end if
return {status:"success", message:statusMsg, windowInfo:windowInfo, totalWindows:totalWindows, accessibleWindows:accessibleWindows}
end getAppWindowStatus
on bringAppToFront(appInfo)
set appName to appName of appInfo
set isRunning to isRunning of appInfo
@ -191,8 +344,8 @@ end bringAppToFront
--#endregion App Resolution Functions
--#region Screenshot Functions
on captureScreenshot(outputPath)
my logVerbose("Capturing screenshot to: " & outputPath)
on captureScreenshot(outputPath, captureMode, appName)
my logVerbose("Capturing screenshot to: " & outputPath & " (mode: " & captureMode & ")")
-- Ensure output directory exists
set outputDir to do shell script "dirname " & quoted form of outputPath
@ -210,18 +363,25 @@ on captureScreenshot(outputPath)
set outputPath to outputPath & "." & fileExt
end if
-- Capture screenshot using screencapture
-- Build screencapture command based on mode
set screencaptureCmd to "screencapture -x"
if captureMode is "window" then
-- Use frontmost window without interaction
set screencaptureCmd to screencaptureCmd & " -o -W"
end if
-- Remove interactive mode - not suitable for unattended operation
-- Add format flag if not PNG (default)
if fileExt is not "png" then
set screencaptureCmd to screencaptureCmd & " -t " & fileExt
end if
-- Add output path
set screencaptureCmd to screencaptureCmd & " " & quoted form of outputPath
-- Capture screenshot
try
set screencaptureCmd to "screencapture -x"
-- Add format flag if not PNG (default)
if fileExt is not "png" then
set screencaptureCmd to screencaptureCmd & " -t " & fileExt
end if
-- Add output path
set screencaptureCmd to screencaptureCmd & " " & quoted form of outputPath
my logVerbose("Running: " & screencaptureCmd)
do shell script screencaptureCmd
@ -246,15 +406,97 @@ on captureScreenshot(outputPath)
end if
end try
end captureScreenshot
on captureMultipleWindows(appName, baseOutputPath)
-- Get detailed window status first
set windowStatus to my getAppWindowStatus(appName)
-- Check if it's an error
if (windowStatus starts with scriptInfoPrefix) then
return windowStatus -- Return the descriptive error
end if
-- Extract window info from successful status
set windowInfo to windowInfo of windowStatus
set totalWindows to totalWindows of windowStatus
set accessibleWindows to accessibleWindows of windowStatus
set capturedFiles to {}
my logVerbose("Multi-window capture: " & totalWindows & " total, " & accessibleWindows & " accessible")
if (count of windowInfo) = 0 then
return my formatErrorMessage("Multi-Window Error", "App '" & appName & "' has no accessible windows for multi-window capture. Try using single screenshot mode instead, or ensure the app has open windows.", "no accessible windows")
end if
-- Get base path components
set outputDir to do shell script "dirname " & quoted form of baseOutputPath
set baseName to do shell script "basename " & quoted form of baseOutputPath
set fileExt to my getFileExtension(baseName)
if fileExt is not "" then
set baseNameNoExt to text 1 thru -((length of fileExt) + 2) of baseName
else
set baseNameNoExt to baseName
set fileExt to defaultScreenshotFormat
end if
my logVerbose("Capturing " & (count of windowInfo) & " windows for " & appName)
repeat with winInfo in windowInfo
set winTitle to item 1 of winInfo
set winIndex to item 2 of winInfo
set sanitizedTitle to my sanitizeFilename(winTitle)
set windowFileName to baseNameNoExt & "_window_" & winIndex & "_" & sanitizedTitle & "." & fileExt
set windowOutputPath to outputDir & "/" & windowFileName
-- Focus the specific window first
try
tell application "System Events"
tell process appName
set frontmost to true
tell window winIndex
perform action "AXRaise"
end tell
end tell
end tell
delay 0.3
on error
my logVerbose("Could not focus window " & winIndex & ", continuing anyway")
end try
-- Capture the frontmost window
set captureResult to my captureScreenshot(windowOutputPath, "window", appName)
if captureResult starts with scriptInfoPrefix then
-- Error occurred, but continue with other windows
my logVerbose("Failed to capture window " & winIndex & ": " & captureResult)
else
set end of capturedFiles to {captureResult, winTitle, winIndex}
end if
end repeat
return capturedFiles
end captureMultipleWindows
--#endregion Screenshot Functions
--#region Main Script Logic (on run)
on run argv
set appSpecificErrorOccurred to false
try
my logVerbose("Starting Screenshotter v1.0.0")
my logVerbose("Starting Screenshotter Enhanced v2.0.0")
set argCount to count argv
-- Handle special commands
if argCount = 1 then
set command to item 1 of argv
if command is "list" or command is "--list" or command is "-l" then
set appList to my listRunningApps()
return my formatAppList(appList)
else if command is "help" or command is "--help" or command is "-h" then
return my usageText()
end if
end if
if argCount < 1 then return my usageText()
set appIdentifier to item 1 of argv
@ -266,6 +508,23 @@ on run argv
set timestamp to do shell script "date +%Y%m%d_%H%M%S"
set outputPath to "/tmp/peekaboo_" & timestamp & ".png"
end if
set captureMode to "screen" -- default
set multiWindow to false
-- Parse additional options
if argCount > 2 then
repeat with i from 3 to argCount
set arg to item i of argv
if arg is "--window" or arg is "-w" then
set captureMode to "window"
-- Remove interactive mode option
else if arg is "--multi" or arg is "-m" then
set multiWindow to true
else if arg is "--verbose" or arg is "-v" then
set verboseLogging to true
end if
end repeat
end if
-- Validate arguments
if appIdentifier is "" then
@ -286,13 +545,13 @@ on run argv
set errorDetails to errorDetails & " This appears to be a bundle ID. Common issues:" & linefeed
set errorDetails to errorDetails & "• Bundle ID may be incorrect (try 'com.apple.' prefix for system apps)" & linefeed
set errorDetails to errorDetails & "• App may not be installed" & linefeed
set errorDetails to errorDetails & "• Try using the app name instead (e.g., 'Safari' instead of bundle ID)"
set errorDetails to errorDetails & "• Use 'osascript peekaboo_enhanced.scpt list' to see available apps"
else
set errorDetails to errorDetails & " This appears to be an app name. Common issues:" & linefeed
set errorDetails to errorDetails & "• App name may be incorrect (case-sensitive)" & linefeed
set errorDetails to errorDetails & "• App may not be installed or running" & linefeed
set errorDetails to errorDetails & "• Try the full app name (e.g., 'Activity Monitor' not 'Activity')" & linefeed
set errorDetails to errorDetails & "• Some apps need to be launched first before capturing"
set errorDetails to errorDetails & "• Use 'osascript peekaboo_enhanced.scpt list' to see running apps"
end if
return my formatErrorMessage("App Resolution Error", errorDetails, "app resolution")
@ -306,14 +565,53 @@ on run argv
set frontError to my bringAppToFront(appInfo)
if frontError is not "" then return frontError
-- Capture screenshot
set screenshotResult to my captureScreenshot(outputPath)
if screenshotResult starts with scriptInfoPrefix then
-- Error occurred
return screenshotResult
-- Pre-capture window validation for better error messages
if multiWindow or captureMode is "window" then
set windowStatus to my getAppWindowStatus(resolvedAppName)
if (windowStatus starts with scriptInfoPrefix) then
-- Add context about what the user was trying to do
if multiWindow then
set contextError to "Multi-window capture failed: " & windowStatus
set contextError to contextError & linefeed & "💡 Suggestion: Try basic screenshot mode without --multi flag"
else
set contextError to "Window capture failed: " & windowStatus
set contextError to contextError & linefeed & "💡 Suggestion: Try full-screen capture mode without --window flag"
end if
return contextError
end if
-- Log successful window detection
set statusMsg to message of windowStatus
my logVerbose("Window validation passed: " & statusMsg)
end if
-- Handle multi-window capture
if multiWindow then
set capturedFiles to my captureMultipleWindows(resolvedAppName, outputPath)
if capturedFiles starts with scriptInfoPrefix then
return capturedFiles -- Error message
else
set windowCount to count of capturedFiles
set resultMsg to scriptInfoPrefix & "Multi-window capture successful! Captured " & windowCount & " window(s) for " & resolvedAppName & ":" & linefeed
repeat with fileInfo in capturedFiles
set filePath to item 1 of fileInfo
set winTitle to item 2 of fileInfo
set resultMsg to resultMsg & " 📸 " & filePath & " → \"" & winTitle & "\"" & linefeed
end repeat
set resultMsg to resultMsg & linefeed & "💡 All windows captured with descriptive filenames. Each file shows a different window of " & resolvedAppName & "."
return resultMsg
end if
else
-- Success
return scriptInfoPrefix & "Screenshot captured successfully! 📸" & linefeed & "• File: " & screenshotResult & linefeed & "• App: " & resolvedAppName & linefeed & "• Mode: full screen" & linefeed & "💡 The full screen with " & resolvedAppName & " active has been saved."
-- Single capture
set screenshotResult to my captureScreenshot(outputPath, captureMode, resolvedAppName)
if screenshotResult starts with scriptInfoPrefix then
return screenshotResult -- Error message
else
set modeDescription to "full screen"
if captureMode is "window" then set modeDescription to "front window only"
return scriptInfoPrefix & "Screenshot captured successfully! 📸" & linefeed & "• File: " & screenshotResult & linefeed & "• App: " & resolvedAppName & linefeed & "• Mode: " & modeDescription & linefeed & "💡 The " & modeDescription & " of " & resolvedAppName & " has been saved."
end if
end if
on error generalErrorMsg number generalErrorNum
@ -326,43 +624,55 @@ end run
--#region Usage Function
on usageText()
set LF to linefeed
set scriptName to "peekaboo.scpt"
set scriptName to "peekaboo_enhanced.scpt"
set outText to scriptName & " - v1.0.0 \"Peekaboo! 👀 → 📸 → 💾\" AppleScript Screenshot Utility" & LF & LF
set outText to scriptName & " - v1.0.0 \"Peekaboo Pro! 👀 → 📸 → 💾\" Enhanced AppleScript Screenshot Utility" & LF & LF
set outText to outText & "Peekaboo—screenshot got you! Now you see it, now it's saved." & LF
set outText to outText & "Takes unattended screenshots of applications by name or bundle ID." & LF & LF
set outText to outText & "Takes unattended screenshots with multi-window support and app discovery." & LF & LF
set outText to outText & "Usage:" & LF
set outText to outText & " osascript " & scriptName & " \"<app_name_or_bundle_id>\" [\"<output_path>\"]" & LF & LF
set outText to outText & " osascript " & scriptName & " \"<app_name_or_bundle_id>\" [\"<output_path>\"] [options]" & LF
set outText to outText & " osascript " & scriptName & " list" & LF
set outText to outText & " osascript " & scriptName & " help" & LF & LF
set outText to outText & "Parameters:" & LF
set outText to outText & " app_name_or_bundle_id: Application name (e.g., 'Safari') or bundle ID (e.g., 'com.apple.Safari')" & LF
set outText to outText & " output_path: Optional absolute path for screenshot file" & LF
set outText to outText & " output_path: Optional absolute path for screenshot file(s)" & LF
set outText to outText & " If not provided, saves to /tmp/peekaboo_TIMESTAMP.png" & LF & LF
set outText to outText & "Options:" & LF
set outText to outText & " --window, -w: Capture frontmost window only" & LF
set outText to outText & " --interactive, -i: Interactive window selection" & LF
set outText to outText & " --multi, -m: Capture all windows with descriptive names" & LF
set outText to outText & " --verbose, -v: Enable verbose logging" & LF & LF
set outText to outText & "Commands:" & LF
set outText to outText & " list: List all running apps with window titles" & LF
set outText to outText & " help: Show this help message" & LF & LF
set outText to outText & "Examples:" & LF
set outText to outText & " # List running applications:" & LF
set outText to outText & " osascript " & scriptName & " list" & LF
set outText to outText & " # Screenshot Safari to /tmp with timestamp:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\"" & LF
set outText to outText & " # Screenshot Safari with custom path:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\" \"/Users/username/Desktop/safari_shot.png\"" & LF
set outText to outText & " # Screenshot using bundle ID:" & LF
set outText to outText & " osascript " & scriptName & " \"com.apple.TextEdit\" \"/tmp/textedit.png\"" & LF
set outText to outText & " # Screenshot with different format:" & LF
set outText to outText & " osascript " & scriptName & " \"Xcode\" \"/Users/username/Screenshots/xcode.jpg\"" & LF & LF
set outText to outText & " # Full screen capture with custom path:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\" \"/Users/username/Desktop/safari.png\"" & LF
set outText to outText & " # Front window only:" & LF
set outText to outText & " osascript " & scriptName & " \"TextEdit\" \"/tmp/textedit.png\" --window" & LF
set outText to outText & " # All windows with descriptive names:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\" \"/tmp/safari_windows.png\" --multi" & LF & LF
set outText to outText & "Features:" & LF
set outText to outText & " • Automatically resolves app names to bundle IDs and vice versa" & LF
set outText to outText & " • Launches apps if not running" & LF
set outText to outText & " • Brings target app to front before capture" & LF
set outText to outText & " • Supports PNG, JPG, PDF, and other formats via file extension" & LF
set outText to outText & " • Creates output directories automatically" & LF
set outText to outText & " • Enhanced error reporting with context" & LF & LF
set outText to outText & "Multi-Window Features:" & LF
set outText to outText & " • --multi creates separate files with descriptive names" & LF
set outText to outText & " • Window titles are sanitized for safe filenames" & LF
set outText to outText & " • Files named as: basename_window_N_title.ext" & LF
set outText to outText & " • Each window is focused before capture for accuracy" & LF & LF
set outText to outText & "Notes:" & LF
set outText to outText & " • Requires Screen Recording permission in System Preferences > Security & Privacy" & LF
set outText to outText & " • Output path must be absolute (starting with '/')" & LF
set outText to outText & " • Default format is PNG if no extension specified" & LF
set outText to outText & " • The script will wait " & (captureDelay as string) & " second(s) after bringing app to front before capture" & LF
set outText to outText & " • Requires Screen Recording permission in System Preferences" & LF
set outText to outText & " • Accessibility permission may be needed for window enumeration" & LF
set outText to outText & " • Window titles longer than " & maxWindowTitleLength & " characters are truncated" & LF
set outText to outText & " • Default capture delay: " & (captureDelay as string) & " second(s)" & LF
return outText
end usageText

View file

@ -1,679 +0,0 @@
#!/usr/bin/osascript
--------------------------------------------------------------------------------
-- peekaboo_enhanced.scpt - v1.0.0 "Peekaboo Pro! 👀 → 📸 → 💾"
-- Enhanced screenshot capture with multi-window support and app discovery
-- Peekaboo—screenshot got you! Now you see it, now it's saved.
--------------------------------------------------------------------------------
--#region Configuration Properties
property scriptInfoPrefix : "Peekaboo 👀: "
property defaultScreenshotFormat : "png"
property captureDelay : 1.0
property windowActivationDelay : 0.5
property enhancedErrorReporting : true
property verboseLogging : false
property maxWindowTitleLength : 50
--#endregion Configuration Properties
--#region Helper Functions
on isValidPath(thePath)
if thePath is not "" and (thePath starts with "/") then
return true
end if
return false
end isValidPath
on getFileExtension(filePath)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to "."
set pathParts to text items of filePath
set AppleScript's text item delimiters to oldDelims
if (count pathParts) > 1 then
return item -1 of pathParts
else
return ""
end if
end getFileExtension
on ensureDirectoryExists(dirPath)
try
do shell script "mkdir -p " & quoted form of dirPath
return true
on error
return false
end try
end ensureDirectoryExists
on sanitizeFilename(filename)
-- Replace problematic characters for filenames
set filename to my replaceText(filename, "/", "_")
set filename to my replaceText(filename, ":", "_")
set filename to my replaceText(filename, "*", "_")
set filename to my replaceText(filename, "?", "_")
set filename to my replaceText(filename, "\"", "_")
set filename to my replaceText(filename, "<", "_")
set filename to my replaceText(filename, ">", "_")
set filename to my replaceText(filename, "|", "_")
if (length of filename) > maxWindowTitleLength then
set filename to text 1 thru maxWindowTitleLength of filename
end if
return filename
end sanitizeFilename
on replaceText(theText, searchStr, replaceStr)
set oldDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to searchStr
set textItems to text items of theText
set AppleScript's text item delimiters to replaceStr
set newText to textItems as text
set AppleScript's text item delimiters to oldDelims
return newText
end replaceText
on formatErrorMessage(errorType, errorMsg, context)
if enhancedErrorReporting then
set formattedMsg to scriptInfoPrefix & errorType & ": " & errorMsg
if context is not "" then
set formattedMsg to formattedMsg & " (Context: " & context & ")"
end if
return formattedMsg
else
return scriptInfoPrefix & errorMsg
end if
end formatErrorMessage
on logVerbose(message)
if verboseLogging then
log "🔍 " & message
end if
end logVerbose
on trimWhitespace(theText)
set whitespaceChars to {" ", tab}
set newText to theText
repeat while (newText is not "") and (character 1 of newText is in whitespaceChars)
if (length of newText) > 1 then
set newText to text 2 thru -1 of newText
else
set newText to ""
end if
end repeat
repeat while (newText is not "") and (character -1 of newText is in whitespaceChars)
if (length of newText) > 1 then
set newText to text 1 thru -2 of newText
else
set newText to ""
end if
end repeat
return newText
end trimWhitespace
--#endregion Helper Functions
--#region App Discovery Functions
on listRunningApps()
set appList to {}
try
tell application "System Events"
repeat with proc in (every application process whose background only is false)
try
set appName to name of proc
set bundleID to bundle identifier of proc
set windowCount to count of windows of proc
set windowTitles to {}
if windowCount > 0 then
repeat with win in windows of proc
try
set winTitle to title of win
if winTitle is not "" then
set end of windowTitles to winTitle
end if
on error
-- Skip windows without accessible titles
end try
end repeat
end if
set end of appList to {appName:appName, bundleID:bundleID, windowCount:windowCount, windowTitles:windowTitles}
on error
-- Skip apps we can't access
end try
end repeat
end tell
on error errMsg
return my formatErrorMessage("Discovery Error", "Failed to enumerate running applications: " & errMsg, "app enumeration")
end try
return appList
end listRunningApps
on formatAppList(appList)
if appList starts with scriptInfoPrefix then
return appList -- Error message
end if
set output to scriptInfoPrefix & "Running Applications:" & linefeed & linefeed
repeat with appInfo in appList
set appName to appName of appInfo
set bundleID to bundleID of appInfo
set windowCount to windowCount of appInfo
set windowTitles to windowTitles of appInfo
set output to output & "• " & appName & " (" & bundleID & ")" & linefeed
set output to output & " Windows: " & windowCount
if windowCount > 0 and (count of windowTitles) > 0 then
set output to output & linefeed
repeat with winTitle in windowTitles
set output to output & " - \"" & winTitle & "\"" & linefeed
end repeat
else
set output to output & linefeed
end if
set output to output & linefeed
end repeat
return output
end formatAppList
--#endregion App Discovery Functions
--#region App Resolution Functions
on resolveAppIdentifier(appIdentifier)
my logVerbose("Resolving app identifier: " & appIdentifier)
-- First try as bundle ID
try
tell application "System Events"
set bundleApps to (every application process whose bundle identifier is appIdentifier)
if (count bundleApps) > 0 then
set targetApp to item 1 of bundleApps
set appName to name of targetApp
my logVerbose("Found running app by bundle ID: " & appName)
return {appName:appName, bundleID:appIdentifier, isRunning:true, resolvedBy:"bundle_id"}
end if
end tell
on error
my logVerbose("Bundle ID lookup failed, trying as app name")
end try
-- Try as application name for running apps
try
tell application "System Events"
set nameApps to (every application process whose name is appIdentifier)
if (count nameApps) > 0 then
set targetApp to item 1 of nameApps
try
set bundleID to bundle identifier of targetApp
on error
set bundleID to ""
end try
my logVerbose("Found running app by name: " & appIdentifier)
return {appName:appIdentifier, bundleID:bundleID, isRunning:true, resolvedBy:"app_name"}
end if
end tell
on error
my logVerbose("App name lookup failed for running processes")
end try
-- Try to find the app in /Applications (not running)
try
set appPath to "/Applications/" & appIdentifier & ".app"
tell application "System Events"
if exists file appPath then
try
set bundleID to bundle identifier of file appPath
on error
set bundleID to ""
end try
my logVerbose("Found app in /Applications: " & appIdentifier)
return {appName:appIdentifier, bundleID:bundleID, isRunning:false, resolvedBy:"applications_folder"}
end if
end tell
on error
my logVerbose("/Applications lookup failed")
end try
-- If it looks like a bundle ID, try launching it directly
if appIdentifier contains "." then
try
tell application "System Events"
launch application id appIdentifier
delay windowActivationDelay
set bundleApps to (every application process whose bundle identifier is appIdentifier)
if (count bundleApps) > 0 then
set targetApp to item 1 of bundleApps
set appName to name of targetApp
my logVerbose("Successfully launched app by bundle ID: " & appName)
return {appName:appName, bundleID:appIdentifier, isRunning:true, resolvedBy:"bundle_id_launch"}
end if
end tell
on error errMsg
my logVerbose("Bundle ID launch failed: " & errMsg)
end try
end if
return missing value
end resolveAppIdentifier
on getAppWindows(appName)
set windowInfo to {}
set windowCount to 0
set accessibleWindows to 0
try
tell application "System Events"
tell process appName
set windowCount to count of windows
repeat with i from 1 to windowCount
try
set win to window i
set winTitle to title of win
if winTitle is "" then set winTitle to "Untitled Window " & i
set end of windowInfo to {winTitle, i}
set accessibleWindows to accessibleWindows + 1
on error
set end of windowInfo to {("Window " & i), i}
end try
end repeat
end tell
end tell
on error errMsg
my logVerbose("Failed to get windows for " & appName & ": " & errMsg)
return {windowInfo:windowInfo, totalWindows:0, accessibleWindows:0, errorMsg:errMsg}
end try
return {windowInfo:windowInfo, totalWindows:windowCount, accessibleWindows:accessibleWindows, errorMsg:""}
end getAppWindows
on getAppWindowStatus(appName)
set windowStatus to my getAppWindows(appName)
set windowInfo to windowInfo of windowStatus
set totalWindows to totalWindows of windowStatus
set accessibleWindows to accessibleWindows of windowStatus
set windowError to errorMsg of windowStatus
if windowError is not "" then
return my formatErrorMessage("Window Access Error", "Cannot access windows for app '" & appName & "': " & windowError & ". The app may not be running or may not have accessibility permissions.", "window enumeration")
end if
if totalWindows = 0 then
return my formatErrorMessage("No Windows Error", "App '" & appName & "' is running but has no windows open. Peekaboo needs at least one window to capture. Please open a window in " & appName & " and try again.", "zero windows")
end if
if accessibleWindows = 0 and totalWindows > 0 then
return my formatErrorMessage("Window Access Error", "App '" & appName & "' has " & totalWindows & " window(s) but none are accessible. This may require accessibility permissions in System Preferences > Security & Privacy > Accessibility.", "accessibility required")
end if
-- Success case
set statusMsg to "App '" & appName & "' has " & totalWindows & " window(s)"
if accessibleWindows < totalWindows then
set statusMsg to statusMsg & " (" & accessibleWindows & " accessible)"
end if
return {status:"success", message:statusMsg, windowInfo:windowInfo, totalWindows:totalWindows, accessibleWindows:accessibleWindows}
end getAppWindowStatus
on bringAppToFront(appInfo)
set appName to appName of appInfo
set isRunning to isRunning of appInfo
my logVerbose("Bringing app to front: " & appName & " (running: " & isRunning & ")")
if not isRunning then
try
tell application appName to activate
delay windowActivationDelay
on error errMsg
return my formatErrorMessage("Activation Error", "Failed to launch app '" & appName & "': " & errMsg, "app launch")
end try
else
try
tell application "System Events"
tell process appName
set frontmost to true
end tell
end tell
delay windowActivationDelay
on error errMsg
return my formatErrorMessage("Focus Error", "Failed to bring app '" & appName & "' to front: " & errMsg, "app focus")
end try
end if
return ""
end bringAppToFront
--#endregion App Resolution Functions
--#region Screenshot Functions
on captureScreenshot(outputPath, captureMode, appName)
my logVerbose("Capturing screenshot to: " & outputPath & " (mode: " & captureMode & ")")
-- Ensure output directory exists
set outputDir to do shell script "dirname " & quoted form of outputPath
if not my ensureDirectoryExists(outputDir) then
return my formatErrorMessage("Directory Error", "Could not create output directory: " & outputDir, "directory creation")
end if
-- Wait for capture delay
delay captureDelay
-- Determine screenshot format
set fileExt to my getFileExtension(outputPath)
if fileExt is "" then
set fileExt to defaultScreenshotFormat
set outputPath to outputPath & "." & fileExt
end if
-- Build screencapture command based on mode
set screencaptureCmd to "screencapture -x"
if captureMode is "window" then
-- Use frontmost window without interaction
set screencaptureCmd to screencaptureCmd & " -o -W"
end if
-- Remove interactive mode - not suitable for unattended operation
-- Add format flag if not PNG (default)
if fileExt is not "png" then
set screencaptureCmd to screencaptureCmd & " -t " & fileExt
end if
-- Add output path
set screencaptureCmd to screencaptureCmd & " " & quoted form of outputPath
-- Capture screenshot
try
my logVerbose("Running: " & screencaptureCmd)
do shell script screencaptureCmd
-- Verify file was created
try
do shell script "test -f " & quoted form of outputPath
return outputPath
on error
return my formatErrorMessage("Capture Error", "Screenshot file was not created at: " & outputPath, "file verification")
end try
on error errMsg number errNum
-- Enhanced error handling for common screencapture issues
if errMsg contains "not authorized" or errMsg contains "Screen Recording" then
return my formatErrorMessage("Permission Error", "Screen Recording permission required. Please go to System Preferences > Security & Privacy > Screen Recording and add your terminal app to the allowed list. Then restart your terminal and try again.", "screen recording permission")
else if errMsg contains "No such file" then
return my formatErrorMessage("Path Error", "Cannot create screenshot at '" & outputPath & "'. Check that the directory exists and you have write permissions.", "file creation")
else if errMsg contains "Permission denied" then
return my formatErrorMessage("Permission Error", "Permission denied writing to '" & outputPath & "'. Check file/directory permissions or try a different location like /tmp/", "write permission")
else
return my formatErrorMessage("Capture Error", "screencapture failed: " & errMsg & ". This may be due to permissions, disk space, or system restrictions.", "error " & errNum)
end if
end try
end captureScreenshot
on captureMultipleWindows(appName, baseOutputPath)
-- Get detailed window status first
set windowStatus to my getAppWindowStatus(appName)
-- Check if it's an error
if (windowStatus starts with scriptInfoPrefix) then
return windowStatus -- Return the descriptive error
end if
-- Extract window info from successful status
set windowInfo to windowInfo of windowStatus
set totalWindows to totalWindows of windowStatus
set accessibleWindows to accessibleWindows of windowStatus
set capturedFiles to {}
my logVerbose("Multi-window capture: " & totalWindows & " total, " & accessibleWindows & " accessible")
if (count of windowInfo) = 0 then
return my formatErrorMessage("Multi-Window Error", "App '" & appName & "' has no accessible windows for multi-window capture. Try using single screenshot mode instead, or ensure the app has open windows.", "no accessible windows")
end if
-- Get base path components
set outputDir to do shell script "dirname " & quoted form of baseOutputPath
set baseName to do shell script "basename " & quoted form of baseOutputPath
set fileExt to my getFileExtension(baseName)
if fileExt is not "" then
set baseNameNoExt to text 1 thru -((length of fileExt) + 2) of baseName
else
set baseNameNoExt to baseName
set fileExt to defaultScreenshotFormat
end if
my logVerbose("Capturing " & (count of windowInfo) & " windows for " & appName)
repeat with winInfo in windowInfo
set winTitle to item 1 of winInfo
set winIndex to item 2 of winInfo
set sanitizedTitle to my sanitizeFilename(winTitle)
set windowFileName to baseNameNoExt & "_window_" & winIndex & "_" & sanitizedTitle & "." & fileExt
set windowOutputPath to outputDir & "/" & windowFileName
-- Focus the specific window first
try
tell application "System Events"
tell process appName
set frontmost to true
tell window winIndex
perform action "AXRaise"
end tell
end tell
end tell
delay 0.3
on error
my logVerbose("Could not focus window " & winIndex & ", continuing anyway")
end try
-- Capture the frontmost window
set captureResult to my captureScreenshot(windowOutputPath, "window", appName)
if captureResult starts with scriptInfoPrefix then
-- Error occurred, but continue with other windows
my logVerbose("Failed to capture window " & winIndex & ": " & captureResult)
else
set end of capturedFiles to {captureResult, winTitle, winIndex}
end if
end repeat
return capturedFiles
end captureMultipleWindows
--#endregion Screenshot Functions
--#region Main Script Logic (on run)
on run argv
set appSpecificErrorOccurred to false
try
my logVerbose("Starting Screenshotter Enhanced v2.0.0")
set argCount to count argv
-- Handle special commands
if argCount = 1 then
set command to item 1 of argv
if command is "list" or command is "--list" or command is "-l" then
set appList to my listRunningApps()
return my formatAppList(appList)
else if command is "help" or command is "--help" or command is "-h" then
return my usageText()
end if
end if
if argCount < 1 then return my usageText()
set appIdentifier to item 1 of argv
-- Use default tmp path if no output path provided
if argCount >= 2 then
set outputPath to item 2 of argv
else
set timestamp to do shell script "date +%Y%m%d_%H%M%S"
set outputPath to "/tmp/peekaboo_" & timestamp & ".png"
end if
set captureMode to "screen" -- default
set multiWindow to false
-- Parse additional options
if argCount > 2 then
repeat with i from 3 to argCount
set arg to item i of argv
if arg is "--window" or arg is "-w" then
set captureMode to "window"
-- Remove interactive mode option
else if arg is "--multi" or arg is "-m" then
set multiWindow to true
else if arg is "--verbose" or arg is "-v" then
set verboseLogging to true
end if
end repeat
end if
-- Validate arguments
if appIdentifier is "" then
return my formatErrorMessage("Argument Error", "App identifier cannot be empty." & linefeed & linefeed & my usageText(), "validation")
end if
if argCount >= 2 and not my isValidPath(outputPath) then
return my formatErrorMessage("Argument Error", "Output path must be an absolute path starting with '/'." & linefeed & linefeed & my usageText(), "validation")
end if
-- Resolve app identifier with detailed diagnostics
set appInfo to my resolveAppIdentifier(appIdentifier)
if appInfo is missing value then
set errorDetails to "Could not resolve app identifier '" & appIdentifier & "'."
-- Provide specific guidance based on identifier type
if appIdentifier contains "." then
set errorDetails to errorDetails & " This appears to be a bundle ID. Common issues:" & linefeed
set errorDetails to errorDetails & "• Bundle ID may be incorrect (try 'com.apple.' prefix for system apps)" & linefeed
set errorDetails to errorDetails & "• App may not be installed" & linefeed
set errorDetails to errorDetails & "• Use 'osascript peekaboo_enhanced.scpt list' to see available apps"
else
set errorDetails to errorDetails & " This appears to be an app name. Common issues:" & linefeed
set errorDetails to errorDetails & "• App name may be incorrect (case-sensitive)" & linefeed
set errorDetails to errorDetails & "• App may not be installed or running" & linefeed
set errorDetails to errorDetails & "• Try the full app name (e.g., 'Activity Monitor' not 'Activity')" & linefeed
set errorDetails to errorDetails & "• Use 'osascript peekaboo_enhanced.scpt list' to see running apps"
end if
return my formatErrorMessage("App Resolution Error", errorDetails, "app resolution")
end if
set resolvedAppName to appName of appInfo
set resolvedBy to resolvedBy of appInfo
my logVerbose("App resolved: " & resolvedAppName & " (method: " & resolvedBy & ")")
-- Bring app to front
set frontError to my bringAppToFront(appInfo)
if frontError is not "" then return frontError
-- Pre-capture window validation for better error messages
if multiWindow or captureMode is "window" then
set windowStatus to my getAppWindowStatus(resolvedAppName)
if (windowStatus starts with scriptInfoPrefix) then
-- Add context about what the user was trying to do
if multiWindow then
set contextError to "Multi-window capture failed: " & windowStatus
set contextError to contextError & linefeed & "💡 Suggestion: Try basic screenshot mode without --multi flag"
else
set contextError to "Window capture failed: " & windowStatus
set contextError to contextError & linefeed & "💡 Suggestion: Try full-screen capture mode without --window flag"
end if
return contextError
end if
-- Log successful window detection
set statusMsg to message of windowStatus
my logVerbose("Window validation passed: " & statusMsg)
end if
-- Handle multi-window capture
if multiWindow then
set capturedFiles to my captureMultipleWindows(resolvedAppName, outputPath)
if capturedFiles starts with scriptInfoPrefix then
return capturedFiles -- Error message
else
set windowCount to count of capturedFiles
set resultMsg to scriptInfoPrefix & "Multi-window capture successful! Captured " & windowCount & " window(s) for " & resolvedAppName & ":" & linefeed
repeat with fileInfo in capturedFiles
set filePath to item 1 of fileInfo
set winTitle to item 2 of fileInfo
set resultMsg to resultMsg & " 📸 " & filePath & " → \"" & winTitle & "\"" & linefeed
end repeat
set resultMsg to resultMsg & linefeed & "💡 All windows captured with descriptive filenames. Each file shows a different window of " & resolvedAppName & "."
return resultMsg
end if
else
-- Single capture
set screenshotResult to my captureScreenshot(outputPath, captureMode, resolvedAppName)
if screenshotResult starts with scriptInfoPrefix then
return screenshotResult -- Error message
else
set modeDescription to "full screen"
if captureMode is "window" then set modeDescription to "front window only"
return scriptInfoPrefix & "Screenshot captured successfully! 📸" & linefeed & "• File: " & screenshotResult & linefeed & "• App: " & resolvedAppName & linefeed & "• Mode: " & modeDescription & linefeed & "💡 The " & modeDescription & " of " & resolvedAppName & " has been saved."
end if
end if
on error generalErrorMsg number generalErrorNum
if appSpecificErrorOccurred then error generalErrorMsg number generalErrorNum
return my formatErrorMessage("Execution Error", generalErrorMsg, "error " & generalErrorNum)
end try
end run
--#endregion Main Script Logic (on run)
--#region Usage Function
on usageText()
set LF to linefeed
set scriptName to "peekaboo_enhanced.scpt"
set outText to scriptName & " - v1.0.0 \"Peekaboo Pro! 👀 → 📸 → 💾\" Enhanced AppleScript Screenshot Utility" & LF & LF
set outText to outText & "Peekaboo—screenshot got you! Now you see it, now it's saved." & LF
set outText to outText & "Takes unattended screenshots with multi-window support and app discovery." & LF & LF
set outText to outText & "Usage:" & LF
set outText to outText & " osascript " & scriptName & " \"<app_name_or_bundle_id>\" [\"<output_path>\"] [options]" & LF
set outText to outText & " osascript " & scriptName & " list" & LF
set outText to outText & " osascript " & scriptName & " help" & LF & LF
set outText to outText & "Parameters:" & LF
set outText to outText & " app_name_or_bundle_id: Application name (e.g., 'Safari') or bundle ID (e.g., 'com.apple.Safari')" & LF
set outText to outText & " output_path: Optional absolute path for screenshot file(s)" & LF
set outText to outText & " If not provided, saves to /tmp/peekaboo_TIMESTAMP.png" & LF & LF
set outText to outText & "Options:" & LF
set outText to outText & " --window, -w: Capture frontmost window only" & LF
set outText to outText & " --interactive, -i: Interactive window selection" & LF
set outText to outText & " --multi, -m: Capture all windows with descriptive names" & LF
set outText to outText & " --verbose, -v: Enable verbose logging" & LF & LF
set outText to outText & "Commands:" & LF
set outText to outText & " list: List all running apps with window titles" & LF
set outText to outText & " help: Show this help message" & LF & LF
set outText to outText & "Examples:" & LF
set outText to outText & " # List running applications:" & LF
set outText to outText & " osascript " & scriptName & " list" & LF
set outText to outText & " # Screenshot Safari to /tmp with timestamp:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\"" & LF
set outText to outText & " # Full screen capture with custom path:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\" \"/Users/username/Desktop/safari.png\"" & LF
set outText to outText & " # Front window only:" & LF
set outText to outText & " osascript " & scriptName & " \"TextEdit\" \"/tmp/textedit.png\" --window" & LF
set outText to outText & " # All windows with descriptive names:" & LF
set outText to outText & " osascript " & scriptName & " \"Safari\" \"/tmp/safari_windows.png\" --multi" & LF & LF
set outText to outText & "Multi-Window Features:" & LF
set outText to outText & " • --multi creates separate files with descriptive names" & LF
set outText to outText & " • Window titles are sanitized for safe filenames" & LF
set outText to outText & " • Files named as: basename_window_N_title.ext" & LF
set outText to outText & " • Each window is focused before capture for accuracy" & LF & LF
set outText to outText & "Notes:" & LF
set outText to outText & " • Requires Screen Recording permission in System Preferences" & LF
set outText to outText & " • Accessibility permission may be needed for window enumeration" & LF
set outText to outText & " • Window titles longer than " & maxWindowTitleLength & " characters are truncated" & LF
set outText to outText & " • Default capture delay: " & (captureDelay as string) & " second(s)" & LF
return outText
end usageText
--#endregion Usage Function