@lattices/cli 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
|
@@ -36,7 +36,6 @@ final class WindowDragSnapController {
|
|
|
36
36
|
|
|
37
37
|
private var dragCandidate: DragWindowCandidate?
|
|
38
38
|
private var activeSession: DragSession?
|
|
39
|
-
private var overlayPanels: [String: WindowSnapOverlayPanel] = [:]
|
|
40
39
|
private var modifierModeEnabled = false
|
|
41
40
|
private var windowHasMoved = false
|
|
42
41
|
|
|
@@ -203,9 +202,7 @@ final class WindowDragSnapController {
|
|
|
203
202
|
}
|
|
204
203
|
|
|
205
204
|
private func hideOverlays() {
|
|
206
|
-
|
|
207
|
-
panel.orderOut(nil)
|
|
208
|
-
}
|
|
205
|
+
ScreenOverlayCanvasController.shared.removeLayers(owner: .dragSnap)
|
|
209
206
|
}
|
|
210
207
|
|
|
211
208
|
private func captureFocusedWindow(at mouseLocation: NSPoint) -> DragWindowCandidate? {
|
|
@@ -271,7 +268,7 @@ final class WindowDragSnapController {
|
|
|
271
268
|
|
|
272
269
|
var resolved: [ResolvedSnapZone] = []
|
|
273
270
|
for screen in NSScreen.screens {
|
|
274
|
-
let screenID =
|
|
271
|
+
let screenID = ScreenOverlayCanvasController.screenID(for: screen)
|
|
275
272
|
for (zone, placement, triggerFractions, priority) in baseZones {
|
|
276
273
|
let triggerRect = Self.screenRect(for: triggerFractions, on: screen)
|
|
277
274
|
let previewRect = Self.screenRect(fromAX: WindowTiler.tileFrame(for: placement, on: screen))
|
|
@@ -312,17 +309,14 @@ final class WindowDragSnapController {
|
|
|
312
309
|
private func render(zones: [ResolvedSnapZone], hoveredZone: ResolvedSnapZone?) {
|
|
313
310
|
let config = WorkspaceManager.shared.snapZonesConfig
|
|
314
311
|
let grouped = Dictionary(grouping: zones, by: \.screenID)
|
|
315
|
-
|
|
312
|
+
var layers: [ScreenOverlayLayerSnapshot] = []
|
|
316
313
|
|
|
317
314
|
for screen in NSScreen.screens {
|
|
318
|
-
let screenID =
|
|
315
|
+
let screenID = ScreenOverlayCanvasController.screenID(for: screen)
|
|
319
316
|
guard let screenZones = grouped[screenID], !screenZones.isEmpty else { continue }
|
|
320
317
|
|
|
321
|
-
let panel = overlayPanels[screenID] ?? makeOverlayPanel(for: screen)
|
|
322
|
-
panel.setFrame(screen.frame, display: false)
|
|
323
|
-
|
|
324
318
|
let localZones = screenZones.map {
|
|
325
|
-
|
|
319
|
+
ScreenOverlaySnapZone(
|
|
326
320
|
id: $0.id,
|
|
327
321
|
label: $0.label,
|
|
328
322
|
rect: $0.visibleRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY),
|
|
@@ -334,7 +328,7 @@ final class WindowDragSnapController {
|
|
|
334
328
|
? hoveredZone?.previewRect.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY)
|
|
335
329
|
: nil
|
|
336
330
|
|
|
337
|
-
|
|
331
|
+
let payload = ScreenOverlaySnapZonesPayload(
|
|
338
332
|
zones: localZones,
|
|
339
333
|
previewRect: previewRect,
|
|
340
334
|
previewLabel: nil,
|
|
@@ -344,26 +338,20 @@ final class WindowDragSnapController {
|
|
|
344
338
|
cornerRadius: config.cornerRadius ?? SnapZonesConfig.defaults.cornerRadius ?? 18
|
|
345
339
|
)
|
|
346
340
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
341
|
+
layers.append(
|
|
342
|
+
ScreenOverlayLayerSnapshot(
|
|
343
|
+
id: ScreenOverlayLayerID("dragSnap.\(screenID)"),
|
|
344
|
+
owner: .dragSnap,
|
|
345
|
+
screen: .screen(id: screenID),
|
|
346
|
+
zIndex: 100,
|
|
347
|
+
opacity: 1,
|
|
348
|
+
payload: .snapZones(payload),
|
|
349
|
+
expiresAt: nil
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
352
|
}
|
|
353
|
-
}
|
|
354
353
|
|
|
355
|
-
|
|
356
|
-
let panel = WindowSnapOverlayPanel(frame: screen.frame)
|
|
357
|
-
overlayPanels[Self.screenID(for: screen)] = panel
|
|
358
|
-
return panel
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private static func screenID(for screen: NSScreen) -> String {
|
|
362
|
-
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
|
363
|
-
if let number = screen.deviceDescription[key] as? NSNumber {
|
|
364
|
-
return number.stringValue
|
|
365
|
-
}
|
|
366
|
-
return screen.localizedName
|
|
354
|
+
ScreenOverlayCanvasController.shared.replaceLayers(owner: .dragSnap, with: layers)
|
|
367
355
|
}
|
|
368
356
|
|
|
369
357
|
private static func screenRect(for fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on screen: NSScreen) -> CGRect {
|
|
@@ -439,190 +427,3 @@ final class WindowDragSnapController {
|
|
|
439
427
|
Swift.max(lower, Swift.min(upper, value))
|
|
440
428
|
}
|
|
441
429
|
}
|
|
442
|
-
|
|
443
|
-
private final class WindowSnapOverlayPanel: NSPanel {
|
|
444
|
-
let overlayView = WindowSnapOverlayView(frame: .zero)
|
|
445
|
-
|
|
446
|
-
init(frame: CGRect) {
|
|
447
|
-
super.init(
|
|
448
|
-
contentRect: frame,
|
|
449
|
-
styleMask: [.borderless, .nonactivatingPanel],
|
|
450
|
-
backing: .buffered,
|
|
451
|
-
defer: false
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
isOpaque = false
|
|
455
|
-
backgroundColor = .clear
|
|
456
|
-
hasShadow = false
|
|
457
|
-
ignoresMouseEvents = true
|
|
458
|
-
level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
|
|
459
|
-
collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
|
|
460
|
-
isMovable = false
|
|
461
|
-
hidesOnDeactivate = false
|
|
462
|
-
animationBehavior = .none
|
|
463
|
-
overlayView.frame = NSRect(origin: .zero, size: frame.size)
|
|
464
|
-
overlayView.autoresizingMask = [.width, .height]
|
|
465
|
-
contentView = overlayView
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
override var canBecomeKey: Bool { false }
|
|
469
|
-
override var canBecomeMain: Bool { false }
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
private final class WindowSnapOverlayView: NSView {
|
|
473
|
-
struct Zone {
|
|
474
|
-
let id: String
|
|
475
|
-
let label: String
|
|
476
|
-
let rect: CGRect
|
|
477
|
-
let isHovered: Bool
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
struct Model {
|
|
481
|
-
let zones: [Zone]
|
|
482
|
-
let previewRect: CGRect?
|
|
483
|
-
let previewLabel: String?
|
|
484
|
-
let zoneOpacity: CGFloat
|
|
485
|
-
let highlightOpacity: CGFloat
|
|
486
|
-
let previewOpacity: CGFloat
|
|
487
|
-
let cornerRadius: CGFloat
|
|
488
|
-
|
|
489
|
-
static let empty = Model(
|
|
490
|
-
zones: [],
|
|
491
|
-
previewRect: nil,
|
|
492
|
-
previewLabel: nil,
|
|
493
|
-
zoneOpacity: 0.10,
|
|
494
|
-
highlightOpacity: 0.22,
|
|
495
|
-
previewOpacity: 0.18,
|
|
496
|
-
cornerRadius: 18
|
|
497
|
-
)
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
var model: Model = .empty {
|
|
501
|
-
didSet { needsDisplay = true }
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
override init(frame frameRect: NSRect) {
|
|
505
|
-
super.init(frame: frameRect)
|
|
506
|
-
wantsLayer = true
|
|
507
|
-
layer?.backgroundColor = NSColor.clear.cgColor
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
required init?(coder: NSCoder) {
|
|
511
|
-
fatalError("init(coder:) has not been implemented")
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
override func draw(_ dirtyRect: NSRect) {
|
|
515
|
-
NSColor.clear.setFill()
|
|
516
|
-
bounds.fill()
|
|
517
|
-
|
|
518
|
-
for zone in model.zones {
|
|
519
|
-
drawZone(zone)
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if let previewRect = model.previewRect {
|
|
523
|
-
drawPreview(previewRect, label: model.previewLabel)
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private func drawZone(_ zone: Zone) {
|
|
528
|
-
let rect = zone.rect.insetBy(dx: 1.5, dy: 1.5)
|
|
529
|
-
let radius = min(model.cornerRadius, min(rect.width, rect.height) * 0.34)
|
|
530
|
-
let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
|
|
531
|
-
let idleStrength = max(0.35, min(model.zoneOpacity / 0.10, 1.4))
|
|
532
|
-
let hoverStrength = max(0.35, min(model.highlightOpacity / 0.22, 1.4))
|
|
533
|
-
|
|
534
|
-
let shadow = NSShadow()
|
|
535
|
-
shadow.shadowBlurRadius = zone.isHovered ? 18 : 10
|
|
536
|
-
shadow.shadowOffset = NSSize(width: 0, height: -2)
|
|
537
|
-
shadow.shadowColor = NSColor.black.withAlphaComponent(zone.isHovered ? 0.20 : 0.10)
|
|
538
|
-
|
|
539
|
-
NSGraphicsContext.saveGraphicsState()
|
|
540
|
-
shadow.set()
|
|
541
|
-
let baseTop = NSColor(
|
|
542
|
-
calibratedWhite: 0.13,
|
|
543
|
-
alpha: zone.isHovered ? 0.42 * hoverStrength : 0.22 * idleStrength
|
|
544
|
-
)
|
|
545
|
-
let baseBottom = NSColor(
|
|
546
|
-
calibratedWhite: 0.07,
|
|
547
|
-
alpha: zone.isHovered ? 0.34 * hoverStrength : 0.15 * idleStrength
|
|
548
|
-
)
|
|
549
|
-
NSGradient(starting: baseTop, ending: baseBottom)?.draw(in: path, angle: -90)
|
|
550
|
-
NSGraphicsContext.restoreGraphicsState()
|
|
551
|
-
|
|
552
|
-
if zone.isHovered {
|
|
553
|
-
let glowPath = path.copy() as! NSBezierPath
|
|
554
|
-
glowPath.lineWidth = 6
|
|
555
|
-
NSColor(calibratedRed: 0.25, green: 0.84, blue: 0.58, alpha: model.highlightOpacity * 0.28).setStroke()
|
|
556
|
-
glowPath.stroke()
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
path.lineWidth = zone.isHovered ? 1.6 : 1.0
|
|
560
|
-
NSColor(
|
|
561
|
-
calibratedRed: 0.52,
|
|
562
|
-
green: 0.94,
|
|
563
|
-
blue: 0.72,
|
|
564
|
-
alpha: zone.isHovered ? 0.54 * hoverStrength : 0.10 * idleStrength
|
|
565
|
-
).setStroke()
|
|
566
|
-
path.stroke()
|
|
567
|
-
|
|
568
|
-
let lipRect = CGRect(x: rect.minX + 1.5, y: rect.maxY - 2.5, width: rect.width - 3, height: 2)
|
|
569
|
-
if lipRect.width > 0 {
|
|
570
|
-
let lipPath = NSBezierPath(roundedRect: lipRect, xRadius: 1, yRadius: 1)
|
|
571
|
-
NSColor.white.withAlphaComponent(zone.isHovered ? 0.18 : 0.08).setFill()
|
|
572
|
-
lipPath.fill()
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
drawLabel(zone.label, in: rect, emphasized: zone.isHovered)
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
private func drawPreview(_ rect: CGRect, label: String?) {
|
|
579
|
-
let previewRect = rect.insetBy(dx: 10, dy: 10)
|
|
580
|
-
let radius = min(model.cornerRadius, min(previewRect.width, previewRect.height) * 0.14)
|
|
581
|
-
let path = NSBezierPath(roundedRect: previewRect, xRadius: radius, yRadius: radius)
|
|
582
|
-
|
|
583
|
-
NSColor(calibratedWhite: 1.0, alpha: model.previewOpacity * 0.22).setFill()
|
|
584
|
-
path.fill()
|
|
585
|
-
|
|
586
|
-
path.lineWidth = 1.6
|
|
587
|
-
path.setLineDash([10, 8], count: 2, phase: 0)
|
|
588
|
-
NSColor(
|
|
589
|
-
calibratedRed: 0.44,
|
|
590
|
-
green: 0.90,
|
|
591
|
-
blue: 0.68,
|
|
592
|
-
alpha: max(0.34, model.previewOpacity * 3.2)
|
|
593
|
-
).setStroke()
|
|
594
|
-
path.stroke()
|
|
595
|
-
path.setLineDash([], count: 0, phase: 0)
|
|
596
|
-
|
|
597
|
-
let innerPath = NSBezierPath(roundedRect: previewRect.insetBy(dx: 7, dy: 7), xRadius: max(radius - 4, 8), yRadius: max(radius - 4, 8))
|
|
598
|
-
innerPath.lineWidth = 1
|
|
599
|
-
NSColor.white.withAlphaComponent(max(0.08, model.previewOpacity * 1.2)).setStroke()
|
|
600
|
-
innerPath.stroke()
|
|
601
|
-
|
|
602
|
-
if let label {
|
|
603
|
-
let tagRect = CGRect(x: previewRect.minX + 14, y: previewRect.maxY - 34, width: 110, height: 24)
|
|
604
|
-
let tagPath = NSBezierPath(roundedRect: tagRect, xRadius: 12, yRadius: 12)
|
|
605
|
-
NSColor(calibratedWhite: 0.08, alpha: 0.62).setFill()
|
|
606
|
-
tagPath.fill()
|
|
607
|
-
NSColor.white.withAlphaComponent(0.10).setStroke()
|
|
608
|
-
tagPath.lineWidth = 1
|
|
609
|
-
tagPath.stroke()
|
|
610
|
-
drawLabel(label, in: tagRect, emphasized: true)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
private func drawLabel(_ label: String, in rect: CGRect, emphasized: Bool) {
|
|
615
|
-
let font = NSFont.monospacedSystemFont(ofSize: emphasized ? 11 : 10, weight: emphasized ? .semibold : .medium)
|
|
616
|
-
let attributes: [NSAttributedString.Key: Any] = [
|
|
617
|
-
.font: font,
|
|
618
|
-
.foregroundColor: NSColor.white.withAlphaComponent(emphasized ? 0.92 : 0.72),
|
|
619
|
-
]
|
|
620
|
-
let attr = NSAttributedString(string: label.uppercased(), attributes: attributes)
|
|
621
|
-
let size = attr.size()
|
|
622
|
-
let drawPoint = CGPoint(
|
|
623
|
-
x: rect.midX - size.width / 2,
|
|
624
|
-
y: rect.midY - size.height / 2
|
|
625
|
-
)
|
|
626
|
-
attr.draw(at: drawPoint)
|
|
627
|
-
}
|
|
628
|
-
}
|
|
@@ -53,11 +53,10 @@ final class WindowPreviewStore: ObservableObject {
|
|
|
53
53
|
queue.async { [weak self] in
|
|
54
54
|
guard let self else { return }
|
|
55
55
|
|
|
56
|
-
let cgImage =
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[.boundsIgnoreFraming, .nominalResolution]
|
|
56
|
+
let cgImage = WindowCapture.image(
|
|
57
|
+
listOption: .optionIncludingWindow,
|
|
58
|
+
windowID: CGWindowID(wid),
|
|
59
|
+
imageOption: [.boundsIgnoreFraming, .nominalResolution]
|
|
61
60
|
)
|
|
62
61
|
|
|
63
62
|
let image = cgImage.map {
|
|
@@ -641,6 +641,14 @@ enum WindowTiler {
|
|
|
641
641
|
"switchToAdjacentSpace: offset=\(offset) point=\(formatCGPoint(context.point)) displayId=\(context.display.displayId) active=\(context.activeSpaceId) displayCurrent=\(context.display.currentSpaceId) resolved=\(context.currentSpaceId) target=\(targetText) spaces=\(spaces)"
|
|
642
642
|
)
|
|
643
643
|
|
|
644
|
+
if let target = context.target {
|
|
645
|
+
let switched = switchToSpace(spaceId: target.id)
|
|
646
|
+
DiagnosticLog.shared.info("switchToAdjacentSpace: SkyLight \(switched ? "reached" : "missed") target \(target.id)")
|
|
647
|
+
if switched {
|
|
648
|
+
return true
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
644
652
|
if getDisplaySpaces().count == 1 {
|
|
645
653
|
if let finalSpaceId = switchToAdjacentSpaceViaSystemShortcut(
|
|
646
654
|
offset: offset,
|
|
@@ -663,10 +671,7 @@ enum WindowTiler {
|
|
|
663
671
|
DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId), falling back to SkyLight target \(target.id)")
|
|
664
672
|
}
|
|
665
673
|
|
|
666
|
-
|
|
667
|
-
let switched = switchToSpace(spaceId: target.id)
|
|
668
|
-
DiagnosticLog.shared.info("switchToAdjacentSpace: SkyLight \(switched ? "reached" : "missed") target \(target.id)")
|
|
669
|
-
return switched
|
|
674
|
+
return false
|
|
670
675
|
}
|
|
671
676
|
|
|
672
677
|
/// Find a window by its title tag and return its CGWindowID and owner PID
|
|
@@ -2094,16 +2099,17 @@ enum WindowTiler {
|
|
|
2094
2099
|
|
|
2095
2100
|
private static func switchToAdjacentSpaceViaSystemShortcut(offset: Int, displayId: String, initialSpaceId: Int) -> Int? {
|
|
2096
2101
|
let keyCode: CGKeyCode = offset < 0 ? 123 : 124
|
|
2097
|
-
let
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
"""
|
|
2103
|
-
let result = ProcessQuery.shell(["/usr/bin/osascript", "-e", script])
|
|
2104
|
-
if result != "ok" {
|
|
2105
|
-
DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut script did not complete for offset \(offset)")
|
|
2102
|
+
guard let source = CGEventSource(stateID: .combinedSessionState),
|
|
2103
|
+
let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
|
|
2104
|
+
let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) else {
|
|
2105
|
+
DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut event source unavailable for offset \(offset)")
|
|
2106
|
+
return nil
|
|
2106
2107
|
}
|
|
2108
|
+
down.flags = .maskControl
|
|
2109
|
+
up.flags = .maskControl
|
|
2110
|
+
down.post(tap: .cghidEventTap)
|
|
2111
|
+
usleep(12_000)
|
|
2112
|
+
up.post(tap: .cghidEventTap)
|
|
2107
2113
|
return waitForSpaceChange(displayId: displayId, initialSpaceId: initialSpaceId, timeout: 1.2)
|
|
2108
2114
|
}
|
|
2109
2115
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Self-healing circuit breaker for session-wide `CGEventTap`s.
|
|
4
|
+
///
|
|
5
|
+
/// macOS disables a tap (`tapDisabledByTimeout`) when its callback exceeds
|
|
6
|
+
/// the OS budget. The naive recovery — re-enable and continue — fights the
|
|
7
|
+
/// OS in a loop when the underlying cause is still present, and the system
|
|
8
|
+
/// input pipeline keeps stuttering.
|
|
9
|
+
///
|
|
10
|
+
/// This breaker counts trips inside a rolling window and backs off in
|
|
11
|
+
/// escalating cooldowns: 30s → 2 min → permanent (until app restart or
|
|
12
|
+
/// manual re-arm). During cooldown the tap stays disabled — input flows
|
|
13
|
+
/// through the OS without our interference. On cooldown expiry, `rearm`
|
|
14
|
+
/// fires on the main queue to re-enable the tap.
|
|
15
|
+
///
|
|
16
|
+
/// Thread-safe; `recordTrip()` is safe to call from the event-tap thread.
|
|
17
|
+
final class EventTapBreaker {
|
|
18
|
+
enum State: Equatable {
|
|
19
|
+
case armed
|
|
20
|
+
case paused(cooldownSec: Int)
|
|
21
|
+
case disabled
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private let label: String
|
|
25
|
+
private let trippedWindow: TimeInterval = 600 // 10 min rolling window
|
|
26
|
+
private let cooldowns: [TimeInterval] = [30, 120] // trip 1 → 30s, trip 2 → 2 min, trip 3+ → permanent
|
|
27
|
+
|
|
28
|
+
private let lock = NSLock()
|
|
29
|
+
private var tripsInWindow: [Date] = []
|
|
30
|
+
private var permanentlyDisabled = false
|
|
31
|
+
private var pendingRearm: DispatchWorkItem?
|
|
32
|
+
private var _state: State = .armed
|
|
33
|
+
|
|
34
|
+
/// Called on the main queue when a cooldown elapses. Caller wires this
|
|
35
|
+
/// to `CGEvent.tapEnable(tap:, enable: true)`.
|
|
36
|
+
var rearm: (() -> Void)?
|
|
37
|
+
|
|
38
|
+
/// Called on the main queue whenever `state` transitions. UI uses this
|
|
39
|
+
/// to surface "paused" / "disabled" messages and re-enable affordances.
|
|
40
|
+
var onStateChanged: ((State) -> Void)?
|
|
41
|
+
|
|
42
|
+
init(label: String) {
|
|
43
|
+
self.label = label
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var state: State {
|
|
47
|
+
lock.lock(); defer { lock.unlock() }
|
|
48
|
+
return _state
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Record that the OS just delivered `.tapDisabledByTimeout`. Schedules
|
|
52
|
+
/// a re-enable after the appropriate cooldown, or marks the breaker
|
|
53
|
+
/// permanently open after too many trips.
|
|
54
|
+
@discardableResult
|
|
55
|
+
func recordTrip() -> Bool {
|
|
56
|
+
lock.lock()
|
|
57
|
+
if permanentlyDisabled { lock.unlock(); return false }
|
|
58
|
+
|
|
59
|
+
let now = Date()
|
|
60
|
+
tripsInWindow.removeAll { now.timeIntervalSince($0) > trippedWindow }
|
|
61
|
+
tripsInWindow.append(now)
|
|
62
|
+
|
|
63
|
+
let count = tripsInWindow.count
|
|
64
|
+
if count > cooldowns.count {
|
|
65
|
+
permanentlyDisabled = true
|
|
66
|
+
pendingRearm?.cancel()
|
|
67
|
+
pendingRearm = nil
|
|
68
|
+
_state = .disabled
|
|
69
|
+
lock.unlock()
|
|
70
|
+
DiagnosticLog.shared.error("\(label): tap tripped \(count)× in \(Int(trippedWindow))s — disabled until app restart or manual re-arm")
|
|
71
|
+
notifyStateChanged(.disabled)
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let cooldown = cooldowns[count - 1]
|
|
76
|
+
_state = .paused(cooldownSec: Int(cooldown))
|
|
77
|
+
let nextState: State = .paused(cooldownSec: Int(cooldown))
|
|
78
|
+
|
|
79
|
+
pendingRearm?.cancel()
|
|
80
|
+
let work = DispatchWorkItem { [weak self] in
|
|
81
|
+
guard let self else { return }
|
|
82
|
+
DiagnosticLog.shared.info("\(self.label): tap auto-recovering")
|
|
83
|
+
self.lock.lock()
|
|
84
|
+
self._state = .armed
|
|
85
|
+
self.lock.unlock()
|
|
86
|
+
self.notifyStateChanged(.armed)
|
|
87
|
+
self.rearm?()
|
|
88
|
+
}
|
|
89
|
+
pendingRearm = work
|
|
90
|
+
lock.unlock()
|
|
91
|
+
|
|
92
|
+
DiagnosticLog.shared.warn("\(label): tap disabled by OS (trip #\(count)) — paused for \(Int(cooldown))s")
|
|
93
|
+
notifyStateChanged(nextState)
|
|
94
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + cooldown, execute: work)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Clears all trip history and any pending cooldown. Caller should
|
|
99
|
+
/// re-enable the tap after this to actually recover.
|
|
100
|
+
/// Use cases: tap (re)install, manual re-arm from Settings.
|
|
101
|
+
func reset() {
|
|
102
|
+
lock.lock()
|
|
103
|
+
let wasNotArmed = _state != .armed
|
|
104
|
+
pendingRearm?.cancel()
|
|
105
|
+
pendingRearm = nil
|
|
106
|
+
tripsInWindow.removeAll()
|
|
107
|
+
permanentlyDisabled = false
|
|
108
|
+
_state = .armed
|
|
109
|
+
lock.unlock()
|
|
110
|
+
if wasNotArmed {
|
|
111
|
+
DiagnosticLog.shared.info("\(label): tap state reset (armed)")
|
|
112
|
+
notifyStateChanged(.armed)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func notifyStateChanged(_ newState: State) {
|
|
117
|
+
guard let callback = onStateChanged else { return }
|
|
118
|
+
if Thread.isMainThread {
|
|
119
|
+
callback(newState)
|
|
120
|
+
} else {
|
|
121
|
+
DispatchQueue.main.async { callback(newState) }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreFoundation
|
|
3
|
+
|
|
4
|
+
/// Hosts a long-lived thread + CFRunLoop dedicated to CGEventTap callbacks,
|
|
5
|
+
/// so taps installed at `.headInsertEventTap` don't add main-thread latency
|
|
6
|
+
/// to every keyboard/mouse event in the user's session.
|
|
7
|
+
///
|
|
8
|
+
/// Callbacks fire on this thread — callers must hop AppKit/UI work back to
|
|
9
|
+
/// main themselves (DispatchQueue.main.async).
|
|
10
|
+
final class EventTapThread {
|
|
11
|
+
static let shared = EventTapThread()
|
|
12
|
+
|
|
13
|
+
private let lock = NSLock()
|
|
14
|
+
private var runLoop: CFRunLoop?
|
|
15
|
+
|
|
16
|
+
private init() {
|
|
17
|
+
let ready = DispatchSemaphore(value: 0)
|
|
18
|
+
let thread = Thread { [unowned self] in
|
|
19
|
+
let loop = CFRunLoopGetCurrent()
|
|
20
|
+
// Keep the run loop alive across add/remove cycles by anchoring a
|
|
21
|
+
// no-op port; otherwise CFRunLoopRun() returns when the last
|
|
22
|
+
// source is removed.
|
|
23
|
+
let keepalive = NSMachPort()
|
|
24
|
+
RunLoop.current.add(keepalive, forMode: .common)
|
|
25
|
+
self.lock.lock()
|
|
26
|
+
self.runLoop = loop
|
|
27
|
+
self.lock.unlock()
|
|
28
|
+
ready.signal()
|
|
29
|
+
CFRunLoopRun()
|
|
30
|
+
}
|
|
31
|
+
thread.qualityOfService = .userInteractive
|
|
32
|
+
thread.name = "com.arach.lattices.EventTapThread"
|
|
33
|
+
thread.start()
|
|
34
|
+
ready.wait()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func add(source: CFRunLoopSource) {
|
|
38
|
+
lock.lock()
|
|
39
|
+
let loop = runLoop
|
|
40
|
+
lock.unlock()
|
|
41
|
+
guard let loop else { return }
|
|
42
|
+
CFRunLoopAddSource(loop, source, .commonModes)
|
|
43
|
+
CFRunLoopWakeUp(loop)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func remove(source: CFRunLoopSource) {
|
|
47
|
+
lock.lock()
|
|
48
|
+
let loop = runLoop
|
|
49
|
+
lock.unlock()
|
|
50
|
+
guard let loop else { return }
|
|
51
|
+
CFRunLoopRemoveSource(loop, source, .commonModes)
|
|
52
|
+
CFRunLoopWakeUp(loop)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum InputCaptureResetCenter {
|
|
4
|
+
static func reset(reason: String) {
|
|
5
|
+
if Thread.isMainThread {
|
|
6
|
+
performReset(reason: reason)
|
|
7
|
+
} else {
|
|
8
|
+
DispatchQueue.main.async {
|
|
9
|
+
performReset(reason: reason)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private static func performReset(reason: String) {
|
|
15
|
+
DiagnosticLog.shared.warn("InputCapture: reset for \(reason)")
|
|
16
|
+
ScreenOverlayCanvasController.shared.resetInputCapture(reason: reason)
|
|
17
|
+
MouseGestureController.shared.resetForSystemInputBoundary(reason: reason)
|
|
18
|
+
KeyboardRemapController.shared.resetForSystemInputBoundary(reason: reason)
|
|
19
|
+
}
|
|
20
|
+
}
|