@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.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
  31. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  32. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  35. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  36. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  37. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  38. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  40. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  41. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  42. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  43. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  44. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  45. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  46. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  47. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
  48. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
  49. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  50. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  51. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  52. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  53. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  54. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  55. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  56. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  57. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  58. package/bin/handsoff-infer.ts +14 -5
  59. package/bin/handsoff-worker.ts +11 -7
  60. package/bin/infer.ts +406 -0
  61. package/bin/lattices-app.ts +57 -7
  62. package/bin/lattices-dev +40 -1
  63. package/bin/lattices.ts +1 -1
  64. package/docs/agent-execution-plan.md +9 -9
  65. package/docs/api.md +119 -0
  66. package/docs/app.md +1 -0
  67. package/docs/companion-deck.md +1 -1
  68. package/docs/gesture-customization-proposal.md +520 -0
  69. package/docs/mouse-gestures.md +79 -0
  70. package/docs/overview.md +2 -2
  71. package/docs/presentation-execution-review.md +9 -9
  72. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  73. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  74. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  75. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  76. package/docs/reference/dewey.config.ts +74 -0
  77. package/docs/reference/install-agent.md +79 -0
  78. package/docs/repo-structure.md +100 -0
  79. package/docs/voice-error-model.md +7 -7
  80. package/docs/voice.md +18 -0
  81. package/package.json +23 -13
  82. package/swift/Package.swift +20 -0
  83. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  84. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  85. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  86. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  87. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  88. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  89. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  90. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  91. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  92. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  93. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  94. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  95. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  96. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  97. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  98. /package/{app → apps/mac}/Info.plist +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  100. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  101. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  102. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  103. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  107. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  108. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -0,0 +1,335 @@
1
+ import AppKit
2
+ import Combine
3
+ import CoreGraphics
4
+
5
+ final class KeyboardRemapController: ObservableObject {
6
+ static let shared = KeyboardRemapController()
7
+
8
+ /// Live state of the event-tap circuit breaker. SettingsView observes
9
+ /// this to surface "paused" / "disabled" status and a re-arm button.
10
+ @Published private(set) var breakerState: EventTapBreaker.State = .armed
11
+
12
+ private static let syntheticMarker: Int64 = 0x4C4B524D
13
+
14
+ private var eventTap: CFMachPort?
15
+ private var runLoopSource: CFRunLoopSource?
16
+ private var subscriptions: Set<AnyCancellable> = []
17
+ private var installedObservers = false
18
+ private var capsLayerActive = false
19
+ private var capsUsedAsModifier = false
20
+ private var capsLayerActivatedAt: CFAbsoluteTime?
21
+ private var capsLayerLastEventAt: CFAbsoluteTime?
22
+ private var bypassUntil: CFAbsoluteTime = 0
23
+ private var lastCapsLayerStaleLogAt: CFAbsoluteTime = 0
24
+ private var pressedKeyCodes = Set<Int64>()
25
+ private let breaker = EventTapBreaker(label: "KeyboardRemap")
26
+ private let budgetMeter = TapBudgetMeter(label: "KeyboardRemap")
27
+ private let maxCapsLayerIdleDuration: TimeInterval = 2.0
28
+ private let maxCapsLayerHeldDuration: TimeInterval = 20.0
29
+ private let emergencyBypassDuration: TimeInterval = 3.0
30
+
31
+ private init() {
32
+ breaker.onStateChanged = { [weak self] newState in
33
+ self?.breakerState = newState
34
+ }
35
+ }
36
+
37
+ /// Re-enable the tap after a breaker trip, clearing trip history.
38
+ /// Settings UI calls this when the user explicitly chooses to recover
39
+ /// from a `disabled` state.
40
+ func reArmAfterBreakerTrip() {
41
+ dispatchPrecondition(condition: .onQueue(.main))
42
+ breaker.reset()
43
+ if let tap = eventTap {
44
+ CGEvent.tapEnable(tap: tap, enable: true)
45
+ }
46
+ }
47
+
48
+ func start() {
49
+ installObserversIfNeeded()
50
+ refresh()
51
+ }
52
+
53
+ func stop() {
54
+ removeEventTap()
55
+ clearCapsLayer()
56
+ }
57
+
58
+ func resetForSystemInputBoundary(reason: String) {
59
+ dispatchPrecondition(condition: .onQueue(.main))
60
+ clearCapsLayer()
61
+ pressedKeyCodes.removeAll()
62
+ breaker.reset()
63
+ if let eventTap {
64
+ CGEvent.tapEnable(tap: eventTap, enable: true)
65
+ } else {
66
+ refresh()
67
+ }
68
+ DiagnosticLog.shared.warn("KeyboardRemap: reset for \(reason)")
69
+ }
70
+
71
+ private func installObserversIfNeeded() {
72
+ guard !installedObservers else { return }
73
+ installedObservers = true
74
+
75
+ Preferences.shared.$keyboardRemapsEnabled
76
+ .receive(on: RunLoop.main)
77
+ .sink { [weak self] _ in self?.refresh() }
78
+ .store(in: &subscriptions)
79
+
80
+ PermissionChecker.shared.$accessibility
81
+ .receive(on: RunLoop.main)
82
+ .sink { [weak self] _ in self?.refresh() }
83
+ .store(in: &subscriptions)
84
+ }
85
+
86
+ private func refresh() {
87
+ guard Preferences.shared.keyboardRemapsEnabled,
88
+ PermissionChecker.shared.accessibility else {
89
+ removeEventTap()
90
+ return
91
+ }
92
+
93
+ KeyboardRemapStore.shared.ensureConfigFile()
94
+ if eventTap == nil {
95
+ installEventTap()
96
+ } else if let eventTap {
97
+ CGEvent.tapEnable(tap: eventTap, enable: true)
98
+ }
99
+ }
100
+
101
+ private func installEventTap() {
102
+ // Fresh install is a clean slate — drop any stale trip history so
103
+ // the new tap's first failure is judged on its own merits.
104
+ breaker.reset()
105
+
106
+ var mask = CGEventMask(0)
107
+ mask |= CGEventMask(1) << CGEventType.keyDown.rawValue
108
+ mask |= CGEventMask(1) << CGEventType.keyUp.rawValue
109
+ mask |= CGEventMask(1) << CGEventType.flagsChanged.rawValue
110
+
111
+ let tap = CGEvent.tapCreate(
112
+ tap: .cgSessionEventTap,
113
+ place: .headInsertEventTap,
114
+ options: .defaultTap,
115
+ eventsOfInterest: mask,
116
+ callback: Self.eventTapCallback,
117
+ userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
118
+ )
119
+
120
+ guard let tap else {
121
+ DiagnosticLog.shared.warn("KeyboardRemap: failed to install keyboard event tap")
122
+ return
123
+ }
124
+
125
+ let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
126
+ eventTap = tap
127
+ runLoopSource = source
128
+
129
+ if let source {
130
+ EventTapThread.shared.add(source: source)
131
+ }
132
+ CGEvent.tapEnable(tap: tap, enable: true)
133
+ breaker.rearm = { [weak self] in
134
+ guard let self, let tap = self.eventTap else { return }
135
+ CGEvent.tapEnable(tap: tap, enable: true)
136
+ }
137
+ DiagnosticLog.shared.info("KeyboardRemap: keyboard event tap installed")
138
+ }
139
+
140
+ private func removeEventTap() {
141
+ if let source = runLoopSource {
142
+ EventTapThread.shared.remove(source: source)
143
+ }
144
+ runLoopSource = nil
145
+ if let tap = eventTap {
146
+ CFMachPortInvalidate(tap)
147
+ }
148
+ eventTap = nil
149
+ }
150
+
151
+ private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
152
+ guard let userInfo else { return Unmanaged.passUnretained(event) }
153
+ let controller = Unmanaged<KeyboardRemapController>.fromOpaque(userInfo).takeUnretainedValue()
154
+ return controller.handleEvent(type: type, event: event)
155
+ }
156
+
157
+ private func handleEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
158
+ let started = CFAbsoluteTimeGetCurrent()
159
+ defer {
160
+ let elapsedMs = (CFAbsoluteTimeGetCurrent() - started) * 1000
161
+ budgetMeter.record(durationMs: elapsedMs)
162
+ }
163
+
164
+ if type == .tapDisabledByTimeout {
165
+ // OS killed the tap because a callback was too slow. Run through
166
+ // the breaker — it backs off in escalating cooldowns rather than
167
+ // re-enabling immediately and getting killed again.
168
+ clearCapsLayer()
169
+ breaker.recordTrip()
170
+ return Unmanaged.passUnretained(event)
171
+ }
172
+ if type == .tapDisabledByUserInput {
173
+ // User-driven disable (rare). Re-enable directly, no cooldown.
174
+ clearCapsLayer()
175
+ if let eventTap {
176
+ CGEvent.tapEnable(tap: eventTap, enable: true)
177
+ }
178
+ return Unmanaged.passUnretained(event)
179
+ }
180
+
181
+ if event.getIntegerValueField(.eventSourceUserData) == Self.syntheticMarker {
182
+ return Unmanaged.passUnretained(event)
183
+ }
184
+
185
+ updatePressedKeys(type: type, keyCode: event.getIntegerValueField(.keyboardEventKeycode))
186
+ if shouldTriggerEmergencyReset(type: type, event: event) {
187
+ emergencyClear(now: started)
188
+ InputCaptureResetCenter.reset(reason: "keyboard emergency chord")
189
+ return Unmanaged.passUnretained(event)
190
+ }
191
+
192
+ if started < bypassUntil {
193
+ return Unmanaged.passUnretained(event)
194
+ }
195
+
196
+ KeyboardRemapStore.shared.scheduleReloadCheckIfNeeded()
197
+ guard let rule = KeyboardRemapStore.shared.capsLockRule,
198
+ rule.toIfHeld == .hyper else {
199
+ return Unmanaged.passUnretained(event)
200
+ }
201
+
202
+ let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
203
+ if type == .flagsChanged, keyCode == rule.from.keyCode {
204
+ return handleCapsLockFlagsChanged(event, rule: rule)
205
+ }
206
+
207
+ reconcileCapsLayer(event: event, type: type, now: started)
208
+ guard capsLayerActive else {
209
+ return Unmanaged.passUnretained(event)
210
+ }
211
+
212
+ switch type {
213
+ case .keyDown:
214
+ if keyCode == 53 {
215
+ emergencyClear(now: started)
216
+ return Unmanaged.passUnretained(event)
217
+ }
218
+ capsUsedAsModifier = true
219
+ capsLayerLastEventAt = started
220
+ event.flags = normalizedFlags(event.flags).union(.latticesHyper)
221
+ return Unmanaged.passUnretained(event)
222
+ case .keyUp:
223
+ capsLayerLastEventAt = started
224
+ event.flags = normalizedFlags(event.flags).union(.latticesHyper)
225
+ return Unmanaged.passUnretained(event)
226
+ default:
227
+ return Unmanaged.passUnretained(event)
228
+ }
229
+ }
230
+
231
+ private func handleCapsLockFlagsChanged(_ event: CGEvent, rule: KeyboardRemapRule) -> Unmanaged<CGEvent>? {
232
+ let isDown = event.flags.contains(.maskAlphaShift)
233
+ if isDown {
234
+ capsLayerActive = true
235
+ capsUsedAsModifier = false
236
+ let now = CFAbsoluteTimeGetCurrent()
237
+ capsLayerActivatedAt = now
238
+ capsLayerLastEventAt = now
239
+ DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer active")
240
+ } else {
241
+ let shouldTap = capsLayerActive && !capsUsedAsModifier && rule.toIfAlone == .escape
242
+ clearCapsLayer()
243
+ if shouldTap {
244
+ postKeyTap(keyCode: 53)
245
+ }
246
+ DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer inactive")
247
+ }
248
+
249
+ return nil
250
+ }
251
+
252
+ private func clearCapsLayer() {
253
+ capsLayerActive = false
254
+ capsUsedAsModifier = false
255
+ capsLayerActivatedAt = nil
256
+ capsLayerLastEventAt = nil
257
+ }
258
+
259
+ private func reconcileCapsLayer(event: CGEvent, type: CGEventType, now: CFAbsoluteTime) {
260
+ guard capsLayerActive else { return }
261
+
262
+ // If a release event was dropped, later key events often arrive
263
+ // without the physical Caps flag. Treat that as an input boundary and
264
+ // fail open before rewriting the user's key.
265
+ if type == .keyDown || type == .keyUp,
266
+ !event.flags.contains(.maskAlphaShift) {
267
+ clearCapsLayer(reason: "physical Caps flag cleared", now: now)
268
+ return
269
+ }
270
+
271
+ if let lastEventAt = capsLayerLastEventAt,
272
+ now - lastEventAt > maxCapsLayerIdleDuration {
273
+ clearCapsLayer(reason: "idle", now: now)
274
+ return
275
+ }
276
+
277
+ if let activatedAt = capsLayerActivatedAt,
278
+ now - activatedAt > maxCapsLayerHeldDuration {
279
+ clearCapsLayer(reason: "held too long", now: now)
280
+ }
281
+ }
282
+
283
+ private func clearCapsLayer(reason: String, now: CFAbsoluteTime) {
284
+ clearCapsLayer()
285
+ if now - lastCapsLayerStaleLogAt > 1 {
286
+ lastCapsLayerStaleLogAt = now
287
+ DiagnosticLog.shared.warn("KeyboardRemap: Caps Lock layer cleared (\(reason))")
288
+ }
289
+ }
290
+
291
+ private func emergencyClear(now: CFAbsoluteTime) {
292
+ clearCapsLayer()
293
+ pressedKeyCodes.removeAll()
294
+ bypassUntil = now + emergencyBypassDuration
295
+ DiagnosticLog.shared.warn("KeyboardRemap: emergency bypass via Escape")
296
+ }
297
+
298
+ private func updatePressedKeys(type: CGEventType, keyCode: Int64) {
299
+ switch type {
300
+ case .keyDown:
301
+ pressedKeyCodes.insert(keyCode)
302
+ case .keyUp:
303
+ pressedKeyCodes.remove(keyCode)
304
+ default:
305
+ break
306
+ }
307
+ }
308
+
309
+ private func shouldTriggerEmergencyReset(type: CGEventType, event: CGEvent) -> Bool {
310
+ guard type == .keyDown else { return false }
311
+ let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
312
+ let flags = event.flags
313
+ return keyCode == 40
314
+ && pressedKeyCodes.contains(53)
315
+ && flags.contains(.maskShift)
316
+ }
317
+
318
+ private func normalizedFlags(_ flags: CGEventFlags) -> CGEventFlags {
319
+ var normalized = flags
320
+ normalized.remove(.maskAlphaShift)
321
+ return normalized
322
+ }
323
+
324
+ private func postKeyTap(keyCode: CGKeyCode) {
325
+ guard let source = CGEventSource(stateID: .combinedSessionState),
326
+ let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
327
+ let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) else {
328
+ return
329
+ }
330
+ down.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
331
+ up.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
332
+ down.post(tap: .cghidEventTap)
333
+ up.post(tap: .cghidEventTap)
334
+ }
335
+ }
@@ -0,0 +1,141 @@
1
+ import AppKit
2
+ import Combine
3
+ import Foundation
4
+
5
+ final class KeyboardRemapStore: ObservableObject {
6
+ static let shared = KeyboardRemapStore()
7
+
8
+ @Published private(set) var config: KeyboardRemapConfig
9
+
10
+ let configURL: URL
11
+ /// Lock-protected mirror of `config` for tap-thread reads. The keyboard
12
+ /// event tap runs on EventTapThread and must not read the @Published
13
+ /// SwiftUI-facing config directly while main may be mutating it.
14
+ private let stateLock = NSLock()
15
+ private var snapshot: KeyboardRemapConfig
16
+ private var lastLoadedModifiedDate: Date?
17
+ private var lastReloadCheckAt: Date = .distantPast
18
+ private var reloadCheckInFlight = false
19
+ private let reloadCheckInterval: TimeInterval = 1.0
20
+
21
+ private init() {
22
+ let dir = FileManager.default.homeDirectoryForCurrentUser
23
+ .appendingPathComponent(".lattices")
24
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
25
+ self.configURL = dir.appendingPathComponent("keyboard-remaps.json")
26
+ self.config = .defaults
27
+ self.snapshot = .defaults
28
+ ensureConfigFile()
29
+ reload()
30
+ }
31
+
32
+ var enabledRules: [KeyboardRemapRule] {
33
+ stateLock.lock(); defer { stateLock.unlock() }
34
+ return snapshot.rules.filter(\.enabled)
35
+ }
36
+
37
+ var summaryLines: [String] {
38
+ enabledRules.map(\.summaryLine)
39
+ }
40
+
41
+ var capsLockRule: KeyboardRemapRule? {
42
+ enabledRules.first { $0.from == .capsLock }
43
+ }
44
+
45
+ func ensureConfigFile() {
46
+ guard !FileManager.default.fileExists(atPath: configURL.path) else { return }
47
+ write(config: .defaults)
48
+ }
49
+
50
+ func reload() {
51
+ // @Published mutation must run on main; hop if called off-main.
52
+ if !Thread.isMainThread {
53
+ DispatchQueue.main.async { [weak self] in self?.reload() }
54
+ return
55
+ }
56
+ let newDate = modifiedDate()
57
+ let newConfig: KeyboardRemapConfig
58
+ guard let data = FileManager.default.contents(atPath: configURL.path) else {
59
+ newConfig = .defaults
60
+ stateLock.lock()
61
+ snapshot = newConfig
62
+ lastLoadedModifiedDate = newDate
63
+ stateLock.unlock()
64
+ config = newConfig
65
+ return
66
+ }
67
+
68
+ do {
69
+ newConfig = try JSONDecoder().decode(KeyboardRemapConfig.self, from: data)
70
+ } catch {
71
+ DiagnosticLog.shared.error("KeyboardRemapStore: failed to decode keyboard-remaps.json - \(error.localizedDescription)")
72
+ newConfig = .defaults
73
+ }
74
+
75
+ stateLock.lock()
76
+ snapshot = newConfig
77
+ lastLoadedModifiedDate = newDate
78
+ stateLock.unlock()
79
+ config = newConfig
80
+ }
81
+
82
+ func scheduleReloadCheckIfNeeded() {
83
+ // Called from the keyboard event-tap thread. Keep this path to memory
84
+ // bookkeeping only; filesystem work runs off the tap callback.
85
+ let now = Date()
86
+ stateLock.lock()
87
+ guard !reloadCheckInFlight,
88
+ now.timeIntervalSince(lastReloadCheckAt) >= reloadCheckInterval else {
89
+ stateLock.unlock()
90
+ return
91
+ }
92
+ reloadCheckInFlight = true
93
+ lastReloadCheckAt = now
94
+ stateLock.unlock()
95
+
96
+ DispatchQueue.global(qos: .utility).async { [weak self] in
97
+ self?.reloadIfNeeded()
98
+ }
99
+ }
100
+
101
+ private func reloadIfNeeded() {
102
+ let currentModifiedDate = modifiedDate()
103
+ stateLock.lock()
104
+ let needsReload = currentModifiedDate != lastLoadedModifiedDate
105
+ if needsReload {
106
+ lastLoadedModifiedDate = currentModifiedDate
107
+ }
108
+ reloadCheckInFlight = false
109
+ stateLock.unlock()
110
+ guard needsReload else { return }
111
+ reload()
112
+ }
113
+
114
+ func restoreDefaults() {
115
+ write(config: .defaults)
116
+ reload()
117
+ DiagnosticLog.shared.info("Keyboard remaps restored to defaults")
118
+ }
119
+
120
+ func openConfiguration() {
121
+ ensureConfigFile()
122
+ NSWorkspace.shared.open(configURL)
123
+ }
124
+
125
+ private func write(config: KeyboardRemapConfig) {
126
+ let encoder = JSONEncoder()
127
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
128
+ guard let data = try? encoder.encode(config) else { return }
129
+ try? data.write(to: configURL, options: .atomic)
130
+ let newDate = modifiedDate()
131
+ stateLock.lock()
132
+ snapshot = config
133
+ lastLoadedModifiedDate = newDate
134
+ stateLock.unlock()
135
+ }
136
+
137
+ private func modifiedDate() -> Date? {
138
+ let attrs = try? FileManager.default.attributesOfItem(atPath: configURL.path)
139
+ return attrs?[.modificationDate] as? Date
140
+ }
141
+ }