@lattices/cli 0.4.14 → 0.5.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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  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/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. 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
- }