diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4df5941 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..779d103 --- /dev/null +++ b/README.md @@ -0,0 +1,300 @@ +# πŸ‘€ PEEKABOO! πŸ“Έ + +## 🎯 **Peekabooβ€”screenshot got you! Now you see it, now it's saved.** + +πŸ‘€ β†’ πŸ“Έ β†’ πŸ’Ύ β€” **Unattended screenshot automation that actually works** + +--- + +## πŸš€ **THE MAGIC** + +**Peekaboo** is your silent screenshot assassin. Point it at any app, and SNAP! β€” it's captured and saved before you can blink. + +- 🎯 **Smart targeting**: App names or bundle IDs +- πŸš€ **Auto-launch**: Sleeping apps? No problem! +- πŸ‘ **Brings apps forward**: Always gets the shot +- πŸ— **Creates directories**: Paths don't exist? Fixed! +- 🎨 **Multi-format**: PNG, JPG, PDF β€” you name it +- πŸ’₯ **Zero interaction**: 100% unattended operation + +--- + +## πŸŽͺ **TWO FLAVORS** + +### 🎯 **Peekaboo Classic** (`peekaboo.scpt`) +*Simple. Fast. Reliable.* + +```bash +# πŸ‘€ One app, one shot +osascript peekaboo.scpt "Safari" "/Users/you/Desktop/safari.png" + +# 🎯 Bundle ID targeting +osascript peekaboo.scpt "com.apple.TextEdit" "/tmp/textedit.jpg" +``` + +### πŸŽͺ **Peekaboo Pro** (`peekaboo_enhanced.scpt`) +*All the power. All the windows. All the time.* + +```bash +# πŸ” What's running right now? +osascript peekaboo_enhanced.scpt list + +# 🎭 Capture ALL windows with smart names +osascript peekaboo_enhanced.scpt "Chrome" "/tmp/chrome.png" --multi + +# πŸͺŸ Just the front window +osascript peekaboo_enhanced.scpt "TextEdit" "/tmp/textedit.png" --window +``` + +--- + +## ⚑ **QUICK WINS** + +### 🎯 **Basic Shot** +```bash +osascript peekaboo.scpt "Finder" "/Desktop/finder.png" +``` +**Result**: Full screen with Finder in focus β†’ `finder.png` + +### 🎭 **Multi-Window Magic** +```bash +osascript peekaboo_enhanced.scpt "Safari" "/tmp/safari.png" --multi +``` +**Result**: Multiple files with smart names: +- `safari_window_1_GitHub.png` +- `safari_window_2_Documentation.png` +- `safari_window_3_Google_Search.png` + +### πŸ” **App Discovery** +```bash +osascript peekaboo_enhanced.scpt list +``` +**Result**: Every running app + window titles. No guessing! + +--- + +## πŸ›  **SETUP** + +### 1️⃣ **Make Executable** +```bash +chmod +x peekaboo.scpt peekaboo_enhanced.scpt +``` + +### 2️⃣ **Grant Powers** +- System Preferences β†’ Security & Privacy β†’ **Screen Recording** +- Add your terminal app to the list +- ✨ You're golden! + +--- + +## 🎨 **FORMAT PARTY** + +Peekaboo speaks all the languages: + +```bash +# PNG (default) - crisp and clean +osascript peekaboo.scpt "Safari" "/tmp/shot.png" + +# JPG - smaller files +osascript peekaboo.scpt "Safari" "/tmp/shot.jpg" + +# PDF - vector goodness +osascript peekaboo.scpt "Safari" "/tmp/shot.pdf" +``` + +--- + +## πŸ† **POWER MOVES** + +### 🎯 **Targeting Options** +```bash +# By name (easy) +osascript peekaboo.scpt "Safari" "/tmp/safari.png" + +# By bundle ID (precise) +osascript peekaboo.scpt "com.apple.Safari" "/tmp/safari.png" + +# By display name (works too!) +osascript peekaboo.scpt "Final Cut Pro" "/tmp/finalcut.png" +``` + +### πŸŽͺ **Pro Features** +```bash +# Multi-window capture +--multi # All windows with descriptive names + +# Window modes +--window # Front window only (unattended!) + +# Debug mode +--verbose # See what's happening under the hood +``` + +### πŸ” **Discovery Mode** +```bash +osascript peekaboo_enhanced.scpt list +``` +Shows you: +- πŸ“± All running apps +- πŸ†” Bundle IDs +- πŸͺŸ Window counts +- πŸ“ Exact window titles + +--- + +## 🎭 **REAL-WORLD SCENARIOS** + +### πŸ“Š **Documentation Screenshots** +```bash +# Capture your entire workflow +osascript peekaboo_enhanced.scpt "Xcode" "/docs/xcode.png" --multi +osascript peekaboo_enhanced.scpt "Terminal" "/docs/terminal.png" --multi +osascript peekaboo_enhanced.scpt "Safari" "/docs/browser.png" --multi +``` + +### πŸš€ **CI/CD Integration** +```bash +# Automated testing screenshots +osascript peekaboo.scpt "Your App" "/test-results/app-$(date +%s).png" +``` + +### 🎬 **Content Creation** +```bash +# Before/after shots +osascript peekaboo.scpt "Photoshop" "/content/before.png" +# ... do your work ... +osascript peekaboo.scpt "Photoshop" "/content/after.png" +``` + +--- + +## 🚨 **TROUBLESHOOTING** + +### πŸ” **Permission Denied?** +- Check Screen Recording permissions +- Restart your terminal after granting access + +### πŸ‘» **App Not Found?** +```bash +# See what's actually running +osascript peekaboo_enhanced.scpt list + +# Try the bundle ID instead +osascript peekaboo.scpt "com.company.AppName" "/tmp/shot.png" +``` + +### πŸ“ **File Not Created?** +- Check the output directory exists (Peekaboo creates it!) +- Verify disk space +- Try a simple `/tmp/test.png` first + +### πŸ› **Debug Mode** +```bash +osascript peekaboo_enhanced.scpt "Safari" "/tmp/debug.png" --verbose +``` + +--- + +## πŸŽͺ **COMPARISON** + +| Feature | Classic 🎯 | Pro πŸŽͺ | +|---------|------------|--------| +| Basic screenshots | βœ… | βœ… | +| App targeting | βœ… | βœ… | +| Multi-format | βœ… | βœ… | +| **App discovery** | ❌ | βœ… | +| **Multi-window** | ❌ | βœ… | +| **Smart naming** | ❌ | βœ… | +| **Window modes** | ❌ | βœ… | +| **Verbose logging** | ❌ | βœ… | + +--- + +## πŸ§ͺ **TESTING** + +We've got you covered: + +```bash +# Run the full test suite +./test_screenshotter.sh + +# Test and cleanup +./test_screenshotter.sh --cleanup +``` + +Tests everything: +- βœ… App resolution (names + bundle IDs) +- βœ… Format support (PNG, JPG, PDF) +- βœ… Error handling +- βœ… Directory creation +- βœ… File validation +- βœ… Multi-window scenarios + +--- + +## βš™οΈ **CUSTOMIZATION** + +Tweak the magic in the script headers: + +```applescript +property captureDelay : 1.0 -- Wait before snap +property windowActivationDelay : 0.5 -- Window focus time +property enhancedErrorReporting : true -- Detailed errors +property verboseLogging : false -- Debug output +``` + +--- + +## πŸŽ‰ **WHY PEEKABOO ROCKS** + +### πŸš€ **Unattended = Unstoppable** +- No clicking, no selecting, no babysitting +- Perfect for automation and CI/CD +- Set it and forget it + +### 🎯 **Smart Targeting** +- Works with app names OR bundle IDs +- Auto-launches sleeping apps +- Always brings your target to the front + +### 🎭 **Multi-Window Mastery** +- Captures ALL windows with descriptive names +- Safe filename generation +- Never overwrites accidentally + +### πŸ” **Discovery Built-In** +- See exactly what's running +- Get precise window titles +- No more guessing games + +--- + +## πŸ“š **INSPIRED BY** + +Built in the style of the legendary **terminator.scpt** β€” because good patterns should be celebrated and extended. + +--- + +## πŸŽͺ **PROJECT FILES** + +``` +πŸ“ AppleScripts/ +β”œβ”€β”€ 🎯 peekaboo.scpt # Classic version +β”œβ”€β”€ πŸŽͺ peekaboo_enhanced.scpt # Pro version +β”œβ”€β”€ πŸ§ͺ test_screenshotter.sh # Test suite +└── πŸ“– README.md # This awesomeness +``` + +--- + +## πŸ† **THE BOTTOM LINE** + +**Peekaboo** doesn't just take screenshots. It **conquers** them. + +πŸ‘€ Point β†’ πŸ“Έ Shoot β†’ πŸ’Ύ Save β†’ πŸŽ‰ Done! + +*Now you see it, now it's saved. Peekaboo!* + +--- + +*Built with ❀️ and lots of β˜• for the macOS automation community.* \ No newline at end of file diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000..21a697c Binary files /dev/null and b/assets/banner.png differ diff --git a/peekaboo.scpt b/peekaboo.scpt new file mode 100755 index 0000000..d9dc6be --- /dev/null +++ b/peekaboo.scpt @@ -0,0 +1,334 @@ +#!/usr/bin/osascript +-------------------------------------------------------------------------------- +-- peekaboo.scpt - v1.0.0 "Peekaboo! πŸ‘€ β†’ πŸ“Έ β†’ πŸ’Ύ" +-- Unattended screenshot capture with app targeting and location specification +-- 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 enhancedErrorReporting : true +property verboseLogging : false +--#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 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 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 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) + my logVerbose("Capturing screenshot to: " & outputPath) + + -- 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 + + -- Capture screenshot using screencapture + 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 + + -- 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 + return my formatErrorMessage("Capture Error", "screencapture failed: " & errMsg, "error " & errNum) + end try +end captureScreenshot +--#endregion Screenshot Functions + +--#region Main Script Logic (on run) +on run argv + set appSpecificErrorOccurred to false + try + my logVerbose("Starting Screenshotter v1.0.0") + + set argCount to count argv + if argCount < 2 then return my usageText() + + set appIdentifier to item 1 of argv + set outputPath to item 2 of argv + + -- Validate arguments + if appIdentifier is "" then + return my formatErrorMessage("Argument Error", "App identifier cannot be empty." & linefeed & linefeed & my usageText(), "validation") + end if + + if 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 + set appInfo to my resolveAppIdentifier(appIdentifier) + if appInfo is missing value then + return my formatErrorMessage("Resolution Error", "Could not resolve app identifier '" & appIdentifier & "'. Check that the app name or bundle ID is correct.", "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 + + -- Capture screenshot + set screenshotResult to my captureScreenshot(outputPath) + if screenshotResult starts with scriptInfoPrefix then + -- Error occurred + return screenshotResult + else + -- Success + return scriptInfoPrefix & "Screenshot captured successfully: " & screenshotResult & " (App: " & resolvedAppName & ")" + 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.scpt" + + set outText to scriptName & " - v1.0.0 \"Peekaboo! πŸ‘€ β†’ πŸ“Έ β†’ πŸ’Ύ\" – 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 & "Usage:" & LF + set outText to outText & " osascript " & scriptName & " \"\" \"\"" & 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: Absolute path for screenshot file (e.g., '/Users/name/Desktop/screenshot.png')" & LF & LF + + set outText to outText & "Examples:" & LF + set outText to outText & " # Screenshot Safari using app name:" & 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 & "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 & "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 + + return outText +end usageText +--#endregion Usage Function \ No newline at end of file diff --git a/peekaboo_enhanced.scpt b/peekaboo_enhanced.scpt new file mode 100755 index 0000000..2c35b41 --- /dev/null +++ b/peekaboo_enhanced.scpt @@ -0,0 +1,573 @@ +#!/usr/bin/osascript +-------------------------------------------------------------------------------- +-- peekaboo_enhanced.scpt - v2.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 {} + try + tell application "System Events" + tell process appName + repeat with i from 1 to count of windows + 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} + 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) + end try + return windowInfo +end getAppWindows + +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 + return my formatErrorMessage("Capture Error", "screencapture failed: " & errMsg, "error " & errNum) + end try +end captureScreenshot + +on captureMultipleWindows(appName, baseOutputPath) + set windowInfo to my getAppWindows(appName) + set capturedFiles to {} + + if (count of windowInfo) = 0 then + return my formatErrorMessage("Window Error", "No accessible windows found for app '" & appName & "'", "window enumeration") + 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 < 2 then return my usageText() + + set appIdentifier to item 1 of argv + set outputPath to item 2 of argv + 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 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 + set appInfo to my resolveAppIdentifier(appIdentifier) + if appInfo is missing value then + return my formatErrorMessage("Resolution Error", "Could not resolve app identifier '" & appIdentifier & "'. Check that the app name or bundle ID is correct.", "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 + + -- 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 resultMsg to scriptInfoPrefix & "Captured " & (count of capturedFiles) & " windows 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 + 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 + return scriptInfoPrefix & "Screenshot captured successfully: " & screenshotResult & " (App: " & resolvedAppName & ", Mode: " & captureMode & ")" + 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 & " - v2.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 & " \"\" \"\" [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: Absolute path for screenshot file(s)" & 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 & " # Full screen capture:" & 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 + set outText to outText & " # Interactive selection:" & LF + set outText to outText & " osascript " & scriptName & " \"Finder\" \"/tmp/finder.png\" --interactive" & 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 \ No newline at end of file diff --git a/test_screenshotter.sh b/test_screenshotter.sh new file mode 100755 index 0000000..0fbdafb --- /dev/null +++ b/test_screenshotter.sh @@ -0,0 +1,271 @@ +#!/bin/bash +################################################################################ +# test_screenshotter.sh - Test suite for screenshotter.scpt +# Tests various scenarios including app names, bundle IDs, formats, and error cases +################################################################################ + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCREENSHOTTER_SCRIPT="$SCRIPT_DIR/screenshotter.scpt" +TEST_OUTPUT_DIR="$HOME/Desktop/screenshotter_tests" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Test counters +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[PASS]${NC} $1" + ((TESTS_PASSED++)) +} + +log_error() { + echo -e "${RED}[FAIL]${NC} $1" + ((TESTS_FAILED++)) +} + +log_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +run_test() { + local test_name="$1" + local app_identifier="$2" + local output_path="$3" + local expected_result="$4" # "success" or "error" + + ((TESTS_RUN++)) + log_info "Running test: $test_name" + + # Run the screenshotter script + local result + local exit_code + + if result=$(osascript "$SCREENSHOTTER_SCRIPT" "$app_identifier" "$output_path" 2>&1); then + exit_code=0 + else + exit_code=1 + fi + + # Check result + if [[ "$expected_result" == "success" ]]; then + if [[ $exit_code -eq 0 ]] && [[ "$result" == *"Screenshot captured successfully"* ]]; then + if [[ -f "$output_path" ]]; then + log_success "$test_name - Screenshot created at $output_path" + # Get file size for verification + local file_size=$(stat -f%z "$output_path" 2>/dev/null || echo "0") + if [[ $file_size -gt 1000 ]]; then + log_info " File size: ${file_size} bytes (reasonable)" + else + log_warning " File size: ${file_size} bytes (suspiciously small)" + fi + else + log_error "$test_name - Script reported success but file not found: $output_path" + fi + else + log_error "$test_name - Expected success but got: $result" + fi + else + # Expected error + if [[ $exit_code -ne 0 ]] || [[ "$result" == *"Error"* ]]; then + log_success "$test_name - Correctly failed with: $result" + else + log_error "$test_name - Expected error but got success: $result" + fi + fi + + echo "" +} + +cleanup_test_files() { + log_info "Cleaning up test files..." + if [[ -d "$TEST_OUTPUT_DIR" ]]; then + rm -rf "$TEST_OUTPUT_DIR" + fi +} + +setup_test_environment() { + log_info "Setting up test environment..." + + # Check if screenshotter script exists + if [[ ! -f "$SCREENSHOTTER_SCRIPT" ]]; then + log_error "Screenshotter script not found at: $SCREENSHOTTER_SCRIPT" + exit 1 + fi + + # Create test output directory + mkdir -p "$TEST_OUTPUT_DIR" + + # Check permissions + log_info "Checking system permissions..." + log_warning "Note: This script requires Screen Recording permission in System Preferences > Security & Privacy" + log_warning "If tests fail, please check that Terminal (or your terminal app) has Screen Recording permission" + echo "" +} + +run_all_tests() { + log_info "Starting screenshotter.scpt test suite" + echo "Timestamp: $TIMESTAMP" + echo "Test output directory: $TEST_OUTPUT_DIR" + echo "" + + # Test 1: Basic app name test with system app + run_test "Basic Finder test" \ + "Finder" \ + "$TEST_OUTPUT_DIR/finder_${TIMESTAMP}.png" \ + "success" + + # Test 2: Bundle ID test + run_test "Bundle ID test (Finder)" \ + "com.apple.finder" \ + "$TEST_OUTPUT_DIR/finder_bundle_${TIMESTAMP}.png" \ + "success" + + # Test 3: Different format test + run_test "JPG format test" \ + "Finder" \ + "$TEST_OUTPUT_DIR/finder_${TIMESTAMP}.jpg" \ + "success" + + # Test 4: TextEdit test (another common app) + run_test "TextEdit test" \ + "TextEdit" \ + "$TEST_OUTPUT_DIR/textedit_${TIMESTAMP}.png" \ + "success" + + # Test 5: PDF format test + run_test "PDF format test" \ + "TextEdit" \ + "$TEST_OUTPUT_DIR/textedit_${TIMESTAMP}.pdf" \ + "success" + + # Test 6: Non-existent app (should fail) + run_test "Non-existent app test" \ + "NonExistentApp12345" \ + "$TEST_OUTPUT_DIR/nonexistent_${TIMESTAMP}.png" \ + "error" + + # Test 7: Invalid path (should fail) + run_test "Invalid path test" \ + "Finder" \ + "relative/path/screenshot.png" \ + "error" + + # Test 8: Empty app name (should fail) + run_test "Empty app name test" \ + "" \ + "$TEST_OUTPUT_DIR/empty_${TIMESTAMP}.png" \ + "error" + + # Test 9: Directory creation test + local deep_dir="$TEST_OUTPUT_DIR/deep/nested/directory" + run_test "Directory creation test" \ + "Finder" \ + "$deep_dir/finder_deep_${TIMESTAMP}.png" \ + "success" + + # Test 10: Bundle ID for third-party app (if Safari is available) + run_test "Safari bundle ID test" \ + "com.apple.Safari" \ + "$TEST_OUTPUT_DIR/safari_bundle_${TIMESTAMP}.png" \ + "success" +} + +show_usage_test() { + log_info "Testing usage output..." + local usage_output + if usage_output=$(osascript "$SCREENSHOTTER_SCRIPT" 2>&1); then + if [[ "$usage_output" == *"Usage:"* ]] && [[ "$usage_output" == *"Examples:"* ]]; then + log_success "Usage output test - Proper usage information displayed" + else + log_error "Usage output test - Usage information incomplete" + fi + else + log_error "Usage output test - Failed to get usage output" + fi + echo "" +} + +show_summary() { + echo "================================" + echo "Test Summary" + echo "================================" + echo "Tests run: $TESTS_RUN" + echo "Tests passed: $TESTS_PASSED" + echo "Tests failed: $TESTS_FAILED" + + if [[ $TESTS_FAILED -eq 0 ]]; then + log_success "All tests passed! πŸŽ‰" + else + log_error "$TESTS_FAILED test(s) failed" + fi + + echo "" + log_info "Test files location: $TEST_OUTPUT_DIR" + if [[ -d "$TEST_OUTPUT_DIR" ]]; then + local file_count=$(find "$TEST_OUTPUT_DIR" -type f | wc -l) + log_info "Generated $file_count screenshot file(s)" + fi +} + +show_file_listing() { + if [[ -d "$TEST_OUTPUT_DIR" ]]; then + echo "" + log_info "Generated test files:" + find "$TEST_OUTPUT_DIR" -type f -exec ls -lh {} \; | while read -r line; do + echo " $line" + done + fi +} + +# Main execution +main() { + setup_test_environment + show_usage_test + run_all_tests + show_summary + show_file_listing + + if [[ "${1:-}" == "--cleanup" ]]; then + cleanup_test_files + log_info "Test files cleaned up" + else + log_info "Run with --cleanup to remove test files" + fi +} + +# Handle script arguments +case "${1:-}" in + --help|-h) + echo "Usage: $0 [--cleanup] [--help]" + echo "" + echo "Options:" + echo " --cleanup Remove test files after running tests" + echo " --help Show this help message" + echo "" + echo "This script tests the screenshotter.scpt AppleScript by:" + echo "- Testing various app names and bundle IDs" + echo "- Testing different output formats (PNG, JPG, PDF)" + echo "- Testing error conditions" + echo "- Verifying file creation and sizes" + exit 0 + ;; + *) + main "$@" + ;; +esac \ No newline at end of file