Skip to content

Commit 38c641c

Browse files
committed
Replacing the display link with an AXObserver solution
For lower CPU usage
1 parent 9030e70 commit 38c641c

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

Core/Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ let package = Package(
5858
"CGEventObserver",
5959
"DisplayLink",
6060
"ActiveApplicationMonitor",
61+
"AXNotificationStream",
6162
]
6263
),
6364
.target(
@@ -72,5 +73,6 @@ let package = Package(
7273
.target(name: "LaunchAgentManager"),
7374
.target(name: "DisplayLink"),
7475
.target(name: "ActiveApplicationMonitor"),
76+
.target(name: "AXNotificationStream"),
7577
]
7678
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import AppKit
2+
import ApplicationServices
3+
import Foundation
4+
5+
public final class AXNotificationStream: AsyncSequence {
6+
public typealias Stream = AsyncStream<Element>
7+
public typealias Continuation = Stream.Continuation
8+
public typealias AsyncIterator = Stream.AsyncIterator
9+
public typealias Element = (String, CFDictionary)
10+
11+
private var continuation: Continuation
12+
private let stream: Stream
13+
14+
public func makeAsyncIterator() -> Stream.AsyncIterator {
15+
stream.makeAsyncIterator()
16+
}
17+
18+
deinit {
19+
continuation.finish()
20+
}
21+
22+
public init(
23+
app: NSRunningApplication,
24+
element: AXUIElement? = nil,
25+
notificationNames: String...
26+
) {
27+
var cont: Continuation!
28+
stream = Stream { continuation in
29+
cont = continuation
30+
}
31+
continuation = cont
32+
var observer: AXObserver?
33+
34+
func callback(
35+
observer: AXObserver,
36+
element: AXUIElement,
37+
notificationName: CFString,
38+
userInfo: CFDictionary,
39+
pointer: UnsafeMutableRawPointer?
40+
) {
41+
guard let pointer = pointer?.assumingMemoryBound(to: Continuation.self)
42+
else { return }
43+
pointer.pointee.yield((notificationName as String, userInfo))
44+
}
45+
46+
_ = AXObserverCreateWithInfoCallback(
47+
app.processIdentifier,
48+
callback,
49+
&observer
50+
)
51+
guard let observer else {
52+
continuation.finish()
53+
return
54+
}
55+
56+
let observingElement = element ?? AXUIElementCreateApplication(app.processIdentifier)
57+
continuation.onTermination = { @Sendable _ in
58+
for name in notificationNames {
59+
AXObserverRemoveNotification(observer, observingElement, name as CFString)
60+
}
61+
CFRunLoopRemoveSource(
62+
CFRunLoopGetMain(),
63+
AXObserverGetRunLoopSource(observer),
64+
.commonModes
65+
)
66+
}
67+
for name in notificationNames {
68+
AXObserverAddNotification(observer, observingElement, name as CFString, &continuation)
69+
}
70+
CFRunLoopAddSource(
71+
CFRunLoopGetMain(),
72+
AXObserverGetRunLoopSource(observer),
73+
.commonModes
74+
)
75+
}
76+
}

Core/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public final class ActiveApplicationMonitor {
3434
continuation.onTermination = { _ in
3535
ActiveApplicationMonitor.shared.removeContinuation(id: id)
3636
}
37+
continuation.yield(activeApplication)
3738
}
3839
}
3940

Core/Sources/Service/GUI/SuggestionPanelController.swift

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ActiveApplicationMonitor
22
import AppKit
3+
import AXNotificationStream
34
import DisplayLink
45
import SwiftUI
56

@@ -25,25 +26,47 @@ final class SuggestionPanelController {
2526
return it
2627
}()
2728

28-
private var displayLinkTask: Task<Void, Never>?
29-
private var activeApplicationMonitorTask: Task<Void, Never>?
3029
let viewModel = SuggestionPanelViewModel()
30+
31+
private var windowChangeObservationTask: Task<Void, Error>?
32+
private var activeApplicationMonitorTask: Task<Void, Error>?
3133
private var activeApplication: NSRunningApplication? {
3234
ActiveApplicationMonitor.activeApplication
3335
}
3436

3537
nonisolated init() {
3638
Task { @MainActor in
37-
displayLinkTask = Task {
38-
for await _ in DisplayLink.createStream() {
39+
activeApplicationMonitorTask = Task { [weak self] in
40+
guard let self else { return }
41+
var previousApp: NSRunningApplication?
42+
for await app in ActiveApplicationMonitor.createStream() {
43+
try Task.checkCancellation()
44+
defer { previousApp = app }
45+
if let app, app.bundleIdentifier == "com.apple.dt.Xcode" {
46+
if app != previousApp {
47+
windowChangeObservationTask?.cancel()
48+
windowChangeObservationTask = nil
49+
self.observeXcodeWindowChangeIfNeeded()
50+
}
51+
}
52+
3953
self.updateWindowLocation()
4054
}
4155
}
56+
}
57+
}
4258

43-
activeApplicationMonitorTask = Task {
44-
for await _ in ActiveApplicationMonitor.createStream() {
45-
self.updateWindowLocation()
46-
}
59+
private func observeXcodeWindowChangeIfNeeded() {
60+
guard windowChangeObservationTask == nil else { return }
61+
windowChangeObservationTask = Task { [weak self] in
62+
guard let self else { return }
63+
let notifications = AXNotificationStream(
64+
app: activeApplication!,
65+
notificationNames: kAXMovedNotification
66+
)
67+
for await _ in notifications {
68+
try Task.checkCancellation()
69+
self.updateWindowLocation()
4770
}
4871
}
4972
}
@@ -91,6 +114,8 @@ final class SuggestionPanelController {
91114
}
92115
}
93116

117+
#warning("MUSTDO: Update when editing file is changed.")
118+
94119
@MainActor
95120
final class SuggestionPanelViewModel: ObservableObject {
96121
@Published var startLineIndex: Int = 0

0 commit comments

Comments
 (0)