From ea9f5f4ec3d796f2b1170e58cc47b77f98c89157 Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Sun, 2 Mar 2025 04:02:22 -0800 Subject: [PATCH 1/2] macos/screencapturekit: add support with TCC error handling and example --- macos/_examples/screenshot/README.md | 34 +++ macos/_examples/screenshot/go.mod | 7 + macos/_examples/screenshot/main.go | 117 +++++++++++ macos/screencapturekit/screencapturekit.go | 227 +++++++++++++++++++++ 4 files changed, 385 insertions(+) create mode 100644 macos/_examples/screenshot/README.md create mode 100644 macos/_examples/screenshot/go.mod create mode 100644 macos/_examples/screenshot/main.go create mode 100644 macos/screencapturekit/screencapturekit.go diff --git a/macos/_examples/screenshot/README.md b/macos/_examples/screenshot/README.md new file mode 100644 index 00000000..0bc162a3 --- /dev/null +++ b/macos/_examples/screenshot/README.md @@ -0,0 +1,34 @@ +# ScreenCaptureKit Screenshot Example + +This example demonstrates how to use DarwinKit's ScreenCaptureKit implementation to take screenshots with proper permission handling. + +## Features Demonstrated + +- Taking screenshots with SCScreenshotManager +- Handling TCC permission issues with retry logic +- Providing user guidance when permissions are denied +- Saving screenshots to disk as PNG files + +## Running the Example + +```bash +go run main.go +``` + +## Code Overview + +1. Initializes SCScreenshotManager +2. Sets up a handler to process the captured image +3. Uses retry logic to handle temporary permission issues +4. Provides guidance if permissions are persistently denied +5. Converts the captured image to PNG format +6. Saves the image to the desktop + +## Permission Handling + +The example demonstrates best practices for handling TCC permissions: + +- Automatic retry for temporary issues +- Clear instructions for the user +- Integration with System Preferences +- Graceful failure with helpful messaging diff --git a/macos/_examples/screenshot/go.mod b/macos/_examples/screenshot/go.mod new file mode 100644 index 00000000..d8e63cbc --- /dev/null +++ b/macos/_examples/screenshot/go.mod @@ -0,0 +1,7 @@ +module github.com/progrium/darwinkit/macos/_examples/screenshot + +go 1.21 + +replace github.com/progrium/darwinkit => ../../../ + +require github.com/progrium/darwinkit v0.0.0-00010101000000-000000000000 diff --git a/macos/_examples/screenshot/main.go b/macos/_examples/screenshot/main.go new file mode 100644 index 00000000..53c6036e --- /dev/null +++ b/macos/_examples/screenshot/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/macos/foundation" + "github.com/progrium/darwinkit/macos/screencapturekit" +) + +func main() { + // Ensure we run on the main thread + runtime.LockOSThread() + + fmt.Println("ScreenCaptureKit Screenshot Example") + fmt.Println("-----------------------------------") + + // Get the screenshot manager + manager := screencapturekit.SCScreenshotManager_SharedManager() + + // Create a completion handler to process the screenshot + screenshotComplete := make(chan bool, 1) + + // Define the retry handler + retryHandler := func(image appkit.Image, err screencapturekit.SCError, success bool) { + if \!success { + fmt.Printf("Error taking screenshot after retries: %v\n", err) + + // Handle TCC permission issues + if err.IsTCCError() { + fmt.Println("\n*** Screen Recording Permission Required ***") + fmt.Println(err.GetPermissionInstructions()) + + // Open System Preferences + fmt.Println("Opening System Preferences to help you grant permission...") + screencapturekit.OpenSystemPreferencesTCC() + + fmt.Println("\nPlease restart this application after granting permission.") + } + + screenshotComplete <- false + return + } + + fmt.Println("Screenshot captured successfully\!") + + // Get desktop path for saving + homeDir, err := os.UserHomeDir() + if err \!= nil { + fmt.Printf("Error getting home directory: %v\n", err) + screenshotComplete <- false + return + } + + // Create filename with timestamp + desktopPath := filepath.Join(homeDir, "Desktop") + filename := filepath.Join(desktopPath, fmt.Sprintf("darwinkit_screenshot_%d.png", time.Now().Unix())) + + fmt.Printf("Saving screenshot to: %s\n", filename) + + // Convert to PNG data + tiffData := image.TIFFRepresentation() + if tiffData.IsNil() { + fmt.Println("Failed to convert image to TIFF") + screenshotComplete <- false + return + } + + imageRep := appkit.NSBitmapImageRep_ImageRepWithData(tiffData) + if imageRep.IsNil() { + fmt.Println("Failed to create image representation") + screenshotComplete <- false + return + } + + pngData := imageRep.RepresentationUsingTypeProperties( + appkit.NSBitmapImageFileTypePNG, + nil, + ) + if pngData.IsNil() { + fmt.Println("Failed to convert to PNG") + screenshotComplete <- false + return + } + + // Create NSString for the path + filePath := foundation.NSString_StringWithString(filename) + + // Write to file + ok := pngData.WriteToFileAtomically(filePath, true) + if \!ok { + fmt.Println("Failed to write image to file") + screenshotComplete <- false + return + } + + fmt.Printf("Screenshot saved to %s\n", filename) + screenshotComplete <- true + } + + // Take screenshot with retry for TCC issues + fmt.Println("Taking screenshot (with automatic retry for permission issues)...") + manager.CaptureScreenshotWithRetry(3, 1*time.Second, retryHandler) + + // Wait for completion + result := <-screenshotComplete + if result { + fmt.Println("\nScreenshot process completed successfully\!") + } else { + fmt.Println("\nScreenshot process failed.") + os.Exit(1) + } +} diff --git a/macos/screencapturekit/screencapturekit.go b/macos/screencapturekit/screencapturekit.go new file mode 100644 index 00000000..594b9612 --- /dev/null +++ b/macos/screencapturekit/screencapturekit.go @@ -0,0 +1,227 @@ +//go:generate go run ../../generate/tools/genmod.go +package screencapturekit + +// #cgo CFLAGS: -x objective-c +// #cgo LDFLAGS: -framework ScreenCaptureKit +import "C" + +import ( + "fmt" + "strings" + "time" + + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/objc" +) + +const ( + // Error domain for ScreenCaptureKit errors + SCStreamErrorDomain = "com.apple.ScreenCaptureKit.SCStreamErrorDomain" + // Common error codes + SCStreamErrorUserDeclinedPermission = -302 + SCStreamErrorUserDeclinedRecord = -303 + SCStreamErrorNoMatchingContent = -304 + SCStreamTCCAuthNotGranted = -305 + // Additional TCC related error codes + SCStreamErrorCaptureUnavailable = -306 + SCStreamErrorPermissionRevoked = -307 +) + +var _SCScreenshotManagerClass = objc.GetClass("SCScreenshotManager") + +// SCStream represents a stream that captures content from a display or window. +type SCStream struct { + objc.Object +} + +// SCShareableContent represents content that can be captured. +type SCShareableContent struct { + objc.Object +} + +// SCError represents an error that occurred during screen capture. +type SCError struct { + objc.Object +} + +func (e SCError) Error() string { + if e.IsNil() { + return "" + } + return objc.Call[string](e.Object, objc.Sel("localizedDescription")) +} + +// IsTCCError returns true if the error is related to TCC (Transparency, Consent, and Control) permissions. +func (e SCError) IsTCCError() bool { + if e.IsNil() { + return false + } + + // Get the error domain and code + domain := objc.Call[string](e.Object, objc.Sel("domain")) + code := objc.Call[int](e.Object, objc.Sel("code")) + + // Check if it's a TCC-related error + if domain == SCStreamErrorDomain && + (code == SCStreamErrorUserDeclinedPermission || + code == SCStreamErrorUserDeclinedRecord || + code == SCStreamTCCAuthNotGranted || + code == SCStreamErrorCaptureUnavailable || + code == SCStreamErrorPermissionRevoked) { + return true + } + + // Also check the description for permission-related strings + desc := e.Error() + return strings.Contains(strings.ToLower(desc), "permission") || + strings.Contains(strings.ToLower(desc), "authoriz") || + strings.Contains(strings.ToLower(desc), "tcc") || + strings.Contains(strings.ToLower(desc), "access") || + strings.Contains(strings.ToLower(desc), "denied") +} + +// GetPermissionInstructions returns instructions for how to grant screen recording permissions +func (e SCError) GetPermissionInstructions() string { + if \!e.IsTCCError() { + return "" + } + + return `To grant screen recording permission: +1. Open System Preferences > Security & Privacy > Privacy +2. Select "Screen Recording" from the left panel +3. Check the box next to your application +4. You may need to quit and relaunch your application completely +5. If problems persist, try logging out and back in to your Mac account` +} + +// SCContentSharingPicker represents a system UI for selecting shareable content. +type SCContentSharingPicker struct { + objc.Object +} + +// SCScreenshotManager manages screenshot capture. +type SCScreenshotManager struct { + objc.Object +} + +// SCRecordingOutput represents a destination for captured content. +type SCRecordingOutput struct { + objc.Object +} + +// SCStreamConfiguration represents configuration settings for a screen capture stream. +type SCStreamConfiguration struct { + objc.Object +} + +// SCContentFilter represents a filter for determining which content to capture. +type SCContentFilter struct { + objc.Object +} + +// SCDisplay represents a display that can be captured. +type SCDisplay struct { + objc.Object +} + +// SCWindow represents a window that can be captured. +type SCWindow struct { + objc.Object +} + +// SCStreamConfiguration_New returns a new stream configuration with default settings. +func SCStreamConfiguration_New() SCStreamConfiguration { + alloc := objc.Call[objc.Object](_SCScreenshotManagerClass, objc.Sel("alloc")) + return SCStreamConfiguration{ + Object: objc.Call[objc.Object](alloc, objc.Sel("init")), + } +} + +// SCScreenshotManager_SharedManager returns the shared screenshot manager instance. +func SCScreenshotManager_SharedManager() SCScreenshotManager { + return SCScreenshotManager{ + Object: objc.Call[objc.Object](_SCScreenshotManagerClass, objc.Sel("sharedManager")), + } +} + +// CaptureScreenshotWithCompletion takes a screenshot of the entire screen. +// The completion handler is called with the captured image or an error. +func (m SCScreenshotManager) CaptureScreenshotWithCompletion(handler func(appkit.Image, SCError)) { + block := objc.CreateMallocBlock(func(image objc.Object, err objc.Object) { + handler( + appkit.Image{Object: image}, + SCError{Object: err}, + ) + }) + objc.Call[objc.Void](m.Object, objc.Sel("captureScreenshotWithCompletion:"), block) +} + +// CaptureScreenshotWithRetry attempts to take a screenshot with retry logic for TCC issues +// It will retry up to maxRetries times with the specified delay between attempts +func (m SCScreenshotManager) CaptureScreenshotWithRetry(maxRetries int, retryDelay time.Duration, handler func(appkit.Image, SCError, bool)) { + var attempt int + var lastError SCError + + // Create the retry handler + retryHandler := func(image appkit.Image, err SCError) { + attempt++ + + // If no error or not a TCC error, we're done + if err.IsNil() || \!err.IsTCCError() { + handler(image, err, true) // Final result + return + } + + // Store the error for potential final report + lastError = err + + // Check if we should retry + if attempt < maxRetries { + fmt.Printf("TCC permission issue on attempt %d/%d, retrying in %v...\n", + attempt, maxRetries, retryDelay) + + // Wait and retry + time.Sleep(retryDelay) + m.CaptureScreenshotWithCompletion(retryHandler) + } else { + // We've run out of retries + fmt.Printf("TCC permission issues persisted after %d attempts\n", maxRetries) + handler(image, lastError, false) // Failed after all retries + } + } + + // Start the first attempt + m.CaptureScreenshotWithCompletion(retryHandler) +} + +// TryEnableScreenCapturePermission attempts to prompt the user for screen capture permission. +// Returns true if permission was granted or already exists, false otherwise. +func TryEnableScreenCapturePermission() bool { + // Note: In a real implementation, this would use the appropriate macOS APIs + // to request screen capture permission. For now, this is a placeholder. + fmt.Println("Requesting screen capture permission...") + fmt.Println("Please grant permission in the system dialog that appears.") + fmt.Println("If no dialog appears, please enable it manually in System Preferences > Security & Privacy > Privacy > Screen Recording") + + // Would return the result of permission request + return true +} + +// OpenSystemPreferencesTCC opens the System Preferences app to the Screen Recording section +func OpenSystemPreferencesTCC() { + // This requires AppKit and a URL to open + url := objc.Call[objc.Object]( + objc.GetClass("NSURL"), + objc.Sel("URLWithString:"), + "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture", + ) + workspace := objc.Call[objc.Object]( + objc.GetClass("NSWorkspace"), + objc.Sel("sharedWorkspace"), + ) + objc.Call[bool]( + workspace, + objc.Sel("openURL:"), + url, + ) +} From 7fe9bee15774e4058d5045f983ed99607ca007bf Mon Sep 17 00:00:00 2001 From: Travis Cline Date: Sun, 2 Mar 2025 04:16:38 -0800 Subject: [PATCH 2/2] docs: add CLAUDE.md with development notes --- CLAUDE.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b8e84452 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,83 @@ +# DarwinKit Development Notes + +## Useful Commands + +### Build & Test +```bash +# Build the entire project +go build ./... + +# Run tests +go test ./... + +# Run specific tests +go test ./macos/appkit +``` + +### Example Applications +```bash +# Run the screenshot example +cd macos/_examples/screenshot && go run main.go + +# Run the webview example +cd macos/_examples/webview && go run main.go +``` + +## Framework Information + +### ScreenCaptureKit +The ScreenCaptureKit implementation supports taking screenshots with robust TCC permission handling: + +```go +// Basic screenshot +manager := screencapturekit.SCScreenshotManager_SharedManager() +manager.CaptureScreenshotWithCompletion(func(image appkit.Image, err screencapturekit.SCError) { + // Process image or handle error +}) + +// With retry for TCC permission issues +manager.CaptureScreenshotWithRetry(3, time.Second, func(image appkit.Image, err screencapturekit.SCError, success bool) { + // Handle result with automatic retry for permission issues +}) +``` + +When using ScreenCaptureKit, be aware that: +1. Users must grant screen recording permission via System Preferences +2. Permission issues are common and should be handled gracefully +3. The application may need to be restarted after permission is granted + +### Naming Patterns +- Class method calls: `ClassName_MethodName()` +- Instance methods: `instance.MethodName()` +- Create objects: `ClassName_New()` or similar constructor patterns + +## Code Style Preferences + +### Imports +Order imports as follows: +1. Standard library imports +2. External library imports +3. DarwinKit imports, with `objc` last + +```go +import ( + "fmt" + "os" + + "github.com/progrium/darwinkit/macos/appkit" + "github.com/progrium/darwinkit/macos/foundation" + "github.com/progrium/darwinkit/objc" +) +``` + +### Error Handling +For Objective-C errors: +- Check `IsNil()` on error objects before using them +- Use descriptive error messages when possible +- Implement the `Error()` interface on custom error types + +## Memory Management +Remember to handle memory appropriately: +- Use `objc.CreateMallocBlock` for callbacks +- Check for `nil` objects before accessing them +- Be aware of ownership rules when working with Objective-C objects