Compare commits

...
2 Commits
Author SHA1 Message Date
TZGyn a7cad2fc2f Terminate stale Kapture instances at launch; log topmost windows
A stale copy surviving an update keeps its own hotkey and overlays on
screen, silently eating every click meant for the new instance (matching
the empty session logs). New copies now force-terminate older instances,
and the first window scan logs the topmost on-screen windows so anything
covering the overlays is visible in diagnostics.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 11:59:03 +08:00
TZGyn 69e1a7fccb Show overlays only after activation completes
The local probe confirmed activation is asynchronous on macOS 26:
isActive flips on a later runloop tick, so overlays shown in the same
tick as activate() belong to a still-inactive app and misses clicks.
Delay overlay creation by a tick and log the activation state at show
time.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 11:51:13 +08:00
3 changed files with 32 additions and 16 deletions
+11
View File
@@ -21,6 +21,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
runOCRSelfTest() runOCRSelfTest()
return return
} }
// Single instance: stale copies keep their own hotkey + overlays alive
// after app updates and eat clicks meant for the new instance.
let myPid = ProcessInfo.processInfo.processIdentifier
let others = NSRunningApplication.runningApplications(withBundleIdentifier: "dev.tzgyn.kapture")
.filter { $0.processIdentifier != myPid }
if !others.isEmpty {
CaptureController.log("launch: other Kapture instances pids=\(others.map { String($0.processIdentifier) }.joined(separator: ",")) — terminating them")
for other in others {
other.forceTerminate()
}
}
HotkeyManager.shared.onTrigger = { CaptureController.shared.begin() } HotkeyManager.shared.onTrigger = { CaptureController.shared.begin() }
HotkeyManager.shared.register() HotkeyManager.shared.register()
} }
+19 -14
View File
@@ -61,21 +61,26 @@ final class CaptureController: NSObject {
} }
isSelecting = true isSelecting = true
previousApp = NSWorkspace.shared.frontmostApplication previousApp = NSWorkspace.shared.frontmostApplication
// Force activation and log whether it stuck — inactive apps don't // Activation is ASYNC: isActive only flips on a later runloop tick
// receive overlay clicks. // (verified on macOS 26). Show overlays only once active, otherwise
let activated = NSApp.activate(ignoringOtherApps: true) // the first click can pass through to the windows below.
CaptureController.log("begin: activate(ignoringOtherApps)=\(activated) active=\(NSApp.isActive)") NSApp.activate(ignoringOtherApps: true)
overlays = NSScreen.screens.map { screen in CaptureController.log("begin: activate issued, active=\(NSApp.isActive)")
OverlayWindow( DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
screen: screen, guard let self, self.isSelecting else { return }
onComplete: { [weak self] rect in self?.finishSelection(rect) }, CaptureController.log("begin: active=\(NSApp.isActive) — showing overlays")
onCancel: { [weak self] in self?.cancelSelection() }, self.overlays = NSScreen.screens.map { screen in
onWindowPick: { [weak self] rect in self?.finishWindowPick(rect) } OverlayWindow(
) screen: screen,
onComplete: { [weak self] rect in self?.finishSelection(rect) },
onCancel: { [weak self] in self?.cancelSelection() },
onWindowPick: { [weak self] rect in self?.finishWindowPick(rect) }
)
}
self.overlays.forEach { $0.makeKeyAndOrderFront(nil) }
self.overlayIDs = self.overlays.map { $0.windowNumber }
CaptureController.log("begin: \(self.overlays.count) overlays, ids=\(self.overlayIDs), active=\(NSApp.isActive)")
} }
overlays.forEach { $0.makeKeyAndOrderFront(nil) }
overlayIDs = overlays.map { $0.windowNumber }
CaptureController.log("begin: \(overlays.count) overlays, ids=\(overlayIDs), active=\(NSApp.isActive)")
} }
/// A window was picked from hover — capture it with rounded corners. /// A window was picked from hover — capture it with rounded corners.
+2 -2
View File
@@ -108,9 +108,9 @@ cat > "$APP/Contents/Info.plist" <<'PLIST'
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>Icon</string> <string>Icon</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.6.3</string> <string>1.6.5</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>63</string> <string>65</string>
<key>LSUIElement</key> <key>LSUIElement</key>
<true/> <true/>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>