Skip to content

Commit 4b510b9

Browse files
committed
WIP! 2025-04-06T10:23:32Z wip
1 parent f5ec91f commit 4b510b9

File tree

5 files changed

+81
-22
lines changed

5 files changed

+81
-22
lines changed

Sources/AppBundle/getNativeFocusedWindow.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ private var focusedApp: (any AbstractApp)? {
1111
return appForTests
1212
} else {
1313
check(appForTests == nil)
14-
return try await NSWorkspace.shared.frontmostApplication?.macApp
14+
if let frontmostApplication = NSWorkspace.shared.frontmostApplication {
15+
return try await MacApp.getOrRegister(frontmostApplication)
16+
} else {
17+
return nil
18+
}
1519
}
1620
}
1721
}

Sources/AppBundle/layout/refresh.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ private func normalizeContainers() {
174174

175175
@MainActor
176176
private func detectNewAppsAndWindows() async throws {
177-
for app in try await detectNewApps() { // todo parallelize
177+
for app in try await detectNewAppsAndGcDead() { // todo parallelize
178178
for id in try await app.detectNewWindowsAndGetIds() {
179179
_ = try await MacWindow.getOrRegister(windowId: id, macApp: app as! MacApp)
180180
}

Sources/AppBundle/tree/MacApp.swift

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ final class MacApp: AbstractApp {
88
/*conforms*/ let pid: Int32
99
/*conforms*/ let bundleId: String?
1010
let nsApp: NSRunningApplication
11-
private let axApp: ThreadGuardedValue<AXUIElement>
1211
let isZoom: Bool
12+
private let axApp: ThreadGuardedValue<AXUIElement>
1313
private let appAxSubscriptions: ThreadGuardedValue<[AxSubscription]> // keep subscriptions in memory
1414
private let windows: ThreadGuardedValue<[UInt32: AxWindow]> = .init([:])
1515
private var thread: Thread?
@@ -34,7 +34,8 @@ final class MacApp: AbstractApp {
3434
}
3535

3636
@MainActor
37-
static func get(_ nsApp: NSRunningApplication) async throws -> MacApp? {
37+
@discardableResult
38+
static func getOrRegister(_ nsApp: NSRunningApplication) async throws -> MacApp? {
3839
// Don't perceive any of the lock screen windows as real windows
3940
// Otherwise, false positive ax notifications might trigger that lead to gcWindows
4041
if nsApp.bundleIdentifier == lockScreenAppBundleId {
@@ -295,9 +296,67 @@ final class MacApp: AbstractApp {
295296
thread = nil // Disallow all future job submissions
296297
}
297298

298-
private func getThreadOrCancel() throws -> Thread { // todo convert untyped throws to throws across the whole app
299-
if let thread { return thread }
300-
throw CancellationError()
299+
@MainActor
300+
private static func refreshAndGetAliveWindowIds(frontmostAppBundleId: String?) async throws -> [UInt32] {
301+
try await withThrowingTaskGroup(of: Void.self) { group in
302+
// Register new apps
303+
for nsApp in NSWorkspace.shared.runningApplications where nsApp.activationPolicy == .regular {
304+
let succ = group.addTaskUnlessCancelled { @Sendable @MainActor in
305+
_ = try await getOrRegister(nsApp)
306+
}
307+
if !succ { throw CancellationError() }
308+
}
309+
try await group.waitForAll()
310+
}
311+
return try await withThrowingTaskGroup(of: [UInt32].self, returning: [UInt32].self) { group in
312+
var result: [UInt32] = []
313+
// gc dead apps. refresh underlying windows
314+
for (_, app) in MacApp.allAppsMap {
315+
let succ = group.addTaskUnlessCancelled { @Sendable @MainActor in
316+
try await app.refreshAndGetAliveWindowIds(frontmostAppBundleId: frontmostAppBundleId)
317+
}
318+
if !succ { throw CancellationError() }
319+
}
320+
for try await child in group {
321+
result.append(contentsOf: child)
322+
}
323+
return result
324+
}
325+
}
326+
327+
@MainActor
328+
private func refreshAndGetAliveWindowIds(frontmostAppBundleId: String?) async throws -> [UInt32] {
329+
if nsApp.isTerminated {
330+
MacApp.allAppsMap.removeValue(forKey: pid)
331+
thread?.runInLoopAsync { [windows, appAxSubscriptions, axApp] job in
332+
axApp.destroy()
333+
appAxSubscriptions.destroy()
334+
windows.destroy()
335+
CFRunLoopStop(CFRunLoopGetCurrent())
336+
}
337+
thread = nil // Disallow all future job submissions
338+
return []
339+
}
340+
guard let thread else { return [] }
341+
return try await thread.runInLoop { [nsApp, windows, axApp] (job) -> [UInt32] in
342+
var result: [UInt32: AxWindow] = windows.threadGuarded
343+
// Second line of defence against lock screen. See the first line of defence: closedWindowsCache
344+
// Second and third lines of defence are technically needed only to avoid potential flickering
345+
if frontmostAppBundleId != lockScreenAppBundleId {
346+
result = try result.filter {
347+
try job.checkCancellation()
348+
return $0.value.ax.containingWindowId(signpostEvent: nsApp.idForDebug) != nil
349+
}
350+
}
351+
352+
for window in axApp.threadGuarded.get(Ax.windowsAttr, signpostEvent: nsApp.idForDebug) ?? [] {
353+
try job.checkCancellation()
354+
result.getOrRegisterAxWindow(window, nsApp)
355+
}
356+
357+
windows.threadGuarded = result
358+
return Array(result.keys)
359+
}
301360
}
302361

303362
@MainActor // todo swift is stupid
@@ -342,6 +401,7 @@ private class AxWindow {
342401
}
343402

344403
extension [UInt32: AxWindow] {
404+
@discardableResult
345405
fileprivate mutating func getOrRegisterAxWindow(_ axWindow: AXUIElement, _ nsApp: NSRunningApplication) -> AxWindow? {
346406
guard let id = axWindow.containingWindowId() else { return nil }
347407
if let existing = self[id] {
@@ -442,7 +502,4 @@ extension NSRunningApplication {
442502
var idForDebug: String {
443503
"PID: \(processIdentifier) ID: \(bundleIdentifier ?? executableURL?.description ?? "")"
444504
}
445-
446-
@MainActor
447-
var macApp: MacApp? { get async throws { try await MacApp.get(self) } }
448505
}

Sources/AppBundle/tree/MacWindow.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ final class MacWindow: Window {
104104
break // Don't switch back on popup destruction
105105
}
106106
}
107-
if unregisterAxWindow {
107+
if unregisterAxWindow { // todo drop?
108108
macApp.unregisterWindow(windowId)
109109
}
110110
}

Sources/AppBundle/util/appBundleUtil.swift

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,15 @@ extension String? {
6060
}
6161

6262
@MainActor
63-
func detectNewApps() async throws -> [any AbstractApp] {
64-
if isUnitTest {
65-
return appForTests.asList()
66-
}
67-
var result = [any AbstractApp]()
68-
for _app in NSWorkspace.shared.runningApplications where _app.activationPolicy == .regular {
69-
if let app = try await _app.macApp {
70-
result.append(app)
71-
}
72-
}
73-
return result
63+
func detectNewAppsAndGcDead() async throws -> [any AbstractApp] {
64+
dieT()
65+
// var result: [any AbstractApp] = []
66+
// for _app in NSWorkspace.shared.runningApplications where _app.activationPolicy == .regular {
67+
// if let app = try await _app.macApp {
68+
// result.append(app)
69+
// }
70+
// }
71+
// return result
7472
}
7573

7674
@MainActor

0 commit comments

Comments
 (0)