@lattices/cli 0.4.9 → 0.4.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
|
@@ -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
|
+
}
|