Skip to content

Commit fcbb01b

Browse files
committed
Switch key stroke observation to use defaultTap
1 parent dc0f27d commit fcbb01b

File tree

5 files changed

+58
-49
lines changed

5 files changed

+58
-49
lines changed

Core/Sources/CGEventObserver/CGEventObserver.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,39 +6,50 @@ public protocol CGEventObserverType {
66
@discardableResult
77
func activateIfPossible() -> Bool
88
func deactivate()
9-
var stream: AsyncStream<CGEvent> { get }
9+
func createStream() -> AsyncStream<CGEvent>
1010
var isEnabled: Bool { get }
1111
}
1212

1313
public final class CGEventObserver: CGEventObserverType {
14-
public let stream: AsyncStream<CGEvent>
1514
public var isEnabled: Bool { port != nil }
1615

17-
private var continuation: AsyncStream<CGEvent>.Continuation
16+
private var continuations: [UUID: AsyncStream<CGEvent>.Continuation] = [:]
1817
private var port: CFMachPort?
1918
private let eventsOfInterest: Set<CGEventType>
2019
private let tapLocation: CGEventTapLocation = .cghidEventTap
2120
private let tapPlacement: CGEventTapPlacement = .tailAppendEventTap
22-
private let tapOptions: CGEventTapOptions = .listenOnly
23-
private var retryTask: Task<Void, Error>?
21+
private let tapOptions: CGEventTapOptions = .defaultTap
2422

2523
deinit {
26-
continuation.finish()
24+
for continuation in continuations {
25+
continuation.value.finish()
26+
}
2727
CFMachPortInvalidate(port)
2828
}
2929

3030
public init(eventsOfInterest: Set<CGEventType>) {
3131
self.eventsOfInterest = eventsOfInterest
32-
var continuation: AsyncStream<CGEvent>.Continuation!
33-
stream = AsyncStream { c in
34-
continuation = c
32+
}
33+
34+
public func createStream() -> AsyncStream<CGEvent> {
35+
.init { continuation in
36+
let id = UUID()
37+
addContinuation(continuation, id: id)
38+
continuation.onTermination = { [weak self] _ in
39+
self?.removeContinuation(id: id)
40+
}
3541
}
36-
self.continuation = continuation
42+
}
43+
44+
private func addContinuation(_ continuation: AsyncStream<CGEvent>.Continuation, id: UUID) {
45+
continuations[id] = continuation
46+
}
47+
48+
private func removeContinuation(id: UUID) {
49+
continuations[id] = nil
3750
}
3851

3952
public func deactivate() {
40-
retryTask?.cancel()
41-
retryTask = nil
4253
guard let port else { return }
4354
os_log(.info, "CGEventObserver deactivated.")
4455
CFMachPortInvalidate(port)
@@ -56,7 +67,7 @@ public final class CGEventObserver: CGEventObserverType {
5667
tapProxy _: CGEventTapProxy,
5768
eventType: CGEventType,
5869
event: CGEvent,
59-
continuationPointer: UnsafeMutableRawPointer?
70+
continuationsPointer: UnsafeMutableRawPointer?
6071
) -> Unmanaged<CGEvent>? {
6172
guard AXIsProcessTrusted() else {
6273
return .passRetained(event)
@@ -66,10 +77,12 @@ public final class CGEventObserver: CGEventObserverType {
6677
return .passRetained(event)
6778
}
6879

69-
if let continuation = continuationPointer?
70-
.assumingMemoryBound(to: AsyncStream<CGEvent>.Continuation.self)
80+
if let continuations = continuationsPointer?
81+
.assumingMemoryBound(to: [UUID: AsyncStream<CGEvent>.Continuation].self)
7182
{
72-
continuation.pointee.yield(event)
83+
for continuation in continuations.pointee {
84+
continuation.value.yield(event)
85+
}
7386
}
7487

7588
return .passRetained(event)
@@ -79,7 +92,7 @@ public final class CGEventObserver: CGEventObserverType {
7992
let tapPlacement = tapPlacement
8093
let tapOptions = tapOptions
8194

82-
guard let port = withUnsafeMutablePointer(to: &continuation, { pointer in
95+
guard let port = withUnsafeMutablePointer(to: &continuations, { pointer in
8396
CGEvent.tapCreate(
8497
tap: tapLocation,
8598
place: tapPlacement,
@@ -89,11 +102,6 @@ public final class CGEventObserver: CGEventObserverType {
89102
userInfo: pointer
90103
)
91104
}) else {
92-
retryTask = Task {
93-
try? await Task.sleep(nanoseconds: 2_000_000_000)
94-
try Task.checkCancellation()
95-
activateIfPossible()
96-
}
97105
return false
98106
}
99107
self.port = port

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ public class RealtimeSuggestionController {
6666
os_log(.info, "Add auto trigger listener: %@.", listener as CVarArg)
6767

6868
if task == nil {
69-
task = Task { [stream = eventObserver.stream] in
70-
for await event in stream {
69+
task = Task { [weak self, eventObserver] in
70+
for await event in eventObserver.createStream() {
71+
guard let self else { return }
7172
await self.handleHIDEvent(event: event)
7273
}
7374
}

DEVELOPMENT.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,44 @@
44

55
### Copilot for Xcode
66

7-
Copilot for Xcode is the app containing both the XPCService and the editor extension.
7+
Copilot for Xcode is the host app containing both the XPCService and the editor extension.
88

99
### EditorExtension
1010

11-
As its name suggests, the editor extension. Since an editor extension must be sandboxed, it will need to talk to a trusted non-sandboxed XPCService to break out the limitations. The identifier of the XPCService must be listed under `com.apple.security.temporary-exception.mach-lookup.global-name` in entitlements.
11+
As its name suggests, the editor extension. Its sole purpose is to forward editor content to the XPCService for processing, and update the editor with the returned content. Due to the sandboxing requirements for editor extensions, it has to communicate with a trusted, non-sandboxed XPCService to bypass the limitations. The XPCService identifier must be included in the `com.apple.security.temporary-exception.mach-lookup.global-name` entitlements.
1212

1313
### ExtensionService
1414

15-
The ExtensionService is a program that runs in the background and does basically everything. It redirects the requests from EditorExtension to `CopilotService` and returns the updated code back to the extension.
15+
The `ExtensionService` is a program that operates in the background and performs a wide range of tasks. It redirects requests from the `EditorExtension` to the `CopilotService` and returns the updated code back to the extension, or presents it in a GUI outside of Xcode.
1616

17-
Since the Xcode source editor extension only allows its commands to be triggered manually, the ExtensionService has to use Apple Scripts to trigger the menu items to generate real-time suggestions.
17+
### Core
1818

19-
The ExtensionService is also using a lot of Apple Script tricks to get the file paths and project/workspace paths of the active Xcode window because Xcode is not providing this information.
19+
Most of the logics are implemented inside the package `Core`.
20+
21+
- The `CopilotService` is responsible for communicating with the GitHub Copilot LSP.
22+
- The `Service` is responsible for handling the requests from the `EditorExtension`, communicating with the `CopilotService`, update the code blocks and present the GUI.
23+
- The `Client` is basically just a wrapper around the XPCService
24+
- The `SuggestionInjector` is responsible for injecting the suggestions into the code. Used in comment mode to present the suggestions, and all modes to accept suggestions.
25+
- The `Environment` contains some swappable global functions. It is used to make testing easier.
26+
- The `SuggestionWidget` is responsible for presenting the suggestions in floating widget mode.
2027

2128
## Building and Archiving the App
2229

23-
Firstly, create a `Secrets.xcconfig` next to the others. You can provide a `GITHUB_TOKEN` here if you want to check for updates.
30+
To get started, create a `Secrets.xcconfig` file at the root directory. You can use the `Secrets.sample.xcconfig` as a reference.
2431

25-
This project contains a Git submodule `copilot.vim`, so you will have to initialize the submodule or download it from [copilot.vim](https://github.com/github/copilot.vim).
32+
This project includes a Git submodule, `copilot.vim`, so you will need to either initialize the submodule or download it from the [copilot.vim](https://github.com/github/copilot.vim) repository.
2633

27-
Then archive the target Copilot for Xcode.
34+
Finally, archive the Copilot for Xcode target.
2835

2936
## Testing Extension
3037

31-
### Testing Real-time Suggestions Commands
38+
Just run both the `ExtensionService` and the `EditorExtension` Target.
3239

33-
Testing Real-time Suggestions is a little bit different because the Apple Script can't find the commands when debugging the extension in Xcode. Instead, you will have to archive the debug version of the app, run the XPCService target simultaneously and use them against each other.
40+
## Unit Tests
3441

35-
### Testing Other Commands
42+
To run unit tests, just run test from the `Copilot for Xcode` target.
3643

37-
Just run both the XPCService and the EditorExtension Target.
44+
For new tests, they should be added to the `TestPlan.xctestplan`.
3845

3946
## Code Style
4047

README.md

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ Thanks to [LSP-copilot](https://github.com/TerminalFi/LSP-copilot) for showing t
2222
- [Limitations](#limitations)
2323
- [FAQ](#faq)
2424
- [License](#license)
25-
- [Development](DEVELOPMENT.md)
25+
26+
27+
For development instruction, check [Development.md](DEVELOPMENT.md).
2628

2729
## Prerequisites
2830

@@ -32,15 +34,10 @@ Thanks to [LSP-copilot](https://github.com/TerminalFi/LSP-copilot) for showing t
3234

3335
## Permissions Required
3436

35-
- Accessibility API
3637
- Folder Access
37-
- Input Monitoring (for real-time suggestions cancellation by pressing esc, arrow keys or clicking the mouse)
38+
- Accessibility API
3839

39-
> You no longer require Input Monitoring to utilize real-time suggestions with version 0.8.1+. If you do not need the cancellation feature, you can simply ignore this permission.
40-
>
41-
> However, to be frank, the Accessibility API is more dangerous than Input Monitoring. To remove the need for Input Monitoring, we can simply change the `CGEventTapOptions` from `listenOnly` to `defaultTap`, which allows for input manipulation.
42-
>
43-
> If you are concerned about key logging and cannot trust the binary, we recommend examining the code and building it yourself. To address any concerns, you can specifically search for `CGEvent.tapCreate`, `AXObserver`, `AX___` within the code.
40+
> If you are concerned about key logging and cannot trust the binary, we recommend examining the code and [building it yourself](DEVELOPMENT.md). To address any concerns, you can specifically search for `CGEvent.tapCreate`, `AXObserver`, `AX___` within the code.
4441
4542
## Installation and Setup
4643

@@ -72,18 +69,14 @@ Then set it up with the following steps:
7269

7370
### Granting Permissions to the App
7471

75-
The first time the app is open and command run, the extension will ask for the necessary permissions. (except Input Monitoring, you have to enable it manually)
72+
The first time the app is open and command run, the extension will ask for the necessary permissions.
7673

7774
Alternatively, you may manually grant the required permissions by navigating to the `Privacy & Security` tab in the `System Settings.app`.
7875

7976
- To grant permissions for the Accessibility API, click `Accessibility`, and drag `CopilotForXcodeExtensionService.app` to the list. You can locate the extension app by clicking `Reveal Extension App in Finder` in the host app.
8077

8178
<img alt="Accessibility API" src="/accessibility_api_permission.png" width="500px">
8279

83-
- To enable Input Monitoring (for real-time suggestions cancellation by pressing esc, arrow keys or clicking the mouse), click `Input Monitoring`, and drag `CopilotForXcodeExtensionService.app` to the list. You can locate the extension app by clicking `Reveal Extension App in Finder` in the host app.
84-
85-
<img alt="Input Monitoring" src="/input_monitoring_permission.png" width="500px">
86-
8780
If you encounter an alert requesting permission that you have previously granted, please remove the permission from the list and add it again to re-grant the necessary permissions.
8881

8982
### Managing `CopilotForXcodeExtensionService.app`

input_monitoring_permission.png

-48.7 KB
Binary file not shown.

0 commit comments

Comments
 (0)