@arach/lattices 0.2.0 → 0.6.1

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 (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -86
  3. package/apps/mac/Info.plist +43 -0
  4. package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
  5. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  6. package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  7. package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
  8. package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
  9. package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
  10. package/apps/mac/Lattices.entitlements +21 -0
  11. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  12. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  13. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  14. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  15. package/apps/mac/Resources/tap.wav +0 -0
  16. package/assets/AppIcon.icns +0 -0
  17. package/bin/assistant-intelligence.ts +912 -0
  18. package/bin/cli/capture.ts +252 -0
  19. package/bin/cli/daemon.ts +22 -0
  20. package/bin/cli/helpers.ts +105 -0
  21. package/bin/cli/layer.ts +178 -0
  22. package/bin/cli/runs.ts +43 -0
  23. package/bin/cli/search.ts +141 -0
  24. package/bin/cli/session.ts +32 -0
  25. package/bin/client.ts +17 -0
  26. package/bin/cua.ts +26 -0
  27. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  28. package/bin/handsoff-infer.ts +96 -0
  29. package/bin/handsoff-worker.ts +531 -0
  30. package/bin/infer.ts +424 -0
  31. package/bin/keychain.ts +75 -0
  32. package/bin/lattices-app.ts +655 -0
  33. package/bin/lattices-build +125 -0
  34. package/bin/lattices-build-env.ts +77 -0
  35. package/bin/lattices-dev +362 -0
  36. package/bin/lattices.ts +3260 -0
  37. package/bin/project-twin.ts +645 -0
  38. package/docs/agent-execution-plan.md +562 -0
  39. package/docs/agent-layer-guide.md +207 -0
  40. package/docs/agents.md +233 -0
  41. package/docs/ai-chat-ux-review.md +416 -0
  42. package/docs/api.md +1041 -47
  43. package/docs/app.md +96 -13
  44. package/docs/assistant-knowledge.md +130 -0
  45. package/docs/companion-deck.md +209 -0
  46. package/docs/component-extraction-roadmap.md +392 -0
  47. package/docs/concepts.md +13 -12
  48. package/docs/config.md +83 -10
  49. package/docs/gesture-customization-proposal.md +520 -0
  50. package/docs/handsoff-test-scenarios.md +84 -0
  51. package/docs/hyperspace-grid-snappiness.md +210 -0
  52. package/docs/layers.md +176 -28
  53. package/docs/mouse-gestures.md +244 -0
  54. package/docs/ocr.md +21 -9
  55. package/docs/overview.md +42 -23
  56. package/docs/presentation-execution-review.md +491 -0
  57. package/docs/prompts/hands-off-system.md +382 -0
  58. package/docs/prompts/hands-off-turn.md +30 -0
  59. package/docs/prompts/voice-advisor.md +31 -0
  60. package/docs/prompts/voice-fallback.md +23 -0
  61. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  62. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  63. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  64. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  65. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  66. package/docs/proposals/LAT-006-followup-gaps.md +103 -0
  67. package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
  68. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  69. package/docs/quickstart.md +8 -12
  70. package/docs/reference/dewey.config.ts +74 -0
  71. package/docs/reference/install-agent.md +79 -0
  72. package/docs/release.md +172 -0
  73. package/docs/repo-structure.md +100 -0
  74. package/docs/terminal-kit.md +87 -0
  75. package/docs/tiling-reference.md +224 -0
  76. package/docs/twins.md +138 -0
  77. package/docs/voice-command-protocol.md +278 -0
  78. package/docs/voice-error-model.md +73 -0
  79. package/docs/voice.md +221 -0
  80. package/package.json +69 -16
  81. package/packages/npm/sdk/cua.d.mts +1 -0
  82. package/packages/npm/sdk/cua.d.ts +188 -0
  83. package/packages/npm/sdk/cua.mjs +376 -0
  84. package/app/Lattices.app/Contents/Info.plist +0 -24
  85. package/app/Package.swift +0 -13
  86. package/app/Sources/ActionRow.swift +0 -61
  87. package/app/Sources/App.swift +0 -10
  88. package/app/Sources/AppDelegate.swift +0 -234
  89. package/app/Sources/AppShellView.swift +0 -62
  90. package/app/Sources/AppTypeClassifier.swift +0 -70
  91. package/app/Sources/AppWindowShell.swift +0 -63
  92. package/app/Sources/CheatSheetHUD.swift +0 -332
  93. package/app/Sources/CommandModeState.swift +0 -1362
  94. package/app/Sources/CommandModeView.swift +0 -1405
  95. package/app/Sources/CommandModeWindow.swift +0 -192
  96. package/app/Sources/CommandPaletteView.swift +0 -307
  97. package/app/Sources/CommandPaletteWindow.swift +0 -134
  98. package/app/Sources/DaemonProtocol.swift +0 -101
  99. package/app/Sources/DaemonServer.swift +0 -414
  100. package/app/Sources/DesktopModel.swift +0 -121
  101. package/app/Sources/DesktopModelTypes.swift +0 -71
  102. package/app/Sources/DiagnosticLog.swift +0 -271
  103. package/app/Sources/EventBus.swift +0 -30
  104. package/app/Sources/HotkeyManager.swift +0 -250
  105. package/app/Sources/HotkeyStore.swift +0 -338
  106. package/app/Sources/InventoryManager.swift +0 -35
  107. package/app/Sources/InventoryPath.swift +0 -43
  108. package/app/Sources/KeyRecorderView.swift +0 -210
  109. package/app/Sources/LatticesApi.swift +0 -1125
  110. package/app/Sources/MainView.swift +0 -467
  111. package/app/Sources/MainWindow.swift +0 -83
  112. package/app/Sources/OcrModel.swift +0 -309
  113. package/app/Sources/OcrStore.swift +0 -295
  114. package/app/Sources/OmniSearchState.swift +0 -283
  115. package/app/Sources/OmniSearchView.swift +0 -288
  116. package/app/Sources/OmniSearchWindow.swift +0 -105
  117. package/app/Sources/OrphanRow.swift +0 -129
  118. package/app/Sources/PaletteCommand.swift +0 -419
  119. package/app/Sources/PermissionChecker.swift +0 -125
  120. package/app/Sources/Preferences.swift +0 -92
  121. package/app/Sources/ProcessModel.swift +0 -199
  122. package/app/Sources/ProcessQuery.swift +0 -151
  123. package/app/Sources/Project.swift +0 -28
  124. package/app/Sources/ProjectRow.swift +0 -368
  125. package/app/Sources/ProjectScanner.swift +0 -121
  126. package/app/Sources/ScreenMapState.swift +0 -2387
  127. package/app/Sources/ScreenMapView.swift +0 -2820
  128. package/app/Sources/ScreenMapWindowController.swift +0 -89
  129. package/app/Sources/SessionManager.swift +0 -72
  130. package/app/Sources/SettingsView.swift +0 -1053
  131. package/app/Sources/SettingsWindow.swift +0 -20
  132. package/app/Sources/TabGroupRow.swift +0 -178
  133. package/app/Sources/Terminal.swift +0 -259
  134. package/app/Sources/TerminalQuery.swift +0 -156
  135. package/app/Sources/TerminalSynthesizer.swift +0 -200
  136. package/app/Sources/Theme.swift +0 -163
  137. package/app/Sources/TilePickerView.swift +0 -209
  138. package/app/Sources/TmuxModel.swift +0 -53
  139. package/app/Sources/TmuxQuery.swift +0 -81
  140. package/app/Sources/WindowTiler.swift +0 -1755
  141. package/app/Sources/WorkspaceManager.swift +0 -434
  142. package/bin/lattices-app.js +0 -221
  143. package/bin/lattices.js +0 -1418
@@ -1,271 +0,0 @@
1
- import AppKit
2
- import SwiftUI
3
-
4
- // MARK: - Log Store
5
-
6
- final class DiagnosticLog: ObservableObject {
7
- static let shared = DiagnosticLog()
8
-
9
- struct Entry: Identifiable {
10
- let id = UUID()
11
- let time: Date
12
- let message: String
13
- let level: Level
14
-
15
- enum Level { case info, success, warning, error }
16
-
17
- var icon: String {
18
- switch level {
19
- case .info: return "›"
20
- case .success: return "✓"
21
- case .warning: return "⚠"
22
- case .error: return "✗"
23
- }
24
- }
25
- }
26
-
27
- @Published var entries: [Entry] = []
28
- private let maxEntries = 80
29
-
30
- func log(_ message: String, level: Entry.Level = .info) {
31
- let entry = Entry(time: Date(), message: message, level: level)
32
- DispatchQueue.main.async {
33
- self.entries.append(entry)
34
- if self.entries.count > self.maxEntries {
35
- self.entries.removeFirst(self.entries.count - self.maxEntries)
36
- }
37
- }
38
- }
39
-
40
- func info(_ msg: String) { log(msg, level: .info) }
41
- func success(_ msg: String) { log(msg, level: .success) }
42
- func warn(_ msg: String) { log(msg, level: .warning) }
43
- func error(_ msg: String) { log(msg, level: .error) }
44
- func clear() { DispatchQueue.main.async { self.entries.removeAll() } }
45
-
46
- // MARK: - Per-Action Timing
47
-
48
- struct TimedAction {
49
- let label: String
50
- let start: Date
51
- }
52
-
53
- func startTimed(_ label: String) -> TimedAction {
54
- info("▸ \(label)")
55
- return TimedAction(label: label, start: Date())
56
- }
57
-
58
- func finish(_ action: TimedAction) {
59
- let ms = Date().timeIntervalSince(action.start) * 1000
60
- success("▸ \(action.label) — \(String(format: "%.0f", ms))ms")
61
- }
62
- }
63
-
64
- // MARK: - Diagnostic Window
65
-
66
- final class DiagnosticWindow {
67
- static let shared = DiagnosticWindow()
68
-
69
- private var window: NSWindow?
70
- private var keyMonitor: Any?
71
- private let log = DiagnosticLog.shared
72
-
73
- var isVisible: Bool { window?.isVisible ?? false }
74
-
75
- func toggle() {
76
- if let w = window, w.isVisible {
77
- dismiss()
78
- } else {
79
- show()
80
- }
81
- }
82
-
83
- func dismiss() {
84
- window?.orderOut(nil)
85
- if let monitor = keyMonitor {
86
- NSEvent.removeMonitor(monitor)
87
- keyMonitor = nil
88
- }
89
- }
90
-
91
- func show() {
92
- if let w = window {
93
- w.orderFrontRegardless()
94
- return
95
- }
96
-
97
- let view = DiagnosticOverlayView()
98
-
99
- let hosting = NSHostingController(rootView: view)
100
- let screen = NSScreen.main
101
- let screenFrame = screen?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1920, height: 1080)
102
- let panelWidth: CGFloat = 480
103
- let panelHeight: CGFloat = max(600, floor(screenFrame.height * 0.55))
104
- hosting.preferredContentSize = NSSize(width: panelWidth, height: panelHeight)
105
-
106
- let w = NSPanel(
107
- contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight),
108
- styleMask: [.titled, .closable, .resizable, .utilityWindow, .nonactivatingPanel],
109
- backing: .buffered,
110
- defer: false
111
- )
112
- w.contentViewController = hosting
113
- w.title = "Lattices Diagnostics"
114
- w.titlebarAppearsTransparent = true
115
- w.isMovableByWindowBackground = true
116
- w.level = .floating
117
- w.isOpaque = false
118
- w.backgroundColor = NSColor(red: 0.1, green: 0.1, blue: 0.12, alpha: 1.0)
119
- w.hasShadow = true
120
- w.alphaValue = 1.0
121
- w.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
122
-
123
- // Position: right edge, vertically centered
124
- let x = screenFrame.maxX - panelWidth - 12
125
- let y = screenFrame.minY + floor((screenFrame.height - panelHeight) / 2)
126
- w.setFrameOrigin(NSPoint(x: x, y: y))
127
-
128
- w.orderFrontRegardless()
129
- window = w
130
-
131
- // Escape key → dismiss
132
- keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
133
- guard event.keyCode == 53,
134
- let win = self?.window,
135
- event.window === win || win.isKeyWindow else { return event }
136
- self?.dismiss()
137
- return nil
138
- }
139
-
140
- // Startup log
141
- let diag = DiagnosticLog.shared
142
- diag.info("Diagnostics opened")
143
- diag.info("Terminal: \(Preferences.shared.terminal.rawValue) (\(Preferences.shared.terminal.bundleId))")
144
- diag.info("Installed: \(Terminal.installed.map(\.rawValue).joined(separator: ", "))")
145
-
146
- // Show running sessions
147
- let task = Process()
148
- task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/tmux")
149
- task.arguments = ["list-sessions", "-F", "#{session_name}"]
150
- let pipe = Pipe()
151
- task.standardOutput = pipe
152
- task.standardError = FileHandle.nullDevice
153
- try? task.run()
154
- task.waitUntilExit()
155
- let sessions = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "none"
156
- diag.info("tmux sessions: \(sessions)")
157
- }
158
- }
159
-
160
- // MARK: - SwiftUI Overlay
161
-
162
- struct DiagnosticOverlayView: View {
163
- @StateObject private var log = DiagnosticLog.shared
164
- @State private var autoScroll = true
165
- @State private var refreshTick = 0
166
-
167
- private static let timeFmt: DateFormatter = {
168
- let f = DateFormatter()
169
- f.dateFormat = "HH:mm:ss.SSS"
170
- return f
171
- }()
172
-
173
- // Fallback timer to catch any missed updates
174
- private let refreshTimer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
175
-
176
- var body: some View {
177
- VStack(spacing: 0) {
178
- // Header
179
- HStack {
180
- Text("DIAGNOSTICS")
181
- .font(.system(size: 10, weight: .bold, design: .monospaced))
182
- .foregroundColor(.green.opacity(0.8))
183
- Spacer()
184
- let _ = refreshTick // force re-render on timer
185
- Text("\(log.entries.count) events")
186
- .font(.system(size: 9, design: .monospaced))
187
- .foregroundColor(.white.opacity(0.4))
188
- Button("Copy") {
189
- let text = log.entries.map { entry in
190
- let t = Self.timeFmt.string(from: entry.time)
191
- return "\(t) \(entry.icon) \(entry.message)"
192
- }.joined(separator: "\n")
193
- NSPasteboard.general.clearContents()
194
- NSPasteboard.general.setString(text, forType: .string)
195
- }
196
- .font(.system(size: 9, design: .monospaced))
197
- .foregroundColor(.white.opacity(0.5))
198
- .buttonStyle(.plain)
199
- Button("Clear") { log.clear() }
200
- .font(.system(size: 9, design: .monospaced))
201
- .foregroundColor(.white.opacity(0.5))
202
- .buttonStyle(.plain)
203
- }
204
- .padding(.horizontal, 10)
205
- .padding(.vertical, 6)
206
- .background(Color.black.opacity(0.3))
207
- .onReceive(refreshTimer) { _ in refreshTick += 1 }
208
-
209
- // Log entries
210
- ScrollViewReader { proxy in
211
- ScrollView {
212
- LazyVStack(alignment: .leading, spacing: 1) {
213
- ForEach(log.entries) { entry in
214
- logRow(entry)
215
- .id(entry.id)
216
- }
217
- }
218
- .padding(.horizontal, 8)
219
- .padding(.vertical, 4)
220
- }
221
- .onChange(of: log.entries.count) { _ in
222
- if autoScroll, let last = log.entries.last {
223
- withAnimation(.easeOut(duration: 0.1)) {
224
- proxy.scrollTo(last.id, anchor: .bottom)
225
- }
226
- }
227
- }
228
- }
229
- }
230
- .frame(minWidth: 420, idealWidth: 480, minHeight: 400, idealHeight: 600)
231
- .background(Color.black.opacity(0.75))
232
- }
233
-
234
- private func logRow(_ entry: DiagnosticLog.Entry) -> some View {
235
- HStack(alignment: .top, spacing: 6) {
236
- Text(Self.timeFmt.string(from: entry.time))
237
- .font(.system(size: 9, design: .monospaced))
238
- .foregroundColor(.white.opacity(0.3))
239
-
240
- Text(entry.icon)
241
- .font(.system(size: 9, design: .monospaced))
242
- .foregroundColor(iconColor(entry.level))
243
- .frame(width: 10)
244
-
245
- Text(entry.message)
246
- .font(.system(size: 10, design: .monospaced))
247
- .foregroundColor(textColor(entry.level))
248
- .lineLimit(3)
249
- .fixedSize(horizontal: false, vertical: true)
250
- }
251
- .padding(.vertical, 1)
252
- }
253
-
254
- private func iconColor(_ level: DiagnosticLog.Entry.Level) -> Color {
255
- switch level {
256
- case .info: return .white.opacity(0.5)
257
- case .success: return .green
258
- case .warning: return .yellow
259
- case .error: return .red
260
- }
261
- }
262
-
263
- private func textColor(_ level: DiagnosticLog.Entry.Level) -> Color {
264
- switch level {
265
- case .info: return .white.opacity(0.7)
266
- case .success: return .green.opacity(0.9)
267
- case .warning: return .yellow.opacity(0.9)
268
- case .error: return .red.opacity(0.9)
269
- }
270
- }
271
- }
@@ -1,30 +0,0 @@
1
- import Foundation
2
-
3
- enum ModelEvent {
4
- case windowsChanged(windows: [WindowEntry], added: [UInt32], removed: [UInt32])
5
- case tmuxChanged(sessions: [TmuxSession])
6
- case layerSwitched(index: Int)
7
- case processesChanged(interesting: [Int])
8
- case ocrScanComplete(windowCount: Int, totalBlocks: Int)
9
- }
10
-
11
- final class EventBus {
12
- static let shared = EventBus()
13
- private var handlers: [(ModelEvent) -> Void] = []
14
- private let lock = NSLock()
15
-
16
- func subscribe(_ handler: @escaping (ModelEvent) -> Void) {
17
- lock.lock()
18
- handlers.append(handler)
19
- lock.unlock()
20
- }
21
-
22
- func post(_ event: ModelEvent) {
23
- lock.lock()
24
- let copy = handlers
25
- lock.unlock()
26
- for handler in copy {
27
- handler(event)
28
- }
29
- }
30
- }
@@ -1,250 +0,0 @@
1
- import Carbon
2
- import AppKit
3
- import Foundation
4
-
5
- /// Global callback registry keyed by hotkey ID
6
- private var hotkeyCallbacks: [UInt32: () -> Void] = [:]
7
-
8
- /// Whether the global Carbon event handler has been installed
9
- private var eventHandlerInstalled = false
10
-
11
- class HotkeyManager {
12
- static let shared = HotkeyManager()
13
- private var hotKeyRefs: [UInt32: EventHotKeyRef] = [:]
14
-
15
- private func ensureEventHandler() {
16
- guard !eventHandlerInstalled else { return }
17
- eventHandlerInstalled = true
18
-
19
- var eventType = EventTypeSpec(
20
- eventClass: OSType(kEventClassKeyboard),
21
- eventKind: UInt32(kEventHotKeyPressed)
22
- )
23
-
24
- InstallEventHandler(
25
- GetApplicationEventTarget(),
26
- { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
27
- guard let event else { return OSStatus(eventNotHandledErr) }
28
- var hotkeyID = EventHotKeyID()
29
- GetEventParameter(
30
- event,
31
- EventParamName(kEventParamDirectObject),
32
- EventParamType(typeEventHotKeyID),
33
- nil,
34
- MemoryLayout<EventHotKeyID>.size,
35
- nil,
36
- &hotkeyID
37
- )
38
- hotkeyCallbacks[hotkeyID.id]?()
39
- return noErr
40
- },
41
- 1,
42
- &eventType,
43
- nil,
44
- nil
45
- )
46
- }
47
-
48
- /// Register Cmd+Shift+M as the global hotkey (palette toggle)
49
- func register(callback: @escaping () -> Void) {
50
- ensureEventHandler()
51
-
52
- let id: UInt32 = 1
53
- hotkeyCallbacks[id] = callback
54
-
55
- let hotKeyID = EventHotKeyID(
56
- signature: OSType(0x444D5558), // "DMUX"
57
- id: id
58
- )
59
-
60
- var ref: EventHotKeyRef?
61
- RegisterEventHotKey(
62
- 46, // 'M'
63
- UInt32(cmdKey | shiftKey),
64
- hotKeyID,
65
- GetApplicationEventTarget(),
66
- 0,
67
- &ref
68
- )
69
- if let ref { hotKeyRefs[id] = ref }
70
- }
71
-
72
- /// Register Hyper+1 (Cmd+Ctrl+Option+Shift+1) for command mode
73
- func registerCommandMode(callback: @escaping () -> Void) {
74
- ensureEventHandler()
75
- let id: UInt32 = 200
76
- hotkeyCallbacks[id] = callback
77
- let hotKeyID = EventHotKeyID(
78
- signature: OSType(0x444D5558), // "DMUX"
79
- id: id
80
- )
81
- var ref: EventHotKeyRef?
82
- RegisterEventHotKey(
83
- 18, // '1' key
84
- UInt32(cmdKey | controlKey | optionKey | shiftKey), // Hyper
85
- hotKeyID,
86
- GetApplicationEventTarget(),
87
- 0,
88
- &ref
89
- )
90
- if let ref { hotKeyRefs[id] = ref }
91
- }
92
-
93
- /// Register Hyper+2 (Cmd+Ctrl+Option+Shift+2) for bezel mode
94
- func registerBezelHotkey(callback: @escaping () -> Void) {
95
- ensureEventHandler()
96
- let id: UInt32 = 201
97
- hotkeyCallbacks[id] = callback
98
- let hotKeyID = EventHotKeyID(
99
- signature: OSType(0x444D5558), // "DMUX"
100
- id: id
101
- )
102
- var ref: EventHotKeyRef?
103
- RegisterEventHotKey(
104
- 19, // '2' key
105
- UInt32(cmdKey | controlKey | optionKey | shiftKey), // Hyper
106
- hotKeyID,
107
- GetApplicationEventTarget(),
108
- 0,
109
- &ref
110
- )
111
- if let ref { hotKeyRefs[id] = ref }
112
- }
113
-
114
- /// Register Cmd+Option+1/2/3... hotkeys for layer switching
115
- func registerLayerHotkeys(count: Int, callback: @escaping (Int) -> Void) {
116
- ensureEventHandler()
117
-
118
- // Key codes for number keys 1-9
119
- let keyCodes: [UInt32] = [18, 19, 20, 21, 23, 22, 26, 28, 25]
120
- let limit = min(count, keyCodes.count)
121
-
122
- for i in 0..<limit {
123
- let id: UInt32 = 101 + UInt32(i)
124
-
125
- // Unregister existing if re-registering
126
- if let existing = hotKeyRefs[id] {
127
- UnregisterEventHotKey(existing)
128
- hotKeyRefs.removeValue(forKey: id)
129
- }
130
-
131
- let layerIndex = i
132
- hotkeyCallbacks[id] = { callback(layerIndex) }
133
-
134
- let hotKeyID = EventHotKeyID(
135
- signature: OSType(0x444D5558), // "DMUX"
136
- id: id
137
- )
138
-
139
- var ref: EventHotKeyRef?
140
- RegisterEventHotKey(
141
- keyCodes[i],
142
- UInt32(cmdKey | optionKey),
143
- hotKeyID,
144
- GetApplicationEventTarget(),
145
- 0,
146
- &ref
147
- )
148
- if let ref { hotKeyRefs[id] = ref }
149
- }
150
- }
151
-
152
- /// Register a single global hotkey with a given ID, key code, and Carbon modifier mask
153
- func registerSingle(id: UInt32, keyCode: UInt32, modifiers: UInt32, callback: @escaping () -> Void) {
154
- ensureEventHandler()
155
-
156
- if let existing = hotKeyRefs[id] {
157
- UnregisterEventHotKey(existing)
158
- hotKeyRefs.removeValue(forKey: id)
159
- }
160
-
161
- hotkeyCallbacks[id] = callback
162
-
163
- let hotKeyID = EventHotKeyID(
164
- signature: OSType(0x444D5558), // "DMUX"
165
- id: id
166
- )
167
-
168
- var ref: EventHotKeyRef?
169
- RegisterEventHotKey(
170
- keyCode,
171
- modifiers,
172
- hotKeyID,
173
- GetApplicationEventTarget(),
174
- 0,
175
- &ref
176
- )
177
- if let ref { hotKeyRefs[id] = ref }
178
- }
179
-
180
- /// Unregister all global hotkeys and clear callbacks
181
- func unregisterAll() {
182
- for (id, ref) in hotKeyRefs {
183
- UnregisterEventHotKey(ref)
184
- hotkeyCallbacks.removeValue(forKey: id)
185
- }
186
- hotKeyRefs.removeAll()
187
- }
188
-
189
- /// Register Ctrl+Option window tiling hotkeys (Magnet-style)
190
- func registerTileHotkeys() {
191
- let mods = UInt32(controlKey | optionKey)
192
-
193
- // Ctrl+Option+← → left
194
- registerSingle(id: 300, keyCode: 123, modifiers: mods) {
195
- WindowTiler.tileFrontmostViaAX(to: .left)
196
- }
197
- // Ctrl+Option+→ → right
198
- registerSingle(id: 301, keyCode: 124, modifiers: mods) {
199
- WindowTiler.tileFrontmostViaAX(to: .right)
200
- }
201
- // Ctrl+Option+Return → maximize
202
- registerSingle(id: 302, keyCode: 36, modifiers: mods) {
203
- WindowTiler.tileFrontmostViaAX(to: .maximize)
204
- }
205
- // Ctrl+Option+C → center
206
- registerSingle(id: 303, keyCode: 8, modifiers: mods) {
207
- WindowTiler.tileFrontmostViaAX(to: .center)
208
- }
209
- // Ctrl+Option+U → top-left
210
- registerSingle(id: 304, keyCode: 32, modifiers: mods) {
211
- WindowTiler.tileFrontmostViaAX(to: .topLeft)
212
- }
213
- // Ctrl+Option+I → top-right
214
- registerSingle(id: 305, keyCode: 34, modifiers: mods) {
215
- WindowTiler.tileFrontmostViaAX(to: .topRight)
216
- }
217
- // Ctrl+Option+J → bottom-left
218
- registerSingle(id: 306, keyCode: 38, modifiers: mods) {
219
- WindowTiler.tileFrontmostViaAX(to: .bottomLeft)
220
- }
221
- // Ctrl+Option+K → bottom-right
222
- registerSingle(id: 307, keyCode: 40, modifiers: mods) {
223
- WindowTiler.tileFrontmostViaAX(to: .bottomRight)
224
- }
225
- // Ctrl+Option+↑ → top
226
- registerSingle(id: 308, keyCode: 126, modifiers: mods) {
227
- WindowTiler.tileFrontmostViaAX(to: .top)
228
- }
229
- // Ctrl+Option+↓ → bottom
230
- registerSingle(id: 309, keyCode: 125, modifiers: mods) {
231
- WindowTiler.tileFrontmostViaAX(to: .bottom)
232
- }
233
- // Ctrl+Option+D → distribute visible windows
234
- registerSingle(id: 310, keyCode: 2, modifiers: mods) {
235
- WindowTiler.distributeVisible()
236
- }
237
- // Ctrl+Option+1 → left third
238
- registerSingle(id: 311, keyCode: 18, modifiers: mods) {
239
- WindowTiler.tileFrontmostViaAX(to: .leftThird)
240
- }
241
- // Ctrl+Option+2 → center third
242
- registerSingle(id: 312, keyCode: 19, modifiers: mods) {
243
- WindowTiler.tileFrontmostViaAX(to: .centerThird)
244
- }
245
- // Ctrl+Option+3 → right third
246
- registerSingle(id: 313, keyCode: 20, modifiers: mods) {
247
- WindowTiler.tileFrontmostViaAX(to: .rightThird)
248
- }
249
- }
250
- }