diff --git a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift index a811505..d589a82 100644 --- a/peekaboo-cli/Sources/peekaboo/ImageCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ImageCommand.swift @@ -102,6 +102,27 @@ struct ImageCommand: ParsableCommand { } else { .unknownError(error.localizedDescription) } + + // Log the full error details for debugging + Logger.shared.debug("Image capture error: \(error)") + + // If it's a CaptureError with an underlying error, log that too + switch captureError { + case let .captureCreationFailed(underlyingError): + if let underlying = underlyingError { + Logger.shared.debug("Underlying capture creation error: \(underlying)") + } + case let .windowCaptureFailed(underlyingError): + if let underlying = underlyingError { + Logger.shared.debug("Underlying window capture error: \(underlying)") + } + case let .fileWriteError(_, underlyingError): + if let underlying = underlyingError { + Logger.shared.debug("Underlying file write error: \(underlying)") + } + default: + break + } if jsonOutput { let code: ErrorCode = switch captureError { @@ -122,10 +143,22 @@ struct ImageCommand: ParsableCommand { default: .CAPTURE_FAILED } + + // Provide additional details for app not found errors + var details: String? = nil + if case .appNotFound = captureError { + let runningApps = NSWorkspace.shared.runningApplications + .filter { $0.activationPolicy == .regular } + .compactMap(\.localizedName) + .sorted() + .joined(separator: ", ") + details = "Available applications: \(runningApps)" + } + outputError( message: captureError.localizedDescription, code: code, - details: "Image capture operation failed" + details: details ?? "Image capture operation failed" ) } else { var localStandardErrorStream = FileHandleTextOutputStream(FileHandle.standardError) @@ -214,7 +247,15 @@ struct ImageCommand: ParsableCommand { } private func captureApplicationWindow(_ appIdentifier: String) throws -> [SavedFile] { - let targetApp = try ApplicationFinder.findApplication(identifier: appIdentifier) + let targetApp: NSRunningApplication + do { + targetApp = try ApplicationFinder.findApplication(identifier: appIdentifier) + } catch ApplicationError.notFound(let identifier) { + throw CaptureError.appNotFound(identifier) + } catch ApplicationError.ambiguous(let identifier, let matches) { + let appNames = matches.map { $0.localizedName ?? $0.bundleIdentifier ?? "Unknown" } + throw CaptureError.unknownError("Multiple applications match '\(identifier)': \(appNames.joined(separator: ", "))") + } if captureFocus == .foreground || (captureFocus == .auto && !targetApp.isActive) { try PermissionsChecker.requireAccessibilityPermission() @@ -260,7 +301,15 @@ struct ImageCommand: ParsableCommand { } private func captureAllApplicationWindows(_ appIdentifier: String) throws -> [SavedFile] { - let targetApp = try ApplicationFinder.findApplication(identifier: appIdentifier) + let targetApp: NSRunningApplication + do { + targetApp = try ApplicationFinder.findApplication(identifier: appIdentifier) + } catch ApplicationError.notFound(let identifier) { + throw CaptureError.appNotFound(identifier) + } catch ApplicationError.ambiguous(let identifier, let matches) { + let appNames = matches.map { $0.localizedName ?? $0.bundleIdentifier ?? "Unknown" } + throw CaptureError.unknownError("Multiple applications match '\(identifier)': \(appNames.joined(separator: ", "))") + } if captureFocus == .foreground || (captureFocus == .auto && !targetApp.isActive) { try PermissionsChecker.requireAccessibilityPermission() @@ -324,7 +373,7 @@ struct ImageCommand: ParsableCommand { if isScreenRecordingPermissionError(error) { throw CaptureError.screenRecordingPermissionDenied } - throw CaptureError.captureCreationFailed + throw CaptureError.captureCreationFailed(error) } } @@ -335,7 +384,7 @@ struct ImageCommand: ParsableCommand { // Find the display by ID guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else { - throw CaptureError.captureCreationFailed + throw CaptureError.captureCreationFailed(nil) } // Create content filter for the entire display @@ -392,7 +441,7 @@ struct ImageCommand: ParsableCommand { if isScreenRecordingPermissionError(error) { throw CaptureError.screenRecordingPermissionDenied } - throw CaptureError.windowCaptureFailed + throw CaptureError.windowCaptureFailed(error) } } diff --git a/peekaboo-cli/Sources/peekaboo/ListCommand.swift b/peekaboo-cli/Sources/peekaboo/ListCommand.swift index 9c27ee5..dc98e2c 100644 --- a/peekaboo-cli/Sources/peekaboo/ListCommand.swift +++ b/peekaboo-cli/Sources/peekaboo/ListCommand.swift @@ -74,18 +74,27 @@ struct AppsSubcommand: ParsableCommand { Foundation.exit(captureError.exitCode) } - private func printApplicationList(_ applications: [ApplicationInfo]) { - print("Running Applications (\(applications.count)):") - print() - + internal func printApplicationList(_ applications: [ApplicationInfo]) { + let output = formatApplicationList(applications) + print(output) + } + + internal func formatApplicationList(_ applications: [ApplicationInfo]) -> String { + var output = "Running Applications (\(applications.count)):\n\n" + for (index, app) in applications.enumerated() { - print("\(index + 1). \(app.app_name)") - print(" Bundle ID: \(app.bundle_id)") - print(" PID: \(app.pid)") - print(" Status: \(app.is_active ? "Active" : "Background")") - print(" Windows: \(app.window_count)") - print() + output += "\(index + 1). \(app.app_name)\n" + output += " Bundle ID: \(app.bundle_id)\n" + output += " PID: \(app.pid)\n" + output += " Status: \(app.is_active ? "Active" : "Background")\n" + // Only show window count if it's not 1 + if app.window_count != 1 { + output += " Windows: \(app.window_count)\n" + } + output += "\n" } + + return output } } diff --git a/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift b/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift index 66800e5..7f220c5 100644 --- a/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ListCommandTests.swift @@ -271,6 +271,170 @@ struct ListCommandTests { let data = try encoder.encode(appData) #expect(!data.isEmpty) } + + // MARK: - Window Count Display Tests + + @Test("printApplicationList hides window count when count is 1", .tags(.fast)) + func printApplicationListHidesWindowCountForSingleWindow() throws { + // Create test applications with different window counts + let applications = [ + ApplicationInfo( + app_name: "Single Window App", + bundle_id: "com.test.single", + pid: 123, + is_active: false, + window_count: 1 + ), + ApplicationInfo( + app_name: "Multi Window App", + bundle_id: "com.test.multi", + pid: 456, + is_active: true, + window_count: 5 + ), + ApplicationInfo( + app_name: "No Windows App", + bundle_id: "com.test.none", + pid: 789, + is_active: false, + window_count: 0 + ) + ] + + // Get formatted output using the testable method + let command = AppsSubcommand() + let output = command.formatApplicationList(applications) + + // Verify that "Windows: 1" is NOT present for single window app + #expect(!output.contains("Windows: 1")) + + // Verify that the single window app is listed but without window count + #expect(output.contains("Single Window App")) + + // Verify that "Windows: 5" IS present for multi window app + #expect(output.contains("Windows: 5")) + + // Verify that "Windows: 0" IS present for no windows app + #expect(output.contains("Windows: 0")) + } + + @Test("printApplicationList shows window count for non-1 values", .tags(.fast)) + func printApplicationListShowsWindowCountForNonSingleWindow() throws { + let applications = [ + ApplicationInfo( + app_name: "Zero Windows", + bundle_id: "com.test.zero", + pid: 100, + is_active: false, + window_count: 0 + ), + ApplicationInfo( + app_name: "Two Windows", + bundle_id: "com.test.two", + pid: 200, + is_active: false, + window_count: 2 + ), + ApplicationInfo( + app_name: "Many Windows", + bundle_id: "com.test.many", + pid: 300, + is_active: false, + window_count: 10 + ) + ] + + let command = AppsSubcommand() + let output = command.formatApplicationList(applications) + + // All these should show window counts since they're not 1 + #expect(output.contains("Windows: 0")) + #expect(output.contains("Windows: 2")) + #expect(output.contains("Windows: 10")) + } + + @Test("printApplicationList formats output correctly", .tags(.fast)) + func printApplicationListFormatsOutputCorrectly() throws { + let applications = [ + ApplicationInfo( + app_name: "Test App", + bundle_id: "com.test.app", + pid: 12345, + is_active: true, + window_count: 1 + ) + ] + + let command = AppsSubcommand() + let output = command.formatApplicationList(applications) + + // Verify basic formatting is present + #expect(output.contains("Running Applications (1):")) + #expect(output.contains("1. Test App")) + #expect(output.contains("Bundle ID: com.test.app")) + #expect(output.contains("PID: 12345")) + #expect(output.contains("Status: Active")) + + // Verify "Windows: 1" is NOT present + #expect(!output.contains("Windows: 1")) + } + + @Test("printApplicationList edge cases", .tags(.fast)) + func printApplicationListEdgeCases() throws { + let applications = [ + ApplicationInfo( + app_name: "Edge Case 1", + bundle_id: "com.test.edge1", + pid: 1, + is_active: false, + window_count: 1 + ), + ApplicationInfo( + app_name: "Edge Case 2", + bundle_id: "com.test.edge2", + pid: 2, + is_active: true, + window_count: 1 + ) + ] + + let command = AppsSubcommand() + let output = command.formatApplicationList(applications) + + // Both apps have 1 window, so neither should show "Windows: 1" + #expect(!output.contains("Windows: 1")) + + // But both apps should be listed + #expect(output.contains("Edge Case 1")) + #expect(output.contains("Edge Case 2")) + #expect(output.contains("Status: Background")) + #expect(output.contains("Status: Active")) + } + + @Test("printApplicationList mixed window counts", .tags(.fast)) + func printApplicationListMixedWindowCounts() throws { + let applications = [ + ApplicationInfo(app_name: "App A", bundle_id: "com.a", pid: 1, is_active: false, window_count: 0), + ApplicationInfo(app_name: "App B", bundle_id: "com.b", pid: 2, is_active: false, window_count: 1), + ApplicationInfo(app_name: "App C", bundle_id: "com.c", pid: 3, is_active: false, window_count: 2), + ApplicationInfo(app_name: "App D", bundle_id: "com.d", pid: 4, is_active: false, window_count: 3) + ] + + let command = AppsSubcommand() + let output = command.formatApplicationList(applications) + + // Should show window counts for 0, 2, and 3, but NOT for 1 + #expect(output.contains("Windows: 0")) + #expect(!output.contains("Windows: 1")) + #expect(output.contains("Windows: 2")) + #expect(output.contains("Windows: 3")) + + // All apps should be listed + #expect(output.contains("App A")) + #expect(output.contains("App B")) + #expect(output.contains("App C")) + #expect(output.contains("App D")) + } } // MARK: - Extended List Command Tests diff --git a/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift b/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift index 9d68b92..3b31975 100644 --- a/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ModelsTests.swift @@ -272,9 +272,9 @@ struct ModelsTests { .contains("Screen recording permission is required") ) #expect(CaptureError.invalidDisplayID.errorDescription == "Invalid display ID provided.") - #expect(CaptureError.captureCreationFailed.errorDescription == "Failed to create the screen capture.") + #expect(CaptureError.captureCreationFailed(nil).errorDescription == "Failed to create the screen capture.") #expect(CaptureError.windowNotFound.errorDescription == "The specified window could not be found.") - #expect(CaptureError.windowCaptureFailed.errorDescription == "Failed to capture the specified window.") + #expect(CaptureError.windowCaptureFailed(nil).errorDescription == "Failed to capture the specified window.") let fileError = CaptureError.fileWriteError("/tmp/test.png", nil) #expect(fileError.errorDescription? .starts(with: "Failed to write capture file to path: /tmp/test.png.") == true @@ -292,9 +292,9 @@ struct ModelsTests { (.screenRecordingPermissionDenied, 11), (.accessibilityPermissionDenied, 12), (.invalidDisplayID, 13), - (.captureCreationFailed, 14), + (.captureCreationFailed(nil), 14), (.windowNotFound, 15), - (.windowCaptureFailed, 16), + (.windowCaptureFailed(nil), 16), (.fileWriteError("test", nil), 17), (.appNotFound("test"), 18), (.invalidWindowIndex(0), 19), diff --git a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift index 72e98e2..2640c71 100644 --- a/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift +++ b/peekaboo-cli/Tests/peekabooTests/ScreenshotValidationTests.swift @@ -299,7 +299,7 @@ struct ScreenshotValidationTests { } guard let image = capturedImage else { - throw CaptureError.windowCaptureFailed + throw CaptureError.windowCaptureFailed(nil) } return image @@ -311,7 +311,7 @@ struct ScreenshotValidationTests { format: ImageFormat ) throws -> ImageCaptureData { guard let image = CGDisplayCreateImage(displayID) else { - throw CaptureError.captureCreationFailed + throw CaptureError.captureCreationFailed(nil) } let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))