@lattices/cli 0.4.10 → 0.4.12
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/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 +2271 -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 +8 -8
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1264 -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/DesktopModel.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/ScreenMapView.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
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import Carbon
|
|
3
|
-
import SwiftUI
|
|
4
|
-
|
|
5
|
-
extension Notification.Name {
|
|
6
|
-
static let latticesPopoverWillShow = Notification.Name("latticesPopoverWillShow")
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/// Manages the NSStatusItem (menu bar icon), left-click popover, and right-click context menu.
|
|
10
|
-
/// Replaces the previous SwiftUI MenuBarExtra approach for full click-event control.
|
|
11
|
-
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
12
|
-
private static weak var shared: AppDelegate?
|
|
13
|
-
|
|
14
|
-
private var statusItem: NSStatusItem!
|
|
15
|
-
private var popover: NSPopover?
|
|
16
|
-
private var contextMenu: NSMenu!
|
|
17
|
-
|
|
18
|
-
/// 3×3 grid icon for the menu bar — L-shape bright, rest dim (template for auto light/dark)
|
|
19
|
-
private static let menuBarIcon: NSImage = {
|
|
20
|
-
let size: CGFloat = 18
|
|
21
|
-
let img = NSImage(size: NSSize(width: size, height: size), flipped: true) { _ in
|
|
22
|
-
let pad: CGFloat = 2
|
|
23
|
-
let gap: CGFloat = 1.5
|
|
24
|
-
let cellSize = (size - 2 * pad - 2 * gap) / 3
|
|
25
|
-
|
|
26
|
-
let solidCells: Set<Int> = [0, 3, 6, 7, 8]
|
|
27
|
-
|
|
28
|
-
for row in 0..<3 {
|
|
29
|
-
for col in 0..<3 {
|
|
30
|
-
let idx = row * 3 + col
|
|
31
|
-
let x = pad + CGFloat(col) * (cellSize + gap)
|
|
32
|
-
let y = pad + CGFloat(row) * (cellSize + gap)
|
|
33
|
-
let rect = NSRect(x: x, y: y, width: cellSize, height: cellSize)
|
|
34
|
-
|
|
35
|
-
if solidCells.contains(idx) {
|
|
36
|
-
NSColor.black.setFill()
|
|
37
|
-
} else {
|
|
38
|
-
NSColor.black.withAlphaComponent(0.25).setFill()
|
|
39
|
-
}
|
|
40
|
-
let path = NSBezierPath(roundedRect: rect, xRadius: 0.8, yRadius: 0.8)
|
|
41
|
-
path.fill()
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return true
|
|
45
|
-
}
|
|
46
|
-
img.isTemplate = true
|
|
47
|
-
return img
|
|
48
|
-
}()
|
|
49
|
-
|
|
50
|
-
/// Toggle between .accessory (hidden from Dock/Cmd+Tab) and .regular (visible)
|
|
51
|
-
/// based on whether any managed windows are open.
|
|
52
|
-
static func updateActivationPolicy() {
|
|
53
|
-
let hasVisibleWindow =
|
|
54
|
-
(Self.shared?.popover?.isShown == true) ||
|
|
55
|
-
CommandModeWindow.shared.isVisible ||
|
|
56
|
-
CommandPaletteWindow.shared.isVisible ||
|
|
57
|
-
MainWindow.shared.isVisible ||
|
|
58
|
-
ScreenMapWindowController.shared.isVisible ||
|
|
59
|
-
OmniSearchWindow.shared.isVisible
|
|
60
|
-
let desired: NSApplication.ActivationPolicy = hasVisibleWindow ? .regular : .accessory
|
|
61
|
-
if NSApp.activationPolicy() != desired {
|
|
62
|
-
NSApp.setActivationPolicy(desired)
|
|
63
|
-
if desired == .regular {
|
|
64
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
70
|
-
Self.shared = self
|
|
71
|
-
NSApp.setActivationPolicy(.accessory)
|
|
72
|
-
NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
73
|
-
registerDeepLinkHandler()
|
|
74
|
-
|
|
75
|
-
// --- Status item ---
|
|
76
|
-
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
|
77
|
-
if let button = statusItem.button {
|
|
78
|
-
button.image = Self.menuBarIcon
|
|
79
|
-
button.action = #selector(statusItemClicked(_:))
|
|
80
|
-
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
|
|
81
|
-
button.target = self
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --- Context menu (right-click) ---
|
|
85
|
-
contextMenu = buildContextMenu()
|
|
86
|
-
|
|
87
|
-
// --- Hotkey registration ---
|
|
88
|
-
let scanner = ProjectScanner.shared
|
|
89
|
-
CommandPaletteWindow.shared.configure(scanner: scanner)
|
|
90
|
-
|
|
91
|
-
let store = HotkeyStore.shared
|
|
92
|
-
store.register(action: .palette) { CommandPaletteWindow.shared.toggle() }
|
|
93
|
-
store.register(action: .unifiedWindow) { ScreenMapWindowController.shared.toggle() }
|
|
94
|
-
store.register(action: .bezel) { Self.showWorkspaceInspector() }
|
|
95
|
-
store.register(action: .cheatSheet) { SettingsWindowController.shared.show() }
|
|
96
|
-
store.register(action: .desktopInventory) {
|
|
97
|
-
DiagnosticLog.shared.info("Hotkey: desktopInventory triggered")
|
|
98
|
-
ScreenMapWindowController.shared.showPage(.desktopInventory)
|
|
99
|
-
}
|
|
100
|
-
store.register(action: .voiceCommand) {
|
|
101
|
-
DiagnosticLog.shared.info("Hotkey: voiceCommand triggered")
|
|
102
|
-
VoiceCommandWindow.shared.toggle()
|
|
103
|
-
}
|
|
104
|
-
store.register(action: .handsOff) {
|
|
105
|
-
DiagnosticLog.shared.info("Hotkey: handsOff triggered")
|
|
106
|
-
HandsOffSession.shared.toggle()
|
|
107
|
-
// Show voice bar when starting, hide when stopping
|
|
108
|
-
if HandsOffSession.shared.state != .idle {
|
|
109
|
-
HUDController.shared.showVoiceBar()
|
|
110
|
-
} else {
|
|
111
|
-
HUDController.shared.hideVoiceBar()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
store.register(action: .hud) { HUDController.shared.toggle() }
|
|
115
|
-
store.register(action: .mouseFinder) { MouseFinder.shared.find() }
|
|
116
|
-
|
|
117
|
-
// Pre-render HUD panels off-screen for instant first open
|
|
118
|
-
DispatchQueue.main.async { HUDController.shared.warmUp() }
|
|
119
|
-
// Pre-build the menu bar popover so the first click doesn't pay the SwiftUI mount cost.
|
|
120
|
-
// Touching `.view` forces NSHostingController to materialize the SwiftUI view tree.
|
|
121
|
-
DispatchQueue.main.async { [weak self] in
|
|
122
|
-
guard let self = self else { return }
|
|
123
|
-
let p = self.makePopover()
|
|
124
|
-
_ = p.contentViewController?.view
|
|
125
|
-
}
|
|
126
|
-
store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
|
|
127
|
-
WindowDragSnapController.shared.start()
|
|
128
|
-
MouseGestureController.shared.start()
|
|
129
|
-
KeyboardRemapController.shared.start()
|
|
130
|
-
|
|
131
|
-
// Session layer cycling
|
|
132
|
-
store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
|
|
133
|
-
store.register(action: .layerPrev) { SessionLayerStore.shared.cyclePrev() }
|
|
134
|
-
store.register(action: .layerTag) { SessionLayerStore.shared.tagFrontmostWindow() }
|
|
135
|
-
|
|
136
|
-
// Layer-switching hotkeys (1-9): session layers take priority
|
|
137
|
-
let workspace = WorkspaceManager.shared
|
|
138
|
-
let configLayerCount = (workspace.config?.layers ?? []).count
|
|
139
|
-
let maxLayers = max(configLayerCount, 9)
|
|
140
|
-
for (i, action) in HotkeyAction.layerActions.prefix(maxLayers).enumerated() {
|
|
141
|
-
let index = i
|
|
142
|
-
store.register(action: action) {
|
|
143
|
-
let session = SessionLayerStore.shared
|
|
144
|
-
if !session.layers.isEmpty && index < session.layers.count {
|
|
145
|
-
session.switchTo(index: index)
|
|
146
|
-
} else {
|
|
147
|
-
workspace.focusLayer(index: index)
|
|
148
|
-
}
|
|
149
|
-
EventBus.shared.post(.layerSwitched(index: index))
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Tiling hotkeys
|
|
154
|
-
let tileMap: [(HotkeyAction, TilePosition)] = [
|
|
155
|
-
(.tileLeft, .left), (.tileRight, .right),
|
|
156
|
-
(.tileMaximize, .maximize), (.tileCenter, .center),
|
|
157
|
-
(.tileTopLeft, .topLeft), (.tileTopRight, .topRight),
|
|
158
|
-
(.tileBottomLeft, .bottomLeft), (.tileBottomRight, .bottomRight),
|
|
159
|
-
(.tileTop, .top), (.tileBottom, .bottom),
|
|
160
|
-
(.tileLeftThird, .leftThird), (.tileCenterThird, .centerThird),
|
|
161
|
-
(.tileRightThird, .rightThird),
|
|
162
|
-
]
|
|
163
|
-
for (action, position) in tileMap {
|
|
164
|
-
store.register(action: action) { WindowTiler.tileFrontmostViaAX(to: position) }
|
|
165
|
-
}
|
|
166
|
-
store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
|
|
167
|
-
store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
|
|
168
|
-
store.register(action: .tileOrganize) {
|
|
169
|
-
let appName = DesktopModel.shared.frontmostWindow()?.app
|
|
170
|
-
?? NSWorkspace.shared.frontmostApplication?.localizedName
|
|
171
|
-
CommandModeWindow.shared.show(launchMode: .organize(appName: appName))
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Onboarding on first launch; otherwise just check permissions
|
|
175
|
-
if !OnboardingWindowController.shared.showIfNeeded() {
|
|
176
|
-
PermissionChecker.shared.check()
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Start daemon services
|
|
180
|
-
let diag = DiagnosticLog.shared
|
|
181
|
-
let tBoot = diag.startTimed("Daemon services boot")
|
|
182
|
-
OcrStore.shared.open()
|
|
183
|
-
DesktopModel.shared.start()
|
|
184
|
-
OcrModel.shared.start()
|
|
185
|
-
TmuxModel.shared.start()
|
|
186
|
-
ProcessModel.shared.start()
|
|
187
|
-
LatticesApi.setup()
|
|
188
|
-
DaemonServer.shared.start()
|
|
189
|
-
if Preferences.shared.companionBridgeEnabled {
|
|
190
|
-
LatticesCompanionBridgeServer.shared.start()
|
|
191
|
-
} else {
|
|
192
|
-
diag.info("CompanionBridge: disabled by preference")
|
|
193
|
-
}
|
|
194
|
-
AgentPool.shared.start()
|
|
195
|
-
diag.finish(tBoot)
|
|
196
|
-
|
|
197
|
-
Task {
|
|
198
|
-
await AppUpdater.shared.checkIfNeeded()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// --diagnostics flag: auto-open diagnostics panel on launch
|
|
202
|
-
if CommandLine.arguments.contains("--diagnostics") {
|
|
203
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
204
|
-
DiagnosticWindow.shared.show()
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// --screen-map flag: auto-open layout on launch
|
|
209
|
-
if CommandLine.arguments.contains("--screen-map") {
|
|
210
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
211
|
-
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
func applicationWillTerminate(_ notification: Notification) {
|
|
217
|
-
KeyboardRemapController.shared.stop()
|
|
218
|
-
LatticesCompanionBridgeServer.shared.stop()
|
|
219
|
-
DaemonServer.shared.stop()
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// MARK: - Status item click handler
|
|
223
|
-
|
|
224
|
-
@objc private func statusItemClicked(_ sender: Any?) {
|
|
225
|
-
guard let event = NSApp.currentEvent, let button = statusItem.button else { return }
|
|
226
|
-
|
|
227
|
-
if event.type == .rightMouseUp {
|
|
228
|
-
// Right-click → context menu
|
|
229
|
-
contextMenu.popUp(positioning: nil, at: NSPoint(x: 0, y: button.bounds.height + 4), in: button)
|
|
230
|
-
} else {
|
|
231
|
-
// Left-click → toggle the menu bar projects popover.
|
|
232
|
-
if let shown = popover, shown.isShown {
|
|
233
|
-
shown.performClose(sender)
|
|
234
|
-
} else {
|
|
235
|
-
showProjectsPopover()
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/// Dismiss the popover programmatically (e.g. from the pop-out button).
|
|
241
|
-
func dismissPopover() {
|
|
242
|
-
popover?.performClose(nil)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// Cached popover — built lazily on first click, reused on every subsequent open.
|
|
246
|
-
/// Keeping the SwiftUI view tree alive avoids rebuilding on each click (slow first paint).
|
|
247
|
-
/// Data refresh is driven from `popoverWillShow` + a notification MainView listens to.
|
|
248
|
-
private func makePopover() -> NSPopover {
|
|
249
|
-
if let p = popover { return p }
|
|
250
|
-
let t = DiagnosticLog.shared.startTimed("makePopover")
|
|
251
|
-
let p = NSPopover()
|
|
252
|
-
p.contentViewController = NSHostingController(rootView: MainView(scanner: ProjectScanner.shared))
|
|
253
|
-
p.behavior = .transient
|
|
254
|
-
p.contentSize = NSSize(width: 380, height: 300)
|
|
255
|
-
p.appearance = NSAppearance(named: .darkAqua)
|
|
256
|
-
p.delegate = self
|
|
257
|
-
popover = p
|
|
258
|
-
DiagnosticLog.shared.finish(t)
|
|
259
|
-
return p
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private func showProjectsPopover() {
|
|
263
|
-
guard let button = statusItem.button else { return }
|
|
264
|
-
let p = makePopover()
|
|
265
|
-
p.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
266
|
-
p.contentViewController?.view.window?.makeKey()
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
func popoverWillShow(_ notification: Notification) {
|
|
270
|
-
Self.updateActivationPolicy()
|
|
271
|
-
NotificationCenter.default.post(name: .latticesPopoverWillShow, object: nil)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
func popoverDidClose(_ notification: Notification) {
|
|
275
|
-
Self.updateActivationPolicy()
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// MARK: - Context menu
|
|
279
|
-
|
|
280
|
-
private func buildContextMenu() -> NSMenu {
|
|
281
|
-
let menu = NSMenu()
|
|
282
|
-
|
|
283
|
-
let actions: [(String, String, Selector)] = [
|
|
284
|
-
("Home", "", #selector(menuWorkspace)),
|
|
285
|
-
("Layout", "", #selector(menuLayout)),
|
|
286
|
-
("Search", "", #selector(menuSearch)),
|
|
287
|
-
("Command Palette", "⌘⇧M", #selector(menuCommandPalette)),
|
|
288
|
-
]
|
|
289
|
-
for (title, shortcut, action) in actions {
|
|
290
|
-
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
291
|
-
item.target = self
|
|
292
|
-
if !shortcut.isEmpty {
|
|
293
|
-
// Display-only; the actual hotkey is global
|
|
294
|
-
}
|
|
295
|
-
menu.addItem(item)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
menu.addItem(.separator())
|
|
299
|
-
|
|
300
|
-
let cliActions: [(String, Selector)] = [
|
|
301
|
-
("Projects…", #selector(menuProjects)),
|
|
302
|
-
("Initialize Project in Terminal…", #selector(menuInitializeProject)),
|
|
303
|
-
("Launch Project in Terminal…", #selector(menuLaunchProject)),
|
|
304
|
-
]
|
|
305
|
-
for (title, action) in cliActions {
|
|
306
|
-
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
307
|
-
item.target = self
|
|
308
|
-
menu.addItem(item)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
menu.addItem(.separator())
|
|
312
|
-
|
|
313
|
-
let update = NSMenuItem(title: "Update Lattices…", action: #selector(menuUpdate), keyEquivalent: "")
|
|
314
|
-
update.target = self
|
|
315
|
-
menu.addItem(update)
|
|
316
|
-
|
|
317
|
-
menu.addItem(.separator())
|
|
318
|
-
|
|
319
|
-
let settings = NSMenuItem(title: "Help & Settings…", action: #selector(menuSettings), keyEquivalent: ",")
|
|
320
|
-
settings.target = self
|
|
321
|
-
menu.addItem(settings)
|
|
322
|
-
|
|
323
|
-
menu.addItem(.separator())
|
|
324
|
-
|
|
325
|
-
let quit = NSMenuItem(title: "Quit Lattices", action: #selector(menuQuit), keyEquivalent: "q")
|
|
326
|
-
quit.target = self
|
|
327
|
-
menu.addItem(quit)
|
|
328
|
-
|
|
329
|
-
return menu
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
@objc private func menuCommandPalette() { CommandPaletteWindow.shared.toggle() }
|
|
333
|
-
@objc private func menuWorkspace() { ScreenMapWindowController.shared.showPage(.home) }
|
|
334
|
-
@objc private func menuLayout() { ScreenMapWindowController.shared.showPage(.screenMap) }
|
|
335
|
-
@objc private func menuSearch() { ScreenMapWindowController.shared.showPage(.desktopInventory) }
|
|
336
|
-
@objc private func menuDocs() { SettingsWindowController.shared.show() }
|
|
337
|
-
@objc private func menuProjects() { DispatchQueue.main.async { self.showProjectsPopover() } }
|
|
338
|
-
@objc private func menuInitializeProject() { CliActionLauncher.initializeProjectInTerminal() }
|
|
339
|
-
@objc private func menuLaunchProject() { CliActionLauncher.launchProjectInTerminal() }
|
|
340
|
-
@objc private func menuHUD() { HUDController.shared.toggle() }
|
|
341
|
-
@objc private func menuWindowBezel() { Self.showWorkspaceInspector() }
|
|
342
|
-
@objc private func menuCheatSheet() { SettingsWindowController.shared.show() }
|
|
343
|
-
@objc private func menuOmniSearch() { OmniSearchWindow.shared.toggle() }
|
|
344
|
-
@MainActor @objc private func menuUpdate() { AppUpdater.shared.promptForUpdate() }
|
|
345
|
-
@objc private func menuSettings() { SettingsWindowController.shared.show() }
|
|
346
|
-
@objc private func menuQuit() { NSApp.terminate(nil) }
|
|
347
|
-
|
|
348
|
-
// MARK: - Deep Links
|
|
349
|
-
|
|
350
|
-
private func registerDeepLinkHandler() {
|
|
351
|
-
NSAppleEventManager.shared().setEventHandler(
|
|
352
|
-
self,
|
|
353
|
-
andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
|
|
354
|
-
forEventClass: AEEventClass(kInternetEventClass),
|
|
355
|
-
andEventID: AEEventID(kAEGetURL)
|
|
356
|
-
)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
@objc private func handleGetURLEvent(
|
|
360
|
-
_ event: NSAppleEventDescriptor,
|
|
361
|
-
withReplyEvent replyEvent: NSAppleEventDescriptor
|
|
362
|
-
) {
|
|
363
|
-
guard
|
|
364
|
-
let value = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
|
|
365
|
-
let url = URL(string: value)
|
|
366
|
-
else {
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
handleDeepLink(url)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
private func handleDeepLink(_ url: URL) {
|
|
373
|
-
guard url.scheme?.localizedCaseInsensitiveCompare("lattices") == .orderedSame else {
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
let host = url.host?.lowercased()
|
|
378
|
-
let action = url.pathComponents
|
|
379
|
-
.first { $0 != "/" }?
|
|
380
|
-
.lowercased()
|
|
381
|
-
|
|
382
|
-
guard host == "companion" else {
|
|
383
|
-
SettingsWindowController.shared.show()
|
|
384
|
-
return
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
switch action {
|
|
388
|
-
case "enable", "start":
|
|
389
|
-
Preferences.shared.companionBridgeEnabled = true
|
|
390
|
-
SettingsWindowController.shared.showCompanion()
|
|
391
|
-
case "disable", "stop":
|
|
392
|
-
Preferences.shared.companionBridgeEnabled = false
|
|
393
|
-
SettingsWindowController.shared.showCompanion()
|
|
394
|
-
default:
|
|
395
|
-
SettingsWindowController.shared.showCompanion()
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
private static func showWorkspaceInspector() {
|
|
400
|
-
guard let entry = DesktopModel.shared.frontmostWindow(),
|
|
401
|
-
entry.app != "Lattices" else {
|
|
402
|
-
ScreenMapWindowController.shared.showPage(.screenMap)
|
|
403
|
-
return
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
ScreenMapWindowController.shared.showWindow(wid: entry.wid)
|
|
407
|
-
}
|
|
408
|
-
}
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import Combine
|
|
3
|
-
import CoreGraphics
|
|
4
|
-
|
|
5
|
-
final class KeyboardRemapController {
|
|
6
|
-
static let shared = KeyboardRemapController()
|
|
7
|
-
|
|
8
|
-
private static let syntheticMarker: Int64 = 0x4C4B524D
|
|
9
|
-
|
|
10
|
-
private var eventTap: CFMachPort?
|
|
11
|
-
private var runLoopSource: CFRunLoopSource?
|
|
12
|
-
private var subscriptions: Set<AnyCancellable> = []
|
|
13
|
-
private var installedObservers = false
|
|
14
|
-
private var capsLayerActive = false
|
|
15
|
-
private var capsUsedAsModifier = false
|
|
16
|
-
|
|
17
|
-
private init() {}
|
|
18
|
-
|
|
19
|
-
func start() {
|
|
20
|
-
installObserversIfNeeded()
|
|
21
|
-
refresh()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
func stop() {
|
|
25
|
-
removeEventTap()
|
|
26
|
-
capsLayerActive = false
|
|
27
|
-
capsUsedAsModifier = false
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private func installObserversIfNeeded() {
|
|
31
|
-
guard !installedObservers else { return }
|
|
32
|
-
installedObservers = true
|
|
33
|
-
|
|
34
|
-
Preferences.shared.$keyboardRemapsEnabled
|
|
35
|
-
.receive(on: RunLoop.main)
|
|
36
|
-
.sink { [weak self] _ in self?.refresh() }
|
|
37
|
-
.store(in: &subscriptions)
|
|
38
|
-
|
|
39
|
-
PermissionChecker.shared.$accessibility
|
|
40
|
-
.receive(on: RunLoop.main)
|
|
41
|
-
.sink { [weak self] _ in self?.refresh() }
|
|
42
|
-
.store(in: &subscriptions)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private func refresh() {
|
|
46
|
-
guard Preferences.shared.keyboardRemapsEnabled,
|
|
47
|
-
PermissionChecker.shared.accessibility else {
|
|
48
|
-
removeEventTap()
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
KeyboardRemapStore.shared.ensureConfigFile()
|
|
53
|
-
if eventTap == nil {
|
|
54
|
-
installEventTap()
|
|
55
|
-
} else if let eventTap {
|
|
56
|
-
CGEvent.tapEnable(tap: eventTap, enable: true)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
private func installEventTap() {
|
|
61
|
-
var mask = CGEventMask(0)
|
|
62
|
-
mask |= CGEventMask(1) << CGEventType.keyDown.rawValue
|
|
63
|
-
mask |= CGEventMask(1) << CGEventType.keyUp.rawValue
|
|
64
|
-
mask |= CGEventMask(1) << CGEventType.flagsChanged.rawValue
|
|
65
|
-
|
|
66
|
-
let tap = CGEvent.tapCreate(
|
|
67
|
-
tap: .cgSessionEventTap,
|
|
68
|
-
place: .headInsertEventTap,
|
|
69
|
-
options: .defaultTap,
|
|
70
|
-
eventsOfInterest: mask,
|
|
71
|
-
callback: Self.eventTapCallback,
|
|
72
|
-
userInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
guard let tap else {
|
|
76
|
-
DiagnosticLog.shared.warn("KeyboardRemap: failed to install keyboard event tap")
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
81
|
-
eventTap = tap
|
|
82
|
-
runLoopSource = source
|
|
83
|
-
|
|
84
|
-
if let source {
|
|
85
|
-
CFRunLoopAddSource(CFRunLoopGetMain(), source, .commonModes)
|
|
86
|
-
}
|
|
87
|
-
CGEvent.tapEnable(tap: tap, enable: true)
|
|
88
|
-
DiagnosticLog.shared.info("KeyboardRemap: keyboard event tap installed")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private func removeEventTap() {
|
|
92
|
-
if let source = runLoopSource {
|
|
93
|
-
CFRunLoopRemoveSource(CFRunLoopGetMain(), source, .commonModes)
|
|
94
|
-
}
|
|
95
|
-
runLoopSource = nil
|
|
96
|
-
if let tap = eventTap {
|
|
97
|
-
CFMachPortInvalidate(tap)
|
|
98
|
-
}
|
|
99
|
-
eventTap = nil
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private static let eventTapCallback: CGEventTapCallBack = { _, type, event, userInfo in
|
|
103
|
-
guard let userInfo else { return Unmanaged.passUnretained(event) }
|
|
104
|
-
let controller = Unmanaged<KeyboardRemapController>.fromOpaque(userInfo).takeUnretainedValue()
|
|
105
|
-
return controller.handleEvent(type: type, event: event)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private func handleEvent(type: CGEventType, event: CGEvent) -> Unmanaged<CGEvent>? {
|
|
109
|
-
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
|
110
|
-
if let eventTap {
|
|
111
|
-
CGEvent.tapEnable(tap: eventTap, enable: true)
|
|
112
|
-
}
|
|
113
|
-
return Unmanaged.passUnretained(event)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if event.getIntegerValueField(.eventSourceUserData) == Self.syntheticMarker {
|
|
117
|
-
return Unmanaged.passUnretained(event)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
KeyboardRemapStore.shared.reloadIfNeeded()
|
|
121
|
-
guard let rule = KeyboardRemapStore.shared.capsLockRule,
|
|
122
|
-
rule.toIfHeld == .hyper else {
|
|
123
|
-
return Unmanaged.passUnretained(event)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
|
|
127
|
-
if type == .flagsChanged, keyCode == rule.from.keyCode {
|
|
128
|
-
return handleCapsLockFlagsChanged(event, rule: rule)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
guard capsLayerActive else {
|
|
132
|
-
return Unmanaged.passUnretained(event)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
switch type {
|
|
136
|
-
case .keyDown:
|
|
137
|
-
capsUsedAsModifier = true
|
|
138
|
-
event.flags = normalizedFlags(event.flags).union(.latticesHyper)
|
|
139
|
-
return Unmanaged.passUnretained(event)
|
|
140
|
-
case .keyUp:
|
|
141
|
-
event.flags = normalizedFlags(event.flags).union(.latticesHyper)
|
|
142
|
-
return Unmanaged.passUnretained(event)
|
|
143
|
-
default:
|
|
144
|
-
return Unmanaged.passUnretained(event)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private func handleCapsLockFlagsChanged(_ event: CGEvent, rule: KeyboardRemapRule) -> Unmanaged<CGEvent>? {
|
|
149
|
-
let isDown = event.flags.contains(.maskAlphaShift)
|
|
150
|
-
if isDown {
|
|
151
|
-
capsLayerActive = true
|
|
152
|
-
capsUsedAsModifier = false
|
|
153
|
-
DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer active")
|
|
154
|
-
} else {
|
|
155
|
-
let shouldTap = capsLayerActive && !capsUsedAsModifier && rule.toIfAlone == .escape
|
|
156
|
-
capsLayerActive = false
|
|
157
|
-
capsUsedAsModifier = false
|
|
158
|
-
if shouldTap {
|
|
159
|
-
postKeyTap(keyCode: 53)
|
|
160
|
-
}
|
|
161
|
-
DiagnosticLog.shared.info("KeyboardRemap: Caps Lock layer inactive")
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return nil
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private func normalizedFlags(_ flags: CGEventFlags) -> CGEventFlags {
|
|
168
|
-
var normalized = flags
|
|
169
|
-
normalized.remove(.maskAlphaShift)
|
|
170
|
-
return normalized
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private func postKeyTap(keyCode: CGKeyCode) {
|
|
174
|
-
guard let source = CGEventSource(stateID: .combinedSessionState),
|
|
175
|
-
let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
|
|
176
|
-
let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) else {
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
down.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
|
|
180
|
-
up.setIntegerValueField(.eventSourceUserData, value: Self.syntheticMarker)
|
|
181
|
-
down.post(tap: .cghidEventTap)
|
|
182
|
-
up.post(tap: .cghidEventTap)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
private var lastLoadedModifiedDate: Date?
|
|
12
|
-
|
|
13
|
-
private init() {
|
|
14
|
-
let dir = FileManager.default.homeDirectoryForCurrentUser
|
|
15
|
-
.appendingPathComponent(".lattices")
|
|
16
|
-
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
17
|
-
self.configURL = dir.appendingPathComponent("keyboard-remaps.json")
|
|
18
|
-
self.config = .defaults
|
|
19
|
-
ensureConfigFile()
|
|
20
|
-
reload()
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
var enabledRules: [KeyboardRemapRule] {
|
|
24
|
-
config.rules.filter(\.enabled)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
var summaryLines: [String] {
|
|
28
|
-
enabledRules.map(\.summaryLine)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
var capsLockRule: KeyboardRemapRule? {
|
|
32
|
-
enabledRules.first { $0.from == .capsLock }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func ensureConfigFile() {
|
|
36
|
-
guard !FileManager.default.fileExists(atPath: configURL.path) else { return }
|
|
37
|
-
write(config: .defaults)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func reload() {
|
|
41
|
-
guard let data = FileManager.default.contents(atPath: configURL.path) else {
|
|
42
|
-
config = .defaults
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
do {
|
|
47
|
-
config = try JSONDecoder().decode(KeyboardRemapConfig.self, from: data)
|
|
48
|
-
lastLoadedModifiedDate = modifiedDate()
|
|
49
|
-
} catch {
|
|
50
|
-
DiagnosticLog.shared.error("KeyboardRemapStore: failed to decode keyboard-remaps.json - \(error.localizedDescription)")
|
|
51
|
-
config = .defaults
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func reloadIfNeeded() {
|
|
56
|
-
let currentModifiedDate = modifiedDate()
|
|
57
|
-
guard currentModifiedDate != lastLoadedModifiedDate else { return }
|
|
58
|
-
reload()
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func restoreDefaults() {
|
|
62
|
-
write(config: .defaults)
|
|
63
|
-
reload()
|
|
64
|
-
DiagnosticLog.shared.info("Keyboard remaps restored to defaults")
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
func openConfiguration() {
|
|
68
|
-
ensureConfigFile()
|
|
69
|
-
NSWorkspace.shared.open(configURL)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private func write(config: KeyboardRemapConfig) {
|
|
73
|
-
let encoder = JSONEncoder()
|
|
74
|
-
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
75
|
-
guard let data = try? encoder.encode(config) else { return }
|
|
76
|
-
try? data.write(to: configURL, options: .atomic)
|
|
77
|
-
lastLoadedModifiedDate = modifiedDate()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private func modifiedDate() -> Date? {
|
|
81
|
-
let attrs = try? FileManager.default.attributesOfItem(atPath: configURL.path)
|
|
82
|
-
return attrs?[.modificationDate] as? Date
|
|
83
|
-
}
|
|
84
|
-
}
|