@arach/lattices 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +157 -0
  2. package/app/Lattices.app/Contents/Info.plist +24 -0
  3. package/app/Package.swift +13 -0
  4. package/app/Sources/App.swift +49 -0
  5. package/app/Sources/AppDelegate.swift +104 -0
  6. package/app/Sources/AppShellView.swift +62 -0
  7. package/app/Sources/AppTypeClassifier.swift +70 -0
  8. package/app/Sources/AppWindowShell.swift +63 -0
  9. package/app/Sources/CheatSheetHUD.swift +331 -0
  10. package/app/Sources/CommandModeState.swift +1341 -0
  11. package/app/Sources/CommandModeView.swift +1380 -0
  12. package/app/Sources/CommandModeWindow.swift +192 -0
  13. package/app/Sources/CommandPaletteView.swift +307 -0
  14. package/app/Sources/CommandPaletteWindow.swift +134 -0
  15. package/app/Sources/DaemonProtocol.swift +101 -0
  16. package/app/Sources/DaemonServer.swift +406 -0
  17. package/app/Sources/DesktopModel.swift +121 -0
  18. package/app/Sources/DesktopModelTypes.swift +71 -0
  19. package/app/Sources/DiagnosticLog.swift +253 -0
  20. package/app/Sources/EventBus.swift +29 -0
  21. package/app/Sources/HotkeyManager.swift +249 -0
  22. package/app/Sources/HotkeyStore.swift +330 -0
  23. package/app/Sources/InventoryManager.swift +35 -0
  24. package/app/Sources/InventoryPath.swift +43 -0
  25. package/app/Sources/KeyRecorderView.swift +210 -0
  26. package/app/Sources/LatticesApi.swift +915 -0
  27. package/app/Sources/MainView.swift +507 -0
  28. package/app/Sources/MainWindow.swift +70 -0
  29. package/app/Sources/OrphanRow.swift +129 -0
  30. package/app/Sources/PaletteCommand.swift +409 -0
  31. package/app/Sources/PermissionChecker.swift +115 -0
  32. package/app/Sources/Preferences.swift +48 -0
  33. package/app/Sources/ProcessModel.swift +199 -0
  34. package/app/Sources/ProcessQuery.swift +151 -0
  35. package/app/Sources/Project.swift +28 -0
  36. package/app/Sources/ProjectRow.swift +368 -0
  37. package/app/Sources/ProjectScanner.swift +121 -0
  38. package/app/Sources/ScreenMapState.swift +2397 -0
  39. package/app/Sources/ScreenMapView.swift +2817 -0
  40. package/app/Sources/ScreenMapWindowController.swift +89 -0
  41. package/app/Sources/SessionManager.swift +72 -0
  42. package/app/Sources/SettingsView.swift +641 -0
  43. package/app/Sources/SettingsWindow.swift +20 -0
  44. package/app/Sources/TabGroupRow.swift +178 -0
  45. package/app/Sources/Terminal.swift +259 -0
  46. package/app/Sources/TerminalQuery.swift +156 -0
  47. package/app/Sources/TerminalSynthesizer.swift +200 -0
  48. package/app/Sources/Theme.swift +124 -0
  49. package/app/Sources/TilePickerView.swift +209 -0
  50. package/app/Sources/TmuxModel.swift +53 -0
  51. package/app/Sources/TmuxQuery.swift +81 -0
  52. package/app/Sources/WindowTiler.swift +1752 -0
  53. package/app/Sources/WorkspaceManager.swift +434 -0
  54. package/bin/daemon-client.js +187 -0
  55. package/bin/lattices-app.js +205 -0
  56. package/bin/lattices.js +1295 -0
  57. package/docs/api.md +707 -0
  58. package/docs/app.md +250 -0
  59. package/docs/concepts.md +225 -0
  60. package/docs/config.md +234 -0
  61. package/docs/layers.md +317 -0
  62. package/docs/overview.md +74 -0
  63. package/docs/quickstart.md +82 -0
  64. package/package.json +38 -0
@@ -0,0 +1,253 @@
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 let log = DiagnosticLog.shared
71
+
72
+ var isVisible: Bool { window?.isVisible ?? false }
73
+
74
+ func toggle() {
75
+ if let w = window, w.isVisible {
76
+ w.orderOut(nil)
77
+ } else {
78
+ show()
79
+ }
80
+ }
81
+
82
+ func show() {
83
+ if let w = window {
84
+ w.orderFrontRegardless()
85
+ return
86
+ }
87
+
88
+ let view = DiagnosticOverlayView()
89
+
90
+ let hosting = NSHostingController(rootView: view)
91
+ let screen = NSScreen.main
92
+ let screenFrame = screen?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1920, height: 1080)
93
+ let panelWidth: CGFloat = 480
94
+ let panelHeight: CGFloat = max(600, floor(screenFrame.height * 0.55))
95
+ hosting.preferredContentSize = NSSize(width: panelWidth, height: panelHeight)
96
+
97
+ let w = NSPanel(
98
+ contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight),
99
+ styleMask: [.titled, .closable, .resizable, .utilityWindow, .nonactivatingPanel],
100
+ backing: .buffered,
101
+ defer: false
102
+ )
103
+ w.contentViewController = hosting
104
+ w.title = "Lattices Diagnostics"
105
+ w.titlebarAppearsTransparent = true
106
+ w.isMovableByWindowBackground = true
107
+ w.level = .floating
108
+ w.isOpaque = false
109
+ w.backgroundColor = NSColor(red: 0.1, green: 0.1, blue: 0.12, alpha: 1.0)
110
+ w.hasShadow = true
111
+ w.alphaValue = 1.0
112
+ w.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
113
+
114
+ // Position: right edge, vertically centered
115
+ let x = screenFrame.maxX - panelWidth - 12
116
+ let y = screenFrame.minY + floor((screenFrame.height - panelHeight) / 2)
117
+ w.setFrameOrigin(NSPoint(x: x, y: y))
118
+
119
+ w.orderFrontRegardless()
120
+ window = w
121
+
122
+ // Startup log
123
+ let diag = DiagnosticLog.shared
124
+ diag.info("Diagnostics opened")
125
+ diag.info("Terminal: \(Preferences.shared.terminal.rawValue) (\(Preferences.shared.terminal.bundleId))")
126
+ diag.info("Installed: \(Terminal.installed.map(\.rawValue).joined(separator: ", "))")
127
+
128
+ // Show running sessions
129
+ let task = Process()
130
+ task.executableURL = URL(fileURLWithPath: "/opt/homebrew/bin/tmux")
131
+ task.arguments = ["list-sessions", "-F", "#{session_name}"]
132
+ let pipe = Pipe()
133
+ task.standardOutput = pipe
134
+ task.standardError = FileHandle.nullDevice
135
+ try? task.run()
136
+ task.waitUntilExit()
137
+ let sessions = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "none"
138
+ diag.info("tmux sessions: \(sessions)")
139
+ }
140
+ }
141
+
142
+ // MARK: - SwiftUI Overlay
143
+
144
+ struct DiagnosticOverlayView: View {
145
+ @StateObject private var log = DiagnosticLog.shared
146
+ @State private var autoScroll = true
147
+ @State private var refreshTick = 0
148
+
149
+ private static let timeFmt: DateFormatter = {
150
+ let f = DateFormatter()
151
+ f.dateFormat = "HH:mm:ss.SSS"
152
+ return f
153
+ }()
154
+
155
+ // Fallback timer to catch any missed updates
156
+ private let refreshTimer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
157
+
158
+ var body: some View {
159
+ VStack(spacing: 0) {
160
+ // Header
161
+ HStack {
162
+ Text("DIAGNOSTICS")
163
+ .font(.system(size: 10, weight: .bold, design: .monospaced))
164
+ .foregroundColor(.green.opacity(0.8))
165
+ Spacer()
166
+ let _ = refreshTick // force re-render on timer
167
+ Text("\(log.entries.count) events")
168
+ .font(.system(size: 9, design: .monospaced))
169
+ .foregroundColor(.white.opacity(0.4))
170
+ Button("Copy") {
171
+ let text = log.entries.map { entry in
172
+ let t = Self.timeFmt.string(from: entry.time)
173
+ return "\(t) \(entry.icon) \(entry.message)"
174
+ }.joined(separator: "\n")
175
+ NSPasteboard.general.clearContents()
176
+ NSPasteboard.general.setString(text, forType: .string)
177
+ }
178
+ .font(.system(size: 9, design: .monospaced))
179
+ .foregroundColor(.white.opacity(0.5))
180
+ .buttonStyle(.plain)
181
+ Button("Clear") { log.clear() }
182
+ .font(.system(size: 9, design: .monospaced))
183
+ .foregroundColor(.white.opacity(0.5))
184
+ .buttonStyle(.plain)
185
+ }
186
+ .padding(.horizontal, 10)
187
+ .padding(.vertical, 6)
188
+ .background(Color.black.opacity(0.3))
189
+ .onReceive(refreshTimer) { _ in refreshTick += 1 }
190
+
191
+ // Log entries
192
+ ScrollViewReader { proxy in
193
+ ScrollView {
194
+ LazyVStack(alignment: .leading, spacing: 1) {
195
+ ForEach(log.entries) { entry in
196
+ logRow(entry)
197
+ .id(entry.id)
198
+ }
199
+ }
200
+ .padding(.horizontal, 8)
201
+ .padding(.vertical, 4)
202
+ }
203
+ .onChange(of: log.entries.count) { _ in
204
+ if autoScroll, let last = log.entries.last {
205
+ withAnimation(.easeOut(duration: 0.1)) {
206
+ proxy.scrollTo(last.id, anchor: .bottom)
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ .frame(minWidth: 420, idealWidth: 480, minHeight: 400, idealHeight: 600)
213
+ .background(Color.black.opacity(0.75))
214
+ }
215
+
216
+ private func logRow(_ entry: DiagnosticLog.Entry) -> some View {
217
+ HStack(alignment: .top, spacing: 6) {
218
+ Text(Self.timeFmt.string(from: entry.time))
219
+ .font(.system(size: 9, design: .monospaced))
220
+ .foregroundColor(.white.opacity(0.3))
221
+
222
+ Text(entry.icon)
223
+ .font(.system(size: 9, design: .monospaced))
224
+ .foregroundColor(iconColor(entry.level))
225
+ .frame(width: 10)
226
+
227
+ Text(entry.message)
228
+ .font(.system(size: 10, design: .monospaced))
229
+ .foregroundColor(textColor(entry.level))
230
+ .lineLimit(3)
231
+ .fixedSize(horizontal: false, vertical: true)
232
+ }
233
+ .padding(.vertical, 1)
234
+ }
235
+
236
+ private func iconColor(_ level: DiagnosticLog.Entry.Level) -> Color {
237
+ switch level {
238
+ case .info: return .white.opacity(0.5)
239
+ case .success: return .green
240
+ case .warning: return .yellow
241
+ case .error: return .red
242
+ }
243
+ }
244
+
245
+ private func textColor(_ level: DiagnosticLog.Entry.Level) -> Color {
246
+ switch level {
247
+ case .info: return .white.opacity(0.7)
248
+ case .success: return .green.opacity(0.9)
249
+ case .warning: return .yellow.opacity(0.9)
250
+ case .error: return .red.opacity(0.9)
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,29 @@
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
+ }
9
+
10
+ final class EventBus {
11
+ static let shared = EventBus()
12
+ private var handlers: [(ModelEvent) -> Void] = []
13
+ private let lock = NSLock()
14
+
15
+ func subscribe(_ handler: @escaping (ModelEvent) -> Void) {
16
+ lock.lock()
17
+ handlers.append(handler)
18
+ lock.unlock()
19
+ }
20
+
21
+ func post(_ event: ModelEvent) {
22
+ lock.lock()
23
+ let copy = handlers
24
+ lock.unlock()
25
+ for handler in copy {
26
+ handler(event)
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,249 @@
1
+ import Carbon
2
+ import AppKit
3
+
4
+ /// Global callback registry keyed by hotkey ID
5
+ private var hotkeyCallbacks: [UInt32: () -> Void] = [:]
6
+
7
+ /// Whether the global Carbon event handler has been installed
8
+ private var eventHandlerInstalled = false
9
+
10
+ class HotkeyManager {
11
+ static let shared = HotkeyManager()
12
+ private var hotKeyRefs: [UInt32: EventHotKeyRef] = [:]
13
+
14
+ private func ensureEventHandler() {
15
+ guard !eventHandlerInstalled else { return }
16
+ eventHandlerInstalled = true
17
+
18
+ var eventType = EventTypeSpec(
19
+ eventClass: OSType(kEventClassKeyboard),
20
+ eventKind: UInt32(kEventHotKeyPressed)
21
+ )
22
+
23
+ InstallEventHandler(
24
+ GetApplicationEventTarget(),
25
+ { (_: EventHandlerCallRef?, event: EventRef?, _: UnsafeMutableRawPointer?) -> OSStatus in
26
+ guard let event else { return OSStatus(eventNotHandledErr) }
27
+ var hotkeyID = EventHotKeyID()
28
+ GetEventParameter(
29
+ event,
30
+ EventParamName(kEventParamDirectObject),
31
+ EventParamType(typeEventHotKeyID),
32
+ nil,
33
+ MemoryLayout<EventHotKeyID>.size,
34
+ nil,
35
+ &hotkeyID
36
+ )
37
+ hotkeyCallbacks[hotkeyID.id]?()
38
+ return noErr
39
+ },
40
+ 1,
41
+ &eventType,
42
+ nil,
43
+ nil
44
+ )
45
+ }
46
+
47
+ /// Register Cmd+Shift+M as the global hotkey (palette toggle)
48
+ func register(callback: @escaping () -> Void) {
49
+ ensureEventHandler()
50
+
51
+ let id: UInt32 = 1
52
+ hotkeyCallbacks[id] = callback
53
+
54
+ let hotKeyID = EventHotKeyID(
55
+ signature: OSType(0x444D5558), // "DMUX"
56
+ id: id
57
+ )
58
+
59
+ var ref: EventHotKeyRef?
60
+ RegisterEventHotKey(
61
+ 46, // 'M'
62
+ UInt32(cmdKey | shiftKey),
63
+ hotKeyID,
64
+ GetApplicationEventTarget(),
65
+ 0,
66
+ &ref
67
+ )
68
+ if let ref { hotKeyRefs[id] = ref }
69
+ }
70
+
71
+ /// Register Hyper+1 (Cmd+Ctrl+Option+Shift+1) for command mode
72
+ func registerCommandMode(callback: @escaping () -> Void) {
73
+ ensureEventHandler()
74
+ let id: UInt32 = 200
75
+ hotkeyCallbacks[id] = callback
76
+ let hotKeyID = EventHotKeyID(
77
+ signature: OSType(0x444D5558), // "DMUX"
78
+ id: id
79
+ )
80
+ var ref: EventHotKeyRef?
81
+ RegisterEventHotKey(
82
+ 18, // '1' key
83
+ UInt32(cmdKey | controlKey | optionKey | shiftKey), // Hyper
84
+ hotKeyID,
85
+ GetApplicationEventTarget(),
86
+ 0,
87
+ &ref
88
+ )
89
+ if let ref { hotKeyRefs[id] = ref }
90
+ }
91
+
92
+ /// Register Hyper+2 (Cmd+Ctrl+Option+Shift+2) for bezel mode
93
+ func registerBezelHotkey(callback: @escaping () -> Void) {
94
+ ensureEventHandler()
95
+ let id: UInt32 = 201
96
+ hotkeyCallbacks[id] = callback
97
+ let hotKeyID = EventHotKeyID(
98
+ signature: OSType(0x444D5558), // "DMUX"
99
+ id: id
100
+ )
101
+ var ref: EventHotKeyRef?
102
+ RegisterEventHotKey(
103
+ 19, // '2' key
104
+ UInt32(cmdKey | controlKey | optionKey | shiftKey), // Hyper
105
+ hotKeyID,
106
+ GetApplicationEventTarget(),
107
+ 0,
108
+ &ref
109
+ )
110
+ if let ref { hotKeyRefs[id] = ref }
111
+ }
112
+
113
+ /// Register Cmd+Option+1/2/3... hotkeys for layer switching
114
+ func registerLayerHotkeys(count: Int, callback: @escaping (Int) -> Void) {
115
+ ensureEventHandler()
116
+
117
+ // Key codes for number keys 1-9
118
+ let keyCodes: [UInt32] = [18, 19, 20, 21, 23, 22, 26, 28, 25]
119
+ let limit = min(count, keyCodes.count)
120
+
121
+ for i in 0..<limit {
122
+ let id: UInt32 = 101 + UInt32(i)
123
+
124
+ // Unregister existing if re-registering
125
+ if let existing = hotKeyRefs[id] {
126
+ UnregisterEventHotKey(existing)
127
+ hotKeyRefs.removeValue(forKey: id)
128
+ }
129
+
130
+ let layerIndex = i
131
+ hotkeyCallbacks[id] = { callback(layerIndex) }
132
+
133
+ let hotKeyID = EventHotKeyID(
134
+ signature: OSType(0x444D5558), // "DMUX"
135
+ id: id
136
+ )
137
+
138
+ var ref: EventHotKeyRef?
139
+ RegisterEventHotKey(
140
+ keyCodes[i],
141
+ UInt32(cmdKey | optionKey),
142
+ hotKeyID,
143
+ GetApplicationEventTarget(),
144
+ 0,
145
+ &ref
146
+ )
147
+ if let ref { hotKeyRefs[id] = ref }
148
+ }
149
+ }
150
+
151
+ /// Register a single global hotkey with a given ID, key code, and Carbon modifier mask
152
+ func registerSingle(id: UInt32, keyCode: UInt32, modifiers: UInt32, callback: @escaping () -> Void) {
153
+ ensureEventHandler()
154
+
155
+ if let existing = hotKeyRefs[id] {
156
+ UnregisterEventHotKey(existing)
157
+ hotKeyRefs.removeValue(forKey: id)
158
+ }
159
+
160
+ hotkeyCallbacks[id] = callback
161
+
162
+ let hotKeyID = EventHotKeyID(
163
+ signature: OSType(0x444D5558), // "DMUX"
164
+ id: id
165
+ )
166
+
167
+ var ref: EventHotKeyRef?
168
+ RegisterEventHotKey(
169
+ keyCode,
170
+ modifiers,
171
+ hotKeyID,
172
+ GetApplicationEventTarget(),
173
+ 0,
174
+ &ref
175
+ )
176
+ if let ref { hotKeyRefs[id] = ref }
177
+ }
178
+
179
+ /// Unregister all global hotkeys and clear callbacks
180
+ func unregisterAll() {
181
+ for (id, ref) in hotKeyRefs {
182
+ UnregisterEventHotKey(ref)
183
+ hotkeyCallbacks.removeValue(forKey: id)
184
+ }
185
+ hotKeyRefs.removeAll()
186
+ }
187
+
188
+ /// Register Ctrl+Option window tiling hotkeys (Magnet-style)
189
+ func registerTileHotkeys() {
190
+ let mods = UInt32(controlKey | optionKey)
191
+
192
+ // Ctrl+Option+← → left
193
+ registerSingle(id: 300, keyCode: 123, modifiers: mods) {
194
+ WindowTiler.tileFrontmostViaAX(to: .left)
195
+ }
196
+ // Ctrl+Option+→ → right
197
+ registerSingle(id: 301, keyCode: 124, modifiers: mods) {
198
+ WindowTiler.tileFrontmostViaAX(to: .right)
199
+ }
200
+ // Ctrl+Option+Return → maximize
201
+ registerSingle(id: 302, keyCode: 36, modifiers: mods) {
202
+ WindowTiler.tileFrontmostViaAX(to: .maximize)
203
+ }
204
+ // Ctrl+Option+C → center
205
+ registerSingle(id: 303, keyCode: 8, modifiers: mods) {
206
+ WindowTiler.tileFrontmostViaAX(to: .center)
207
+ }
208
+ // Ctrl+Option+U → top-left
209
+ registerSingle(id: 304, keyCode: 32, modifiers: mods) {
210
+ WindowTiler.tileFrontmostViaAX(to: .topLeft)
211
+ }
212
+ // Ctrl+Option+I → top-right
213
+ registerSingle(id: 305, keyCode: 34, modifiers: mods) {
214
+ WindowTiler.tileFrontmostViaAX(to: .topRight)
215
+ }
216
+ // Ctrl+Option+J → bottom-left
217
+ registerSingle(id: 306, keyCode: 38, modifiers: mods) {
218
+ WindowTiler.tileFrontmostViaAX(to: .bottomLeft)
219
+ }
220
+ // Ctrl+Option+K → bottom-right
221
+ registerSingle(id: 307, keyCode: 40, modifiers: mods) {
222
+ WindowTiler.tileFrontmostViaAX(to: .bottomRight)
223
+ }
224
+ // Ctrl+Option+↑ → top
225
+ registerSingle(id: 308, keyCode: 126, modifiers: mods) {
226
+ WindowTiler.tileFrontmostViaAX(to: .top)
227
+ }
228
+ // Ctrl+Option+↓ → bottom
229
+ registerSingle(id: 309, keyCode: 125, modifiers: mods) {
230
+ WindowTiler.tileFrontmostViaAX(to: .bottom)
231
+ }
232
+ // Ctrl+Option+D → distribute visible windows
233
+ registerSingle(id: 310, keyCode: 2, modifiers: mods) {
234
+ WindowTiler.distributeVisible()
235
+ }
236
+ // Ctrl+Option+1 → left third
237
+ registerSingle(id: 311, keyCode: 18, modifiers: mods) {
238
+ WindowTiler.tileFrontmostViaAX(to: .leftThird)
239
+ }
240
+ // Ctrl+Option+2 → center third
241
+ registerSingle(id: 312, keyCode: 19, modifiers: mods) {
242
+ WindowTiler.tileFrontmostViaAX(to: .centerThird)
243
+ }
244
+ // Ctrl+Option+3 → right third
245
+ registerSingle(id: 313, keyCode: 20, modifiers: mods) {
246
+ WindowTiler.tileFrontmostViaAX(to: .rightThird)
247
+ }
248
+ }
249
+ }