diff --git a/peekaboo-cli/Sources/peekaboo/ApplicationFinder.swift b/peekaboo-cli/Sources/peekaboo/ApplicationFinder.swift index 4abdf1f..9e931d0 100644 --- a/peekaboo-cli/Sources/peekaboo/ApplicationFinder.swift +++ b/peekaboo-cli/Sources/peekaboo/ApplicationFinder.swift @@ -26,7 +26,7 @@ final class ApplicationFinder: Sendable { // Find all possible matches let allMatches = findAllMatches(for: identifier, in: runningApps) - + // Filter out browser helpers for common browser searches let matches = filterBrowserHelpers(matches: allMatches, identifier: identifier) @@ -180,7 +180,7 @@ final class ApplicationFinder: Sendable { // Provide browser-specific error messages let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"] let lowerIdentifier = identifier.lowercased() - + if browserIdentifiers.contains(lowerIdentifier) { // Logger.shared.error("\(identifier.capitalized) browser is not running or not found") } else { @@ -319,48 +319,49 @@ final class ApplicationFinder: Sendable { return count } - + private static func filterBrowserHelpers(matches: [AppMatch], identifier: String) -> [AppMatch] { // Define common browser identifiers that should filter out helpers let browserIdentifiers = ["chrome", "safari", "firefox", "edge", "brave", "arc", "opera"] let lowerIdentifier = identifier.lowercased() - + // Check if the search is for a common browser guard browserIdentifiers.contains(lowerIdentifier) else { return matches // No filtering for non-browser searches } - + // Logger.shared.debug("Filtering browser helpers for '\(identifier)' search") - + // Filter out helper processes for browser searches let filteredMatches = matches.filter { match in guard let appName = match.app.localizedName?.lowercased() else { return true } - + // Exclude obvious helper processes - let isHelper = appName.contains("helper") || - appName.contains("renderer") || - appName.contains("utility") || - appName.contains("plugin") || - appName.contains("service") || - appName.contains("crashpad") || - appName.contains("gpu") || - appName.contains("background") - + let isHelper = appName.contains("helper") || + appName.contains("renderer") || + appName.contains("utility") || + appName.contains("plugin") || + appName.contains("service") || + appName.contains("crashpad") || + appName.contains("gpu") || + appName.contains("background") + if isHelper { // Logger.shared.debug("Filtering out helper process: \(appName)") return false } - + return true } - + // If we filtered out all matches, return the original matches to avoid "not found" errors // But log a warning about this case if filteredMatches.isEmpty && !matches.isEmpty { - // Logger.shared.debug("All matches were filtered as helpers, returning original matches to avoid 'not found' error") + // Logger.shared.debug("All matches were filtered as helpers, returning original matches to avoid 'not + // found' error") return matches } - + // Logger.shared.debug("After browser helper filtering: \(filteredMatches.count) matches remaining") return filteredMatches } diff --git a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift index cff38cf..68191b4 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift @@ -164,10 +164,15 @@ struct ImageCommand: AsyncParsableCommand { return savedFiles } - private func captureAllScreensWithFallback(displays: [CGDirectDisplayID]) async throws(CaptureError) -> [SavedFile] { + private func captureAllScreensWithFallback(displays: [CGDirectDisplayID]) async throws(CaptureError) + -> [SavedFile] { var savedFiles: [SavedFile] = [] for (index, displayID) in displays.enumerated() { - let savedFile = try await captureSingleDisplayWithFallback(displayID: displayID, index: index, labelSuffix: "") + let savedFile = try await captureSingleDisplayWithFallback( + displayID: displayID, + index: index, + labelSuffix: "" + ) savedFiles.append(savedFile) } return savedFiles @@ -243,12 +248,12 @@ struct ImageCommand: AsyncParsableCommand { let availableTitles = windows.map { "\"\($0.title)\"" }.joined(separator: ", ") let searchTerm = windowTitle let appName = targetApp.localizedName ?? "Unknown" - + Logger.shared.debug( "Window not found. Searched for '\(searchTerm)' in \(appName). " + - "Available windows: \(availableTitles)" + "Available windows: \(availableTitles)" ) - + throw CaptureError.windowTitleNotFound(searchTerm, appName, availableTitles) } targetWindow = window @@ -410,38 +415,38 @@ struct ImageCommand: AsyncParsableCommand { throw CaptureError.windowCaptureFailed(error) } } - + private func captureFrontmostWindow() async throws -> [SavedFile] { Logger.shared.debug("Capturing frontmost window") - + // Get the frontmost (active) application guard let frontmostApp = NSWorkspace.shared.frontmostApplication else { throw CaptureError.appNotFound("No frontmost application found") } - + Logger.shared.debug("Frontmost app: \(frontmostApp.localizedName ?? "Unknown")") - + // Get windows for the frontmost app let windows = try WindowManager.getWindowsForApp(pid: frontmostApp.processIdentifier) guard !windows.isEmpty else { throw CaptureError.noWindowsFound(frontmostApp.localizedName ?? "frontmost application") } - + // Get the frontmost window (index 0) let frontmostWindow = windows[0] - + Logger.shared.debug("Capturing frontmost window: '\(frontmostWindow.title)'") - + // Generate output path let timestamp = DateFormatter.timestamp.string(from: Date()) let appName = frontmostApp.localizedName ?? "UnknownApp" let safeName = appName.replacingOccurrences(of: " ", with: "_") let fileName = "frontmost_\(safeName)_\(timestamp).\(format.rawValue)" let filePath = OutputPathResolver.getOutputPathWithFallback(basePath: path, fileName: fileName) - + // Capture the window try await captureWindow(frontmostWindow, to: filePath) - + return [SavedFile( path: filePath, item_label: appName, diff --git a/peekaboo-cli/Sources/peekaboo/ImageErrorHandler.swift b/peekaboo-cli/Sources/peekaboo/ImageErrorHandler.swift index 83041af..8e6680d 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageErrorHandler.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageErrorHandler.swift @@ -1,7 +1,7 @@ -import Foundation import AppKit +import Foundation -struct ImageErrorHandler { +enum ImageErrorHandler { static func handleError(_ error: Error, jsonOutput: Bool) { let captureError: CaptureError = if let err = error as? CaptureError { err diff --git a/peekaboo-cli/Sources/peekaboo/ImageSaver.swift b/peekaboo-cli/Sources/peekaboo/ImageSaver.swift index 71f21b3..a9a7a99 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageSaver.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageSaver.swift @@ -1,5 +1,5 @@ -import Foundation import CoreGraphics +import Foundation import ImageIO import UniformTypeIdentifiers diff --git a/peekaboo-cli/Sources/peekaboo/JSONOutput.swift b/peekaboo-cli/Sources/peekaboo/JSONOutput.swift index 0c5b8c1..b7f8e43 100644 --- a/peekaboo-cli/Sources/peekaboo/JSONOutput.swift +++ b/peekaboo-cli/Sources/peekaboo/JSONOutput.swift @@ -7,11 +7,17 @@ struct JSONResponse: Codable { let debug_logs: [String] let error: ErrorInfo? - init(success: Bool, data: Any? = nil, messages: [String]? = nil, debugLogs: [String] = [], error: ErrorInfo? = nil) { + init( + success: Bool, + data: Any? = nil, + messages: [String]? = nil, + debugLogs: [String] = [], + error: ErrorInfo? = nil + ) { self.success = success self.data = data.map(AnyCodable.init) self.messages = messages - self.debug_logs = debugLogs + debug_logs = debugLogs self.error = error } } diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index 4752482..0c1d318 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -9,7 +9,7 @@ struct ListCommand: AsyncParsableCommand { subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self], defaultSubcommand: AppsSubcommand.self ) - + func run() async throws { // Root command doesn't do anything, subcommands handle everything } diff --git a/peekaboo-cli/Sources/peekaboo/Models.swift b/peekaboo-cli/Sources/peekaboo/Models.swift index 425ee34..6d2b7b4 100644 --- a/peekaboo-cli/Sources/peekaboo/Models.swift +++ b/peekaboo-cli/Sources/peekaboo/Models.swift @@ -61,10 +61,10 @@ struct WindowBounds: Codable, Sendable { let y_coordinate: Int let width: Int let height: Int - + private enum CodingKeys: String, CodingKey { - case x_coordinate = "x_coordinate" - case y_coordinate = "y_coordinate" + case x_coordinate + case y_coordinate case width case height } @@ -149,7 +149,8 @@ enum CaptureError: Error, LocalizedError, Sendable { if !availableTitles.isEmpty { message += " Available windows: \(availableTitles)." } - message += " Note: For URLs, try without the protocol (e.g., 'example.com:8080' instead of 'http://example.com:8080')." + message += + " Note: For URLs, try without the protocol (e.g., 'example.com:8080' instead of 'http://example.com:8080')." return message case let .windowCaptureFailed(underlyingError): var message = "Failed to capture the specified window." diff --git a/peekaboo-cli/Sources/peekaboo/OutputPathResolver.swift b/peekaboo-cli/Sources/peekaboo/OutputPathResolver.swift index d2a01e0..15bd8ae 100644 --- a/peekaboo-cli/Sources/peekaboo/OutputPathResolver.swift +++ b/peekaboo-cli/Sources/peekaboo/OutputPathResolver.swift @@ -2,7 +2,7 @@ import Foundation struct OutputPathResolver: Sendable { static func getOutputPath(basePath: String?, fileName: String, screenIndex: Int? = nil) -> String { - if let basePath = basePath { + if let basePath { validatePath(basePath) return determineOutputPath(basePath: basePath, fileName: fileName, screenIndex: screenIndex) } else { @@ -11,7 +11,7 @@ struct OutputPathResolver: Sendable { } static func getOutputPathWithFallback(basePath: String?, fileName: String) -> String { - if let basePath = basePath { + if let basePath { validatePath(basePath) return determineOutputPathWithFallback(basePath: basePath, fileName: fileName) } else { @@ -118,17 +118,17 @@ struct OutputPathResolver: Sendable { return "\(basePath)/\(fileName)" } } - + private static func validatePath(_ path: String) { // Check for path traversal attempts if path.contains("../") || path.contains("..\\") { // Logger.shared.debug("Potential path traversal detected in path: \(path)") } - + // Check for system-sensitive paths let sensitivePathPrefixes = ["/etc/", "/usr/", "/bin/", "/sbin/", "/System/", "/Library/System/"] let normalizedPath = (path as NSString).standardizingPath - + for prefix in sensitivePathPrefixes where normalizedPath.hasPrefix(prefix) { // Logger.shared.debug("Path points to system directory: \(path) -> \(normalizedPath)") break diff --git a/peekaboo-cli/Sources/peekaboo/ScreenCapture.swift b/peekaboo-cli/Sources/peekaboo/ScreenCapture.swift index d43cf1e..37be2b4 100644 --- a/peekaboo-cli/Sources/peekaboo/ScreenCapture.swift +++ b/peekaboo-cli/Sources/peekaboo/ScreenCapture.swift @@ -1,5 +1,5 @@ -import Foundation import CoreGraphics +import Foundation @preconcurrency import ScreenCaptureKit struct ScreenCapture: Sendable { diff --git a/peekaboo-cli/Sources/peekaboo/main.swift b/peekaboo-cli/Sources/peekaboo/main.swift index 411558e..d0d60e5 100644 --- a/peekaboo-cli/Sources/peekaboo/main.swift +++ b/peekaboo-cli/Sources/peekaboo/main.swift @@ -11,8 +11,8 @@ struct PeekabooCommand: AsyncParsableCommand { subcommands: [ImageCommand.self, ListCommand.self], defaultSubcommand: ImageCommand.self ) - + func run() async throws { // Root command doesn't do anything, subcommands handle everything } -} \ No newline at end of file +} diff --git a/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift b/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift index 5996c6f..2347aca 100644 --- a/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ApplicationFinderTests.swift @@ -423,14 +423,14 @@ struct ApplicationFinderEdgeCaseTests { } } } - + // MARK: - Browser Helper Filtering Tests - + @Test("Browser helper filtering for Chrome searches", .tags(.browserFiltering)) func browserHelperFilteringChrome() { // Test that Chrome helper processes are filtered out when searching for "chrome" // Note: This test documents expected behavior even when Chrome isn't running - + do { let result = try ApplicationFinder.findApplication(identifier: "chrome") // If found, should be the main Chrome app, not a helper @@ -446,11 +446,11 @@ struct ApplicationFinderEdgeCaseTests { print("Chrome not found, which is acceptable for browser helper filtering test") } } - + @Test("Browser helper filtering for Safari searches", .tags(.browserFiltering)) func browserHelperFilteringSafari() { // Test that Safari helper processes are filtered out when searching for "safari" - + do { let result = try ApplicationFinder.findApplication(identifier: "safari") // If found, should be the main Safari app, not a helper @@ -465,14 +465,14 @@ struct ApplicationFinderEdgeCaseTests { print("Safari not found, which is acceptable for browser helper filtering test") } } - + @Test("Non-browser searches should not filter helpers", .tags(.browserFiltering)) func nonBrowserSearchesPreserveHelpers() { // Test that non-browser searches still find helper processes if that's what's being searched for - + // This tests that helper filtering only applies to browser identifiers let nonBrowserIdentifiers = ["finder", "textedit", "calculator", "activity monitor"] - + for identifier in nonBrowserIdentifiers { do { let result = try ApplicationFinder.findApplication(identifier: identifier) @@ -484,13 +484,13 @@ struct ApplicationFinderEdgeCaseTests { } } } - + @Test("Browser error messages are more specific", .tags(.browserFiltering)) func browserSpecificErrorMessages() { // Test that browser-specific error messages are provided when browsers aren't found - + let browserIdentifiers = ["chrome", "firefox", "edge"] - + for browser in browserIdentifiers { do { _ = try ApplicationFinder.findApplication(identifier: browser) diff --git a/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift b/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift index 29fe11d..821fae3 100644 --- a/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ImageCommandTests.swift @@ -263,7 +263,6 @@ struct ImageCommandTests { #expect(command.screenIndex == index) } - @Test( "Window index boundary values", arguments: [0, 1, 10, 9999] @@ -273,7 +272,6 @@ struct ImageCommandTests { #expect(command.windowIndex == index) } - @Test("Error handling for invalid combinations", .tags(.fast)) func invalidCombinations() { // Window capture without app should fail in execution @@ -340,7 +338,11 @@ struct ImageCommandPathHandlingTests { func singleScreenFilePath() { // For single screen, should use exact path let fileName = "screen_1_20250608_120000.png" - let result = OutputPathResolver.determineOutputPath(basePath: "/tmp/my-screenshot.png", fileName: fileName, screenIndex: 0) + let result = OutputPathResolver.determineOutputPath( + basePath: "/tmp/my-screenshot.png", + fileName: fileName, + screenIndex: 0 + ) #expect(result == "/tmp/my-screenshot.png") } @@ -575,7 +577,10 @@ struct ImageCommandErrorHandlingTests { // This test validates the logic without actually creating directories let fileName = "screen_1_20250608_120001.png" - let result = OutputPathResolver.determineOutputPath(basePath: "/tmp/test-path-creation/file.png", fileName: fileName) + let result = OutputPathResolver.determineOutputPath( + basePath: "/tmp/test-path-creation/file.png", + fileName: fileName + ) // Should return the intended path even if directory creation might fail #expect(result == "/tmp/test-path-creation/file_1_20250608_120001.png") diff --git a/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift b/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift index b78398b..14ff023 100644 --- a/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/JSONOutputTests.swift @@ -12,7 +12,7 @@ struct JSONOutputTests { struct TestWrapper: Codable { let value: AnyCodable } - + // Test string let stringWrapper = TestWrapper(value: AnyCodable("test string")) let stringData = try JSONEncoder().encode(stringWrapper) diff --git a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift index 28872c5..a5cedcd 100644 --- a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift @@ -139,21 +139,21 @@ struct ScreenshotValidationTests { // 1. The physical pixel dimensions of the display // 2. How macOS reports display information // 3. Whether the display is Retina or not - // + // // Instead of trying to match exact dimensions, verify: // - The image has reasonable dimensions // - The aspect ratio is preserved - + #expect(image.size.width > 0) #expect(image.size.height > 0) #expect(image.size.width <= 8192) // Max reasonable display width #expect(image.size.height <= 8192) // Max reasonable display height - + // Verify aspect ratio is reasonable (between 1:3 and 3:1) let aspectRatio = image.size.width / image.size.height #expect(aspectRatio > 0.33) #expect(aspectRatio < 3.0) - + print("Display \(index): captured \(image.size.width)x\(image.size.height)") } } catch { @@ -204,7 +204,7 @@ struct ScreenshotValidationTests { // - Whether screen recording permission dialogs appear #expect(averageTime < 1.5) // Average should be under 1.5 seconds #expect(maxTime < 3.0) // Max should be under 3 seconds - + // Performance benchmarks on typical hardware: // - Single 1080p display: ~100-200ms // - Single 4K display: ~300-500ms @@ -315,18 +315,18 @@ struct ScreenshotValidationTests { format: ImageFormat ) async throws -> ImageCaptureData { let availableContent = try await SCShareableContent.current - + guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else { throw CaptureError.captureCreationFailed(nil) } - + let filter = SCContentFilter(display: scDisplay, excludingWindows: []) - + let configuration = SCStreamConfiguration() configuration.backgroundColor = .clear configuration.shouldBeOpaque = true configuration.showsCursor = false - + let image = try await SCScreenshotManager.captureImage( contentFilter: filter, configuration: configuration