Skip to content

Commit 70875fa

Browse files
gi11esclaude
andauthored
fix: remove NSEvent monitor on ProjectPicker close (#59)
* fix: remove NSEvent monitor on ProjectPicker close (#56) Each call to ProjectPicker.show() was adding a local key-down monitor that was never removed, causing monitors to accumulate on every Cmd+O. Store the monitor token and remove it in cancel(), confirm(), and before re-adding in show(). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: handle panel close button in ProjectPicker monitor cleanup Make ProjectPicker the panel's NSWindowDelegate so clicking the close button (X) routes through cancel(), which removes the key monitor and calls completion(nil). Previously, the close button bypassed cleanup entirely. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 64dd588 commit 70875fa

File tree

1 file changed

+24
-3
lines changed

1 file changed

+24
-3
lines changed

Sources/Window/ProjectPicker.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Fuse
33

44
/// A Spotlight-style project picker that appears when creating a new Claude tab.
55
/// Shows recent projects from ~/.claude/projects/, sorted by recency.
6-
class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate {
6+
class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTextFieldDelegate, NSWindowDelegate {
77

88
typealias Completion = (String?) -> Void // nil = cancelled, String = chosen path
99

@@ -17,6 +17,7 @@ class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTex
1717
private var filteredProjects: [(path: String, lastUsed: Date)] = []
1818
private var spotlightSearch: Process?
1919
private var spotlightPipe: Pipe?
20+
private var keyMonitor: Any?
2021
private var excludePaths: Set<String> = []
2122
private let fuse = Fuse(threshold: 0.4)
2223

@@ -61,6 +62,7 @@ class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTex
6162

6263
super.init()
6364

65+
panel.delegate = self
6466
tableView.dataSource = self
6567
tableView.delegate = self
6668
tableView.target = self
@@ -116,8 +118,12 @@ class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTex
116118
panel.makeKeyAndOrderFront(nil)
117119
panel.makeFirstResponder(searchField)
118120

119-
// Monitor for Escape key
120-
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
121+
// Monitor for Escape key — remove any prior monitor to avoid leaks
122+
if let existing = keyMonitor {
123+
NSEvent.removeMonitor(existing)
124+
keyMonitor = nil
125+
}
126+
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
121127
guard let self = self, self.panel.isVisible else { return event }
122128

123129
if event.keyCode == 53 { // Escape
@@ -148,13 +154,15 @@ class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTex
148154

149155
private func cancel() {
150156
cancelSpotlightSearch()
157+
removeKeyMonitor()
151158
panel.orderOut(nil)
152159
completion?(nil)
153160
completion = nil
154161
}
155162

156163
private func confirm() {
157164
cancelSpotlightSearch()
165+
removeKeyMonitor()
158166
let row = tableView.selectedRow
159167
let path: String
160168
if row >= 0, row < filteredProjects.count {
@@ -204,6 +212,19 @@ class ProjectPicker: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSTex
204212
confirm()
205213
}
206214

215+
private func removeKeyMonitor() {
216+
if let monitor = keyMonitor {
217+
NSEvent.removeMonitor(monitor)
218+
keyMonitor = nil
219+
}
220+
}
221+
222+
// MARK: - NSWindowDelegate
223+
224+
func windowWillClose(_ notification: Notification) {
225+
cancel()
226+
}
227+
207228
/// Safely terminate the spotlight search, clearing the readability handler
208229
/// first to prevent NSFileHandleOperationException crashes.
209230
private func cancelSpotlightSearch() {

0 commit comments

Comments
 (0)