mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-25 09:25:47 +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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue