mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
feat: Hide window count for single-window apps (PR #6)
- Only show window count when it's not 1 in list apps output - Extract formatApplicationList method for better testability - Fix Swift test compatibility with new CaptureError signatures - Add comprehensive test coverage for window count display logic This improves readability by reducing visual clutter for the common case of apps with single windows. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f3c3cbb073
commit
c6148849f8
5 changed files with 244 additions and 22 deletions
|
|
@ -103,6 +103,27 @@ struct ImageCommand: ParsableCommand {
|
||||||
.unknownError(error.localizedDescription)
|
.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 {
|
if jsonOutput {
|
||||||
let code: ErrorCode = switch captureError {
|
let code: ErrorCode = switch captureError {
|
||||||
case .screenRecordingPermissionDenied:
|
case .screenRecordingPermissionDenied:
|
||||||
|
|
@ -122,10 +143,22 @@ struct ImageCommand: ParsableCommand {
|
||||||
default:
|
default:
|
||||||
.CAPTURE_FAILED
|
.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(
|
outputError(
|
||||||
message: captureError.localizedDescription,
|
message: captureError.localizedDescription,
|
||||||
code: code,
|
code: code,
|
||||||
details: "Image capture operation failed"
|
details: details ?? "Image capture operation failed"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
var localStandardErrorStream = FileHandleTextOutputStream(FileHandle.standardError)
|
var localStandardErrorStream = FileHandleTextOutputStream(FileHandle.standardError)
|
||||||
|
|
@ -214,7 +247,15 @@ struct ImageCommand: ParsableCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureApplicationWindow(_ appIdentifier: String) throws -> [SavedFile] {
|
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) {
|
if captureFocus == .foreground || (captureFocus == .auto && !targetApp.isActive) {
|
||||||
try PermissionsChecker.requireAccessibilityPermission()
|
try PermissionsChecker.requireAccessibilityPermission()
|
||||||
|
|
@ -260,7 +301,15 @@ struct ImageCommand: ParsableCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func captureAllApplicationWindows(_ appIdentifier: String) throws -> [SavedFile] {
|
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) {
|
if captureFocus == .foreground || (captureFocus == .auto && !targetApp.isActive) {
|
||||||
try PermissionsChecker.requireAccessibilityPermission()
|
try PermissionsChecker.requireAccessibilityPermission()
|
||||||
|
|
@ -324,7 +373,7 @@ struct ImageCommand: ParsableCommand {
|
||||||
if isScreenRecordingPermissionError(error) {
|
if isScreenRecordingPermissionError(error) {
|
||||||
throw CaptureError.screenRecordingPermissionDenied
|
throw CaptureError.screenRecordingPermissionDenied
|
||||||
}
|
}
|
||||||
throw CaptureError.captureCreationFailed
|
throw CaptureError.captureCreationFailed(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +384,7 @@ struct ImageCommand: ParsableCommand {
|
||||||
|
|
||||||
// Find the display by ID
|
// Find the display by ID
|
||||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
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
|
// Create content filter for the entire display
|
||||||
|
|
@ -392,7 +441,7 @@ struct ImageCommand: ParsableCommand {
|
||||||
if isScreenRecordingPermissionError(error) {
|
if isScreenRecordingPermissionError(error) {
|
||||||
throw CaptureError.screenRecordingPermissionDenied
|
throw CaptureError.screenRecordingPermissionDenied
|
||||||
}
|
}
|
||||||
throw CaptureError.windowCaptureFailed
|
throw CaptureError.windowCaptureFailed(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,18 +74,27 @@ struct AppsSubcommand: ParsableCommand {
|
||||||
Foundation.exit(captureError.exitCode)
|
Foundation.exit(captureError.exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func printApplicationList(_ applications: [ApplicationInfo]) {
|
internal func printApplicationList(_ applications: [ApplicationInfo]) {
|
||||||
print("Running Applications (\(applications.count)):")
|
let output = formatApplicationList(applications)
|
||||||
print()
|
print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal func formatApplicationList(_ applications: [ApplicationInfo]) -> String {
|
||||||
|
var output = "Running Applications (\(applications.count)):\n\n"
|
||||||
|
|
||||||
for (index, app) in applications.enumerated() {
|
for (index, app) in applications.enumerated() {
|
||||||
print("\(index + 1). \(app.app_name)")
|
output += "\(index + 1). \(app.app_name)\n"
|
||||||
print(" Bundle ID: \(app.bundle_id)")
|
output += " Bundle ID: \(app.bundle_id)\n"
|
||||||
print(" PID: \(app.pid)")
|
output += " PID: \(app.pid)\n"
|
||||||
print(" Status: \(app.is_active ? "Active" : "Background")")
|
output += " Status: \(app.is_active ? "Active" : "Background")\n"
|
||||||
print(" Windows: \(app.window_count)")
|
// Only show window count if it's not 1
|
||||||
print()
|
if app.window_count != 1 {
|
||||||
|
output += " Windows: \(app.window_count)\n"
|
||||||
|
}
|
||||||
|
output += "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,170 @@ struct ListCommandTests {
|
||||||
let data = try encoder.encode(appData)
|
let data = try encoder.encode(appData)
|
||||||
#expect(!data.isEmpty)
|
#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
|
// MARK: - Extended List Command Tests
|
||||||
|
|
|
||||||
|
|
@ -272,9 +272,9 @@ struct ModelsTests {
|
||||||
.contains("Screen recording permission is required")
|
.contains("Screen recording permission is required")
|
||||||
)
|
)
|
||||||
#expect(CaptureError.invalidDisplayID.errorDescription == "Invalid display ID provided.")
|
#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.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)
|
let fileError = CaptureError.fileWriteError("/tmp/test.png", nil)
|
||||||
#expect(fileError.errorDescription?
|
#expect(fileError.errorDescription?
|
||||||
.starts(with: "Failed to write capture file to path: /tmp/test.png.") == true
|
.starts(with: "Failed to write capture file to path: /tmp/test.png.") == true
|
||||||
|
|
@ -292,9 +292,9 @@ struct ModelsTests {
|
||||||
(.screenRecordingPermissionDenied, 11),
|
(.screenRecordingPermissionDenied, 11),
|
||||||
(.accessibilityPermissionDenied, 12),
|
(.accessibilityPermissionDenied, 12),
|
||||||
(.invalidDisplayID, 13),
|
(.invalidDisplayID, 13),
|
||||||
(.captureCreationFailed, 14),
|
(.captureCreationFailed(nil), 14),
|
||||||
(.windowNotFound, 15),
|
(.windowNotFound, 15),
|
||||||
(.windowCaptureFailed, 16),
|
(.windowCaptureFailed(nil), 16),
|
||||||
(.fileWriteError("test", nil), 17),
|
(.fileWriteError("test", nil), 17),
|
||||||
(.appNotFound("test"), 18),
|
(.appNotFound("test"), 18),
|
||||||
(.invalidWindowIndex(0), 19),
|
(.invalidWindowIndex(0), 19),
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ struct ScreenshotValidationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let image = capturedImage else {
|
guard let image = capturedImage else {
|
||||||
throw CaptureError.windowCaptureFailed
|
throw CaptureError.windowCaptureFailed(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
@ -311,7 +311,7 @@ struct ScreenshotValidationTests {
|
||||||
format: ImageFormat
|
format: ImageFormat
|
||||||
) throws -> ImageCaptureData {
|
) throws -> ImageCaptureData {
|
||||||
guard let image = CGDisplayCreateImage(displayID) else {
|
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))
|
let nsImage = NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue