@lattices/cli 0.4.14 → 0.6.0

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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,124 +0,0 @@
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
- }
@@ -1,54 +0,0 @@
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
- }
@@ -1,20 +0,0 @@
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
- }
@@ -1,69 +0,0 @@
1
- import CoreGraphics
2
- import Foundation
3
-
4
- enum KeyboardRemapKey: String, Codable, Equatable {
5
- case capsLock = "caps_lock"
6
-
7
- var keyCode: Int64 {
8
- switch self {
9
- case .capsLock: return 57
10
- }
11
- }
12
-
13
- var displayLabel: String {
14
- switch self {
15
- case .capsLock: return "Caps Lock"
16
- }
17
- }
18
- }
19
-
20
- enum KeyboardRemapAction: String, Codable, Equatable {
21
- case escape
22
- case hyper
23
-
24
- var displayLabel: String {
25
- switch self {
26
- case .escape: return "Escape"
27
- case .hyper: return "Hyper"
28
- }
29
- }
30
- }
31
-
32
- struct KeyboardRemapRule: Codable, Equatable, Identifiable {
33
- var id: String
34
- var enabled: Bool
35
- var from: KeyboardRemapKey
36
- var toIfHeld: KeyboardRemapAction
37
- var toIfAlone: KeyboardRemapAction?
38
-
39
- var summaryLine: String {
40
- let held = "hold \(from.displayLabel) -> \(toIfHeld.displayLabel)"
41
- guard let alone = toIfAlone else { return held }
42
- return "\(held), tap -> \(alone.displayLabel)"
43
- }
44
- }
45
-
46
- struct KeyboardRemapConfig: Codable, Equatable {
47
- var rules: [KeyboardRemapRule]
48
-
49
- static let defaults = KeyboardRemapConfig(
50
- rules: [
51
- KeyboardRemapRule(
52
- id: "caps_lock_hyper_escape",
53
- enabled: true,
54
- from: .capsLock,
55
- toIfHeld: .hyper,
56
- toIfAlone: .escape
57
- )
58
- ]
59
- )
60
- }
61
-
62
- extension CGEventFlags {
63
- static let latticesHyper: CGEventFlags = [
64
- .maskCommand,
65
- .maskControl,
66
- .maskAlternate,
67
- .maskShift,
68
- ]
69
- }
@@ -1,346 +0,0 @@
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: [Int64: CFAbsoluteTime] = [:]
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 maxTrackedKeyDownDuration: TimeInterval = 120.0
30
- private let emergencyBypassDuration: TimeInterval = 3.0
31
-
32
- private init() {
33
- breaker.onStateChanged = { [weak self] newState in
34
- self?.breakerState = newState
35
- }
36
- }
37
-
38
- /// Re-enable the tap after a breaker trip, clearing trip history.
39
- /// Settings UI calls this when the user explicitly chooses to recover
40
- /// from a `disabled` state.
41
- func reArmAfterBreakerTrip() {
42
- dispatchPrecondition(condition: .onQueue(.main))
43
- breaker.reset()
44
- if let tap = eventTap {
45
- CGEvent.tapEnable(tap: tap, enable: true)
46
- }
47
- }
48
-
49
- func start() {
50
- installObserversIfNeeded()
51
- refresh()
52
- }
53
-
54
- func stop() {
55
- removeEventTap()
56
- clearCapsLayer()
57
- }
58
-
59
- func resetForSystemInputBoundary(reason: String) {
60
- dispatchPrecondition(condition: .onQueue(.main))
61
- clearCapsLayer()
62
- pressedKeyCodes.removeAll()
63
- breaker.reset()
64
- if let eventTap {
65
- CGEvent.tapEnable(tap: eventTap, enable: true)
66
- } else {
67
- refresh()
68
- }
69
- DiagnosticLog.shared.warn("KeyboardRemap: reset for \(reason)")
70
- }
71
-
72
- private func installObserversIfNeeded() {
73
- guard !installedObservers else { return }
74
- installedObservers = true
75
-
76
- Preferences.shared.$keyboardRemapsEnabled
77
- .receive(on: RunLoop.main)
78
- .sink { [weak self] _ in self?.refresh() }
79
- .store(in: &subscriptions)
80
-
81
- PermissionChecker.shared.$accessibility
82
- .receive(on: RunLoop.main)
83
- .sink { [weak self] _ in self?.refresh() }
84
- .store(in: &subscriptions)
85
- }
86
-
87
- private func refresh() {
88
- guard Preferences.shared.keyboardRemapsEnabled,
89
- PermissionChecker.shared.accessibility else {
90
- removeEventTap()
91
- return
92
- }
93
-
94
- KeyboardRemapStore.shared.ensureConfigFile()
95
- if eventTap == nil {
96
- installEventTap()
97
- } else if let eventTap {
98
- CGEvent.tapEnable(tap: eventTap, enable: true)
99
- }
100
- }
101
-
102
- private func installEventTap() {
103
- // Fresh install is a clean slate — drop any stale trip history so
104
- // the new tap's first failure is judged on its own merits.
105
- breaker.reset()
106
-
107
- var mask = CGEventMask(0)
108
- mask |= CGEventMask(1) << CGEventType.keyDown.rawValue
109
- mask |= CGEventMask(1) << CGEventType.keyUp.rawValue
110
- mask |= CGEventMask(1) << CGEventType.flagsChanged.rawValue
111
-
112
- let tap = CGEvent.tapCreate(
113
- tap: .cgSessionEventTap,
114
- place: .headInsertEventTap,
115
- options: .defaultTap,
116
- eventsOfInterest: mask,
117
- callback: Self.eventTapCallback,
118
- userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
119
- )
120
-
121
- guard let tap else {
122
- DiagnosticLog.shared.warn("KeyboardRemap: failed to install keyboard event tap")
123
- return
124
- }
125
-
126
- let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
127
- eventTap = tap
128
- runLoopSource = source
129
-
130
- if let source {
131
- EventTapThread.shared.add(source: source)
132
- }
133
- CGEvent.tapEnable(tap: tap, enable: true)
134
- breaker.rearm = { [weak self] in
135
- guard let self, let tap = self.eventTap else { return }
136
- CGEvent.tapEnable(tap: tap, enable: true)
137
- }
138
- DiagnosticLog.shared.info("KeyboardRemap: keyboard event tap installed")
139
- }
140
-
141
- private func removeEventTap() {
142
- if let source = runLoopSource {
143
- EventTapThread.shared.remove(source: source)
144
- }
145
- runLoopSource = nil
146
- if let tap = eventTap {
147
- CFMachPortInvalidate(tap)
148
- }
149
- eventTap = nil
150
- }
151
-
152
- private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
153
- guard let userInfo else { return Unmanaged.passUnretained(event) }
154
- let controller = Unmanaged<KeyboardRemapController>.fromOpaque(userInfo).takeUnretainedValue()
155
- return controller.handleEvent(type: type, event: event)
156
- }
157
-
158
- private func handleEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
159
- let started = CFAbsoluteTimeGetCurrent()
160
- defer {
161
- let elapsedMs = (CFAbsoluteTimeGetCurrent() - started) * 1000
162
- budgetMeter.record(durationMs: elapsedMs)
163
- }
164
-
165
- if type == .tapDisabledByTimeout {
166
- // OS killed the tap because a callback was too slow. Run through
167
- // the breaker — it backs off in escalating cooldowns rather than
168
- // re-enabling immediately and getting killed again.
169
- clearCapsLayer()
170
- breaker.recordTrip()
171
- return Unmanaged.passUnretained(event)
172
- }
173
- if type == .tapDisabledByUserInput {
174
- // User-driven disable (rare). Re-enable directly, no cooldown.
175
- clearCapsLayer()
176
- if let eventTap {
177
- CGEvent.tapEnable(tap: eventTap, enable: true)
178
- }
179
- return Unmanaged.passUnretained(event)
180
- }
181
-
182
- if event.getIntegerValueField(.eventSourceUserData) == Self.syntheticMarker {
183
- return Unmanaged.passUnretained(event)
184
- }
185
-
186
- expireStalePressedKeys(now: started)
187
- updatePressedKeys(type: type, keyCode: event.getIntegerValueField(.keyboardEventKeycode), now: started)
188
- if shouldTriggerEmergencyReset(type: type, event: event) {
189
- emergencyClear(now: started)
190
- InputCaptureResetCenter.reset(reason: "keyboard emergency chord")
191
- return Unmanaged.passUnretained(event)
192
- }
193
-
194
- if started < bypassUntil {
195
- return Unmanaged.passUnretained(event)
196
- }
197
-
198
- KeyboardRemapStore.shared.scheduleReloadCheckIfNeeded()
199
- guard let rule = KeyboardRemapStore.shared.capsLockRule,
200
- rule.toIfHeld == .hyper else {
201
- return Unmanaged.passUnretained(event)
202
- }
203
-
204
- let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
205
- if type == .flagsChanged, keyCode == rule.from.keyCode {
206
- return handleCapsLockFlagsChanged(event, rule: rule)
207
- }
208
-
209
- reconcileCapsLayer(event: event, type: type, now: started)
210
- guard capsLayerActive else {
211
- return Unmanaged.passUnretained(event)
212
- }
213
-
214
- switch type {
215
- case .keyDown:
216
- if keyCode == 53 {
217
- emergencyClear(now: started)
218
- return Unmanaged.passUnretained(event)
219
- }
220
- capsUsedAsModifier = true
221
- capsLayerLastEventAt = started
222
- event.flags = normalizedFlags(event.flags).union(.latticesHyper)
223
- return Unmanaged.passUnretained(event)
224
- case .keyUp:
225
- capsLayerLastEventAt = started
226
- event.flags = normalizedFlags(event.flags).union(.latticesHyper)
227
- return Unmanaged.passUnretained(event)
228
- default:
229
- return Unmanaged.passUnretained(event)
230
- }
231
- }
232
-
233
- private func handleCapsLockFlagsChanged(_ event: CGEvent, rule: KeyboardRemapRule) -> Unmanaged<CGEvent>? {
234
- let isDown = event.flags.contains(.maskAlphaShift)
235
- if isDown {
236
- capsLayerActive = true
237
- capsUsedAsModifier = false
238
- let now = CFAbsoluteTimeGetCurrent()
239
- capsLayerActivatedAt = now
240
- capsLayerLastEventAt = now
241
- DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer active")
242
- } else {
243
- let shouldTap = capsLayerActive && !capsUsedAsModifier && rule.toIfAlone == .escape
244
- clearCapsLayer()
245
- if shouldTap {
246
- postKeyTap(keyCode: 53)
247
- }
248
- DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer inactive")
249
- }
250
-
251
- return nil
252
- }
253
-
254
- private func clearCapsLayer() {
255
- capsLayerActive = false
256
- capsUsedAsModifier = false
257
- capsLayerActivatedAt = nil
258
- capsLayerLastEventAt = nil
259
- }
260
-
261
- private func reconcileCapsLayer(event: CGEvent, type: CGEventType, now: CFAbsoluteTime) {
262
- guard capsLayerActive else { return }
263
-
264
- // If a release event was dropped, later key events often arrive
265
- // without the physical Caps flag. Treat that as an input boundary and
266
- // fail open before rewriting the user's key.
267
- if type == .keyDown || type == .keyUp,
268
- !event.flags.contains(.maskAlphaShift) {
269
- clearCapsLayer(reason: "physical Caps flag cleared", now: now)
270
- return
271
- }
272
-
273
- if let lastEventAt = capsLayerLastEventAt,
274
- now - lastEventAt > maxCapsLayerIdleDuration {
275
- clearCapsLayer(reason: "idle", now: now)
276
- return
277
- }
278
-
279
- if let activatedAt = capsLayerActivatedAt,
280
- now - activatedAt > maxCapsLayerHeldDuration {
281
- clearCapsLayer(reason: "held too long", now: now)
282
- }
283
- }
284
-
285
- private func clearCapsLayer(reason: String, now: CFAbsoluteTime) {
286
- clearCapsLayer()
287
- if now - lastCapsLayerStaleLogAt > 1 {
288
- lastCapsLayerStaleLogAt = now
289
- DiagnosticLog.shared.warn("KeyboardRemap: Caps Lock layer cleared (\(reason))")
290
- }
291
- }
292
-
293
- private func emergencyClear(now: CFAbsoluteTime) {
294
- clearCapsLayer()
295
- pressedKeyCodes.removeAll()
296
- bypassUntil = now + emergencyBypassDuration
297
- DiagnosticLog.shared.warn("KeyboardRemap: emergency bypass via Escape")
298
- }
299
-
300
- private func updatePressedKeys(type: CGEventType, keyCode: Int64, now: CFAbsoluteTime) {
301
- switch type {
302
- case .keyDown:
303
- pressedKeyCodes[keyCode] = now
304
- case .keyUp:
305
- pressedKeyCodes.removeValue(forKey: keyCode)
306
- default:
307
- break
308
- }
309
- }
310
-
311
- private func expireStalePressedKeys(now: CFAbsoluteTime) {
312
- let staleKeys = pressedKeyCodes.filter { now - $0.value > maxTrackedKeyDownDuration }.map(\.key)
313
- guard !staleKeys.isEmpty else { return }
314
- for keyCode in staleKeys {
315
- pressedKeyCodes.removeValue(forKey: keyCode)
316
- }
317
- DiagnosticLog.shared.warn("KeyboardRemap: cleared stale key-down state for \(staleKeys.count) key(s)")
318
- }
319
-
320
- private func shouldTriggerEmergencyReset(type: CGEventType, event: CGEvent) -> Bool {
321
- guard type == .keyDown else { return false }
322
- let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
323
- let flags = event.flags
324
- return keyCode == 40
325
- && pressedKeyCodes[53] != nil
326
- && flags.contains(.maskShift)
327
- }
328
-
329
- private func normalizedFlags(_ flags: CGEventFlags) -> CGEventFlags {
330
- var normalized = flags
331
- normalized.remove(.maskAlphaShift)
332
- return normalized
333
- }
334
-
335
- private func postKeyTap(keyCode: CGKeyCode) {
336
- guard let source = CGEventSource(stateID: .combinedSessionState),
337
- let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
338
- let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) else {
339
- return
340
- }
341
- down.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
342
- up.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
343
- down.post(tap: .cghidEventTap)
344
- up.post(tap: .cghidEventTap)
345
- }
346
- }