@lattices/cli 0.3.0 → 0.4.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.
- package/README.md +85 -9
- package/app/Package.swift +8 -1
- package/app/Sources/AdvisorLearningStore.swift +90 -0
- package/app/Sources/AgentSession.swift +377 -0
- package/app/Sources/AppDelegate.swift +44 -12
- package/app/Sources/AppShellView.swift +81 -8
- package/app/Sources/AudioProvider.swift +386 -0
- package/app/Sources/CheatSheetHUD.swift +261 -19
- package/app/Sources/DaemonProtocol.swift +13 -0
- package/app/Sources/DaemonServer.swift +8 -0
- package/app/Sources/DesktopModel.swift +164 -5
- package/app/Sources/DesktopModelTypes.swift +2 -0
- package/app/Sources/DiagnosticLog.swift +104 -2
- package/app/Sources/EventBus.swift +1 -0
- package/app/Sources/HUDBottomBar.swift +279 -0
- package/app/Sources/HUDController.swift +1158 -0
- package/app/Sources/HUDLeftBar.swift +849 -0
- package/app/Sources/HUDMinimap.swift +179 -0
- package/app/Sources/HUDRightBar.swift +774 -0
- package/app/Sources/HUDState.swift +367 -0
- package/app/Sources/HUDTopBar.swift +243 -0
- package/app/Sources/HandsOffSession.swift +733 -0
- package/app/Sources/HomeDashboardView.swift +125 -0
- package/app/Sources/HotkeyManager.swift +2 -0
- package/app/Sources/HotkeyStore.swift +45 -9
- package/app/Sources/IntentEngine.swift +925 -0
- package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
- package/app/Sources/Intents/DistributeIntent.swift +56 -0
- package/app/Sources/Intents/FocusIntent.swift +69 -0
- package/app/Sources/Intents/HelpIntent.swift +41 -0
- package/app/Sources/Intents/KillIntent.swift +47 -0
- package/app/Sources/Intents/LatticeIntent.swift +78 -0
- package/app/Sources/Intents/LaunchIntent.swift +67 -0
- package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
- package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
- package/app/Sources/Intents/ScanIntent.swift +52 -0
- package/app/Sources/Intents/SearchIntent.swift +190 -0
- package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
- package/app/Sources/Intents/TileIntent.swift +61 -0
- package/app/Sources/LatticesApi.swift +1235 -30
- package/app/Sources/LauncherHUD.swift +348 -0
- package/app/Sources/MainView.swift +147 -44
- package/app/Sources/OcrModel.swift +34 -1
- package/app/Sources/OmniSearchState.swift +99 -102
- package/app/Sources/OnboardingView.swift +457 -0
- package/app/Sources/PermissionChecker.swift +2 -12
- package/app/Sources/PiChatDock.swift +454 -0
- package/app/Sources/PiChatSession.swift +815 -0
- package/app/Sources/PiWorkspaceView.swift +364 -0
- package/app/Sources/PlacementSpec.swift +195 -0
- package/app/Sources/Preferences.swift +59 -0
- package/app/Sources/ProjectScanner.swift +1 -1
- package/app/Sources/ScreenMapState.swift +701 -55
- package/app/Sources/ScreenMapView.swift +843 -103
- package/app/Sources/ScreenMapWindowController.swift +22 -0
- package/app/Sources/SessionLayerStore.swift +285 -0
- package/app/Sources/SessionManager.swift +4 -1
- package/app/Sources/SettingsView.swift +186 -3
- package/app/Sources/Theme.swift +9 -8
- package/app/Sources/TmuxModel.swift +7 -0
- package/app/Sources/TmuxQuery.swift +27 -3
- package/app/Sources/VoiceChatView.swift +192 -0
- package/app/Sources/VoiceCommandWindow.swift +1594 -0
- package/app/Sources/VoiceIntentResolver.swift +671 -0
- package/app/Sources/VoxClient.swift +454 -0
- package/app/Sources/WindowTiler.swift +348 -87
- package/app/Sources/WorkspaceManager.swift +127 -18
- package/bin/client.ts +16 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +280 -0
- package/bin/handsoff-worker.ts +731 -0
- package/bin/{lattices-app.js → lattices-app.ts} +67 -32
- package/bin/lattices-dev +160 -0
- package/bin/{lattices.js → lattices.ts} +600 -137
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agents.md +142 -0
- package/docs/api.md +153 -34
- package/docs/app.md +29 -1
- package/docs/config.md +5 -1
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/layers.md +20 -20
- package/docs/ocr.md +14 -5
- package/docs/overview.md +5 -1
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +374 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/tiling-reference.md +167 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice.md +219 -0
- package/package.json +21 -10
- package/bin/client.js +0 -4
|
@@ -25,6 +25,13 @@ private let _SLSMoveWindow: SLSMoveWindowFunc? = {
|
|
|
25
25
|
return unsafeBitCast(sym, to: SLSMoveWindowFunc.self)
|
|
26
26
|
}()
|
|
27
27
|
|
|
28
|
+
private typealias SLSOrderWindowFunc = @convention(c) (Int32, UInt32, Int32, UInt32) -> CGError
|
|
29
|
+
|
|
30
|
+
private let _SLSOrderWindow: SLSOrderWindowFunc? = {
|
|
31
|
+
guard let sl = skyLight, let sym = dlsym(sl, "SLSOrderWindow") ?? dlsym(sl, "CGSOrderWindow") else { return nil }
|
|
32
|
+
return unsafeBitCast(sym, to: SLSOrderWindowFunc.self)
|
|
33
|
+
}()
|
|
34
|
+
|
|
28
35
|
private let _SLSDisableUpdate: SLSDisableUpdateFunc? = {
|
|
29
36
|
guard let sl = skyLight, let sym = dlsym(sl, "SLSDisableUpdate") else { return nil }
|
|
30
37
|
return unsafeBitCast(sym, to: SLSDisableUpdateFunc.self)
|
|
@@ -122,25 +129,75 @@ private class HighlightBorderView: NSView {
|
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
// MARK: - Grid Tiling
|
|
133
|
+
|
|
134
|
+
/// Compute fractional (x, y, w, h) for a cell in a cols×rows grid.
|
|
135
|
+
func tileGrid(cols: Int, rows: Int, col: Int, row: Int) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
|
|
136
|
+
let w = 1.0 / CGFloat(cols)
|
|
137
|
+
let h = 1.0 / CGFloat(rows)
|
|
138
|
+
return (CGFloat(col) * w, CGFloat(row) * h, w, h)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Parse a grid string like "grid:3x2:0,0" → fractional (x, y, w, h).
|
|
142
|
+
func parseGridString(_ str: String) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
|
|
143
|
+
GridPlacement.parse(str)?.fractions
|
|
144
|
+
}
|
|
145
|
+
|
|
125
146
|
enum TilePosition: String, CaseIterable, Identifiable {
|
|
147
|
+
// 1x1
|
|
148
|
+
case maximize = "maximize"
|
|
149
|
+
case center = "center"
|
|
150
|
+
// 2x1 (halves, full height)
|
|
126
151
|
case left = "left"
|
|
127
152
|
case right = "right"
|
|
153
|
+
// 1x2 (halves, full width)
|
|
128
154
|
case top = "top"
|
|
129
155
|
case bottom = "bottom"
|
|
156
|
+
// 2x2 (quarters)
|
|
130
157
|
case topLeft = "top-left"
|
|
131
158
|
case topRight = "top-right"
|
|
132
159
|
case bottomLeft = "bottom-left"
|
|
133
160
|
case bottomRight = "bottom-right"
|
|
134
|
-
|
|
135
|
-
case center = "center"
|
|
161
|
+
// 3x1 (thirds, full height)
|
|
136
162
|
case leftThird = "left-third"
|
|
137
163
|
case centerThird = "center-third"
|
|
138
164
|
case rightThird = "right-third"
|
|
165
|
+
// 3x2 (sixths)
|
|
166
|
+
case topLeftThird = "top-left-third"
|
|
167
|
+
case topCenterThird = "top-center-third"
|
|
168
|
+
case topRightThird = "top-right-third"
|
|
169
|
+
case bottomLeftThird = "bottom-left-third"
|
|
170
|
+
case bottomCenterThird = "bottom-center-third"
|
|
171
|
+
case bottomRightThird = "bottom-right-third"
|
|
172
|
+
// 4x1 (fourths, full height)
|
|
173
|
+
case firstFourth = "first-fourth"
|
|
174
|
+
case secondFourth = "second-fourth"
|
|
175
|
+
case thirdFourth = "third-fourth"
|
|
176
|
+
case lastFourth = "last-fourth"
|
|
177
|
+
// 4x2 (eighths)
|
|
178
|
+
case topFirstFourth = "top-first-fourth"
|
|
179
|
+
case topSecondFourth = "top-second-fourth"
|
|
180
|
+
case topThirdFourth = "top-third-fourth"
|
|
181
|
+
case topLastFourth = "top-last-fourth"
|
|
182
|
+
case bottomFirstFourth = "bottom-first-fourth"
|
|
183
|
+
case bottomSecondFourth = "bottom-second-fourth"
|
|
184
|
+
case bottomThirdFourth = "bottom-third-fourth"
|
|
185
|
+
case bottomLastFourth = "bottom-last-fourth"
|
|
186
|
+
// Horizontal thirds / quarters
|
|
187
|
+
case topThird = "top-third"
|
|
188
|
+
case middleThird = "middle-third"
|
|
189
|
+
case bottomThird = "bottom-third"
|
|
190
|
+
case leftQuarter = "left-quarter"
|
|
191
|
+
case rightQuarter = "right-quarter"
|
|
192
|
+
case topQuarter = "top-quarter"
|
|
193
|
+
case bottomQuarter = "bottom-quarter"
|
|
139
194
|
|
|
140
195
|
var id: String { rawValue }
|
|
141
196
|
|
|
142
197
|
var label: String {
|
|
143
198
|
switch self {
|
|
199
|
+
case .maximize: return "Max"
|
|
200
|
+
case .center: return "Center"
|
|
144
201
|
case .left: return "Left"
|
|
145
202
|
case .right: return "Right"
|
|
146
203
|
case .top: return "Top"
|
|
@@ -149,11 +206,34 @@ enum TilePosition: String, CaseIterable, Identifiable {
|
|
|
149
206
|
case .topRight: return "Top Right"
|
|
150
207
|
case .bottomLeft: return "Bottom Left"
|
|
151
208
|
case .bottomRight: return "Bottom Right"
|
|
152
|
-
case .
|
|
153
|
-
case .
|
|
154
|
-
case .
|
|
155
|
-
case .
|
|
156
|
-
case .
|
|
209
|
+
case .leftThird: return "Left ⅓"
|
|
210
|
+
case .centerThird: return "Center ⅓"
|
|
211
|
+
case .rightThird: return "Right ⅓"
|
|
212
|
+
case .topLeftThird: return "Top Left ⅓"
|
|
213
|
+
case .topCenterThird: return "Top Center ⅓"
|
|
214
|
+
case .topRightThird: return "Top Right ⅓"
|
|
215
|
+
case .bottomLeftThird: return "Bottom Left ⅓"
|
|
216
|
+
case .bottomCenterThird: return "Bottom Center ⅓"
|
|
217
|
+
case .bottomRightThird: return "Bottom Right ⅓"
|
|
218
|
+
case .firstFourth: return "1st ¼"
|
|
219
|
+
case .secondFourth: return "2nd ¼"
|
|
220
|
+
case .thirdFourth: return "3rd ¼"
|
|
221
|
+
case .lastFourth: return "4th ¼"
|
|
222
|
+
case .topFirstFourth: return "Top 1st ¼"
|
|
223
|
+
case .topSecondFourth: return "Top 2nd ¼"
|
|
224
|
+
case .topThirdFourth: return "Top 3rd ¼"
|
|
225
|
+
case .topLastFourth: return "Top 4th ¼"
|
|
226
|
+
case .bottomFirstFourth: return "Bottom 1st ¼"
|
|
227
|
+
case .bottomSecondFourth: return "Bottom 2nd ¼"
|
|
228
|
+
case .bottomThirdFourth: return "Bottom 3rd ¼"
|
|
229
|
+
case .bottomLastFourth: return "Bottom 4th ¼"
|
|
230
|
+
case .topThird: return "Top ⅓"
|
|
231
|
+
case .middleThird: return "Middle ⅓"
|
|
232
|
+
case .bottomThird: return "Bottom ⅓"
|
|
233
|
+
case .leftQuarter: return "Left ¼"
|
|
234
|
+
case .rightQuarter: return "Right ¼"
|
|
235
|
+
case .topQuarter: return "Top ¼"
|
|
236
|
+
case .bottomQuarter: return "Bottom ¼"
|
|
157
237
|
}
|
|
158
238
|
}
|
|
159
239
|
|
|
@@ -172,25 +252,66 @@ enum TilePosition: String, CaseIterable, Identifiable {
|
|
|
172
252
|
case .leftThird: return "rectangle.leadingthird.inset.filled"
|
|
173
253
|
case .centerThird: return "rectangle.center.inset.filled"
|
|
174
254
|
case .rightThird: return "rectangle.trailingthird.inset.filled"
|
|
255
|
+
case .topThird: return "rectangle.topthird.inset.filled"
|
|
256
|
+
case .middleThird: return "rectangle.center.inset.filled"
|
|
257
|
+
case .bottomThird: return "rectangle.bottomthird.inset.filled"
|
|
258
|
+
case .leftQuarter: return "rectangle.leadinghalf.inset.filled"
|
|
259
|
+
case .rightQuarter:return "rectangle.trailinghalf.inset.filled"
|
|
260
|
+
case .topQuarter: return "rectangle.tophalf.inset.filled"
|
|
261
|
+
case .bottomQuarter:return "rectangle.bottomhalf.inset.filled"
|
|
262
|
+
default: return "rectangle.split.3x3.fill"
|
|
175
263
|
}
|
|
176
264
|
}
|
|
177
265
|
|
|
178
266
|
/// Returns (x, y, w, h) as fractions of screen
|
|
179
267
|
var rect: (CGFloat, CGFloat, CGFloat, CGFloat) {
|
|
180
268
|
switch self {
|
|
181
|
-
|
|
182
|
-
case .right: return (0.5, 0, 0.5, 1.0)
|
|
183
|
-
case .top: return (0, 0, 1.0, 0.5)
|
|
184
|
-
case .bottom: return (0, 0.5, 1.0, 0.5)
|
|
185
|
-
case .topLeft: return (0, 0, 0.5, 0.5)
|
|
186
|
-
case .topRight: return (0.5, 0, 0.5, 0.5)
|
|
187
|
-
case .bottomLeft: return (0, 0.5, 0.5, 0.5)
|
|
188
|
-
case .bottomRight: return (0.5, 0.5, 0.5, 0.5)
|
|
269
|
+
// 1x1
|
|
189
270
|
case .maximize: return (0, 0, 1.0, 1.0)
|
|
190
271
|
case .center: return (0.15, 0.1, 0.7, 0.8)
|
|
191
|
-
|
|
192
|
-
case .
|
|
193
|
-
case .
|
|
272
|
+
// 2x1
|
|
273
|
+
case .left: return tileGrid(cols: 2, rows: 1, col: 0, row: 0)
|
|
274
|
+
case .right: return tileGrid(cols: 2, rows: 1, col: 1, row: 0)
|
|
275
|
+
// 1x2
|
|
276
|
+
case .top: return tileGrid(cols: 1, rows: 2, col: 0, row: 0)
|
|
277
|
+
case .bottom: return tileGrid(cols: 1, rows: 2, col: 0, row: 1)
|
|
278
|
+
// 2x2
|
|
279
|
+
case .topLeft: return tileGrid(cols: 2, rows: 2, col: 0, row: 0)
|
|
280
|
+
case .topRight: return tileGrid(cols: 2, rows: 2, col: 1, row: 0)
|
|
281
|
+
case .bottomLeft: return tileGrid(cols: 2, rows: 2, col: 0, row: 1)
|
|
282
|
+
case .bottomRight: return tileGrid(cols: 2, rows: 2, col: 1, row: 1)
|
|
283
|
+
// 3x1
|
|
284
|
+
case .leftThird: return tileGrid(cols: 3, rows: 1, col: 0, row: 0)
|
|
285
|
+
case .centerThird: return tileGrid(cols: 3, rows: 1, col: 1, row: 0)
|
|
286
|
+
case .rightThird: return tileGrid(cols: 3, rows: 1, col: 2, row: 0)
|
|
287
|
+
// 3x2
|
|
288
|
+
case .topLeftThird: return tileGrid(cols: 3, rows: 2, col: 0, row: 0)
|
|
289
|
+
case .topCenterThird: return tileGrid(cols: 3, rows: 2, col: 1, row: 0)
|
|
290
|
+
case .topRightThird: return tileGrid(cols: 3, rows: 2, col: 2, row: 0)
|
|
291
|
+
case .bottomLeftThird: return tileGrid(cols: 3, rows: 2, col: 0, row: 1)
|
|
292
|
+
case .bottomCenterThird: return tileGrid(cols: 3, rows: 2, col: 1, row: 1)
|
|
293
|
+
case .bottomRightThird: return tileGrid(cols: 3, rows: 2, col: 2, row: 1)
|
|
294
|
+
// 4x1
|
|
295
|
+
case .firstFourth: return tileGrid(cols: 4, rows: 1, col: 0, row: 0)
|
|
296
|
+
case .secondFourth: return tileGrid(cols: 4, rows: 1, col: 1, row: 0)
|
|
297
|
+
case .thirdFourth: return tileGrid(cols: 4, rows: 1, col: 2, row: 0)
|
|
298
|
+
case .lastFourth: return tileGrid(cols: 4, rows: 1, col: 3, row: 0)
|
|
299
|
+
// 4x2
|
|
300
|
+
case .topFirstFourth: return tileGrid(cols: 4, rows: 2, col: 0, row: 0)
|
|
301
|
+
case .topSecondFourth: return tileGrid(cols: 4, rows: 2, col: 1, row: 0)
|
|
302
|
+
case .topThirdFourth: return tileGrid(cols: 4, rows: 2, col: 2, row: 0)
|
|
303
|
+
case .topLastFourth: return tileGrid(cols: 4, rows: 2, col: 3, row: 0)
|
|
304
|
+
case .bottomFirstFourth: return tileGrid(cols: 4, rows: 2, col: 0, row: 1)
|
|
305
|
+
case .bottomSecondFourth: return tileGrid(cols: 4, rows: 2, col: 1, row: 1)
|
|
306
|
+
case .bottomThirdFourth: return tileGrid(cols: 4, rows: 2, col: 2, row: 1)
|
|
307
|
+
case .bottomLastFourth: return tileGrid(cols: 4, rows: 2, col: 3, row: 1)
|
|
308
|
+
case .topThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 0)
|
|
309
|
+
case .middleThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 1)
|
|
310
|
+
case .bottomThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 2)
|
|
311
|
+
case .leftQuarter: return tileGrid(cols: 4, rows: 1, col: 0, row: 0)
|
|
312
|
+
case .rightQuarter: return tileGrid(cols: 4, rows: 1, col: 3, row: 0)
|
|
313
|
+
case .topQuarter: return tileGrid(cols: 1, rows: 4, col: 0, row: 0)
|
|
314
|
+
case .bottomQuarter: return tileGrid(cols: 1, rows: 4, col: 0, row: 3)
|
|
194
315
|
}
|
|
195
316
|
}
|
|
196
317
|
}
|
|
@@ -272,6 +393,10 @@ enum WindowTiler {
|
|
|
272
393
|
/// Convert fractional rect to AppleScript bounds {left, top, right, bottom}
|
|
273
394
|
/// AppleScript uses top-left origin; NSScreen uses bottom-left origin
|
|
274
395
|
private static func appleScriptBounds(for position: TilePosition, screen: NSScreen? = nil) -> (Int, Int, Int, Int) {
|
|
396
|
+
appleScriptBounds(for: position.rect, screen: screen)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private static func appleScriptBounds(for fractions: (CGFloat, CGFloat, CGFloat, CGFloat), screen: NSScreen? = nil) -> (Int, Int, Int, Int) {
|
|
275
400
|
let targetScreen = screen ?? NSScreen.main
|
|
276
401
|
guard let targetScreen else { return (0, 0, 960, 540) }
|
|
277
402
|
let full = targetScreen.frame
|
|
@@ -282,7 +407,7 @@ enum WindowTiler {
|
|
|
282
407
|
let visW = Int(visible.width)
|
|
283
408
|
let visH = Int(visible.height)
|
|
284
409
|
|
|
285
|
-
let (fx, fy, fw, fh) =
|
|
410
|
+
let (fx, fy, fw, fh) = fractions
|
|
286
411
|
let x1 = visLeft + Int(CGFloat(visW) * fx)
|
|
287
412
|
let y1 = visTop + Int(CGFloat(visH) * fy)
|
|
288
413
|
let x2 = x1 + Int(CGFloat(visW) * fw)
|
|
@@ -290,13 +415,23 @@ enum WindowTiler {
|
|
|
290
415
|
return (x1, y1, x2, y2)
|
|
291
416
|
}
|
|
292
417
|
|
|
418
|
+
/// Get the main screen's frame (safe to call from main thread only).
|
|
419
|
+
static func mainScreenFrame() -> CGRect {
|
|
420
|
+
return NSScreen.main?.frame ?? NSScreen.screens.first?.frame ?? CGRect(x: 0, y: 0, width: 1920, height: 1080)
|
|
421
|
+
}
|
|
422
|
+
|
|
293
423
|
/// Compute AX-coordinate frame for a tile position on a given screen
|
|
294
424
|
static func tileFrame(for position: TilePosition, on screen: NSScreen) -> CGRect {
|
|
425
|
+
tileFrame(fractions: position.rect, on: screen)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Compute AX-coordinate frame for arbitrary fractional placement on a given screen.
|
|
429
|
+
static func tileFrame(fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on screen: NSScreen) -> CGRect {
|
|
295
430
|
let visible = screen.visibleFrame
|
|
296
431
|
guard let primary = NSScreen.screens.first else { return .zero }
|
|
297
432
|
let primaryH = primary.frame.height
|
|
298
433
|
let axTop = primaryH - visible.maxY
|
|
299
|
-
let (fx, fy, fw, fh) =
|
|
434
|
+
let (fx, fy, fw, fh) = fractions
|
|
300
435
|
return CGRect(
|
|
301
436
|
x: visible.origin.x + visible.width * fx,
|
|
302
437
|
y: axTop + visible.height * fy,
|
|
@@ -305,6 +440,10 @@ enum WindowTiler {
|
|
|
305
440
|
)
|
|
306
441
|
}
|
|
307
442
|
|
|
443
|
+
static func tileFrame(for placement: PlacementSpec, on screen: NSScreen) -> CGRect {
|
|
444
|
+
tileFrame(fractions: placement.fractions, on: screen)
|
|
445
|
+
}
|
|
446
|
+
|
|
308
447
|
/// Compute AX-coordinate frame for a tile position within a raw display CGRect (CG/AX coords)
|
|
309
448
|
static func tileFrame(for position: TilePosition, inDisplay displayRect: CGRect) -> CGRect {
|
|
310
449
|
let (fx, fy, fw, fh) = position.rect
|
|
@@ -330,12 +469,16 @@ enum WindowTiler {
|
|
|
330
469
|
/// Tile a specific terminal window on a given screen.
|
|
331
470
|
/// Fast path: DesktopModel → AX. Fallback: AX search → AppleScript last resort.
|
|
332
471
|
static func tile(session: String, terminal: Terminal, to position: TilePosition, on screen: NSScreen) {
|
|
472
|
+
tile(session: session, terminal: terminal, to: .tile(position), on: screen)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static func tile(session: String, terminal: Terminal, to placement: PlacementSpec, on screen: NSScreen) {
|
|
333
476
|
let diag = DiagnosticLog.shared
|
|
334
|
-
let t = diag.startTimed("tile: \(session) → \(
|
|
477
|
+
let t = diag.startTimed("tile: \(session) → \(placement.wireValue)")
|
|
335
478
|
|
|
336
479
|
// Fast path: use DesktopModel cache → single AX move
|
|
337
480
|
if let entry = DesktopModel.shared.windowForSession(session) {
|
|
338
|
-
let frame = tileFrame(for:
|
|
481
|
+
let frame = tileFrame(for: placement, on: screen)
|
|
339
482
|
batchMoveAndRaiseWindows([(wid: entry.wid, pid: entry.pid, frame: frame)])
|
|
340
483
|
diag.success("tile fast path (DesktopModel): \(session)")
|
|
341
484
|
diag.finish(t)
|
|
@@ -345,7 +488,7 @@ enum WindowTiler {
|
|
|
345
488
|
// AX fallback: search terminal windows by title tag
|
|
346
489
|
let tag = Terminal.windowTag(for: session)
|
|
347
490
|
if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag) {
|
|
348
|
-
let targetFrame = tileFrame(for:
|
|
491
|
+
let targetFrame = tileFrame(for: placement, on: screen)
|
|
349
492
|
var newPos = CGPoint(x: targetFrame.origin.x, y: targetFrame.origin.y)
|
|
350
493
|
var newSize = CGSize(width: targetFrame.width, height: targetFrame.height)
|
|
351
494
|
let win = axWindow
|
|
@@ -370,7 +513,7 @@ enum WindowTiler {
|
|
|
370
513
|
|
|
371
514
|
// AppleScript last resort (slow, single-monitor)
|
|
372
515
|
diag.warn("tile AppleScript last resort: \(session)")
|
|
373
|
-
let bounds = appleScriptBounds(for:
|
|
516
|
+
let bounds = appleScriptBounds(for: placement.fractions, screen: screen)
|
|
374
517
|
switch terminal {
|
|
375
518
|
case .terminal:
|
|
376
519
|
tileAppleScript(app: "Terminal", tag: tag, bounds: bounds)
|
|
@@ -385,8 +528,12 @@ enum WindowTiler {
|
|
|
385
528
|
/// Tile a specific terminal window (found by lattices session tag) to a position.
|
|
386
529
|
/// Uses the same fast path strategy as tile(session:terminal:to:on:) with main screen.
|
|
387
530
|
static func tile(session: String, terminal: Terminal, to position: TilePosition) {
|
|
531
|
+
tile(session: session, terminal: terminal, to: .tile(position))
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
static func tile(session: String, terminal: Terminal, to placement: PlacementSpec) {
|
|
388
535
|
let screen = NSScreen.main ?? NSScreen.screens[0]
|
|
389
|
-
tile(session: session, terminal: terminal, to:
|
|
536
|
+
tile(session: session, terminal: terminal, to: placement, on: screen)
|
|
390
537
|
}
|
|
391
538
|
|
|
392
539
|
/// Tile the frontmost window (works for any terminal)
|
|
@@ -537,7 +684,8 @@ enum WindowTiler {
|
|
|
537
684
|
}
|
|
538
685
|
|
|
539
686
|
/// Attempt CGS-based window move. Returns nil if APIs are unavailable.
|
|
540
|
-
|
|
687
|
+
/// Move a window between spaces via CGS private APIs. Internal — used by present() and moveWindowToSpace().
|
|
688
|
+
internal static func moveViaCGS(wid: UInt32, fromSpaces: [Int], toSpace: Int) -> MoveResult? {
|
|
541
689
|
let diag = DiagnosticLog.shared
|
|
542
690
|
guard let mainConn = CGS.mainConnectionID,
|
|
543
691
|
let addToSpaces = CGS.addWindowsToSpaces,
|
|
@@ -997,53 +1145,59 @@ enum WindowTiler {
|
|
|
997
1145
|
DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
|
|
998
1146
|
}
|
|
999
1147
|
|
|
1000
|
-
/// Tile any window by its CG window ID to a position
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1148
|
+
/// Tile any window by its CG window ID to a position.
|
|
1149
|
+
/// Delegates to `batchMoveAndRaiseWindows` which is the battle-tested path:
|
|
1150
|
+
/// uses `_AXUIElementGetWindow` for direct wid→AX mapping, disables enhanced UI,
|
|
1151
|
+
/// freezes screen rendering, and verifies+retries drifted windows.
|
|
1152
|
+
/// Tile a window using raw fractional coordinates.
|
|
1153
|
+
static func tileWindowById(wid: UInt32, pid: Int32, fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on targetScreen: NSScreen? = nil) {
|
|
1154
|
+
let screen = targetScreen ?? NSScreen.main ?? NSScreen.screens[0]
|
|
1155
|
+
let frame = tileFrame(fractions: fractions, on: screen)
|
|
1156
|
+
DiagnosticLog.shared.info("tileWindowById: wid=\(wid) fractions=\(fractions) frame=(\(Int(frame.origin.x)),\(Int(frame.origin.y))) \(Int(frame.width))x\(Int(frame.height))")
|
|
1157
|
+
if let app = NSRunningApplication(processIdentifier: pid) { app.activate() }
|
|
1158
|
+
let moves = [(wid: wid, pid: pid, frame: frame)]
|
|
1159
|
+
batchMoveAndRaiseWindows(moves)
|
|
1160
|
+
let drifted = verifyMoves(moves)
|
|
1161
|
+
if !drifted.isEmpty {
|
|
1162
|
+
usleep(100_000)
|
|
1163
|
+
batchMoveAndRaiseWindows(drifted.map { (wid: $0.wid, pid: $0.pid, frame: $0.frame) })
|
|
1009
1164
|
}
|
|
1010
|
-
|
|
1011
|
-
$0.frame.contains(NSPoint(x: windowFrame.midX, y: windowFrame.midY))
|
|
1012
|
-
}) ?? NSScreen.main ?? NSScreen.screens[0]
|
|
1165
|
+
}
|
|
1013
1166
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1167
|
+
static func tileWindowById(wid: UInt32, pid: Int32, to position: TilePosition, on targetScreen: NSScreen? = nil) {
|
|
1168
|
+
tileWindowById(wid: wid, pid: pid, to: .tile(position), on: targetScreen)
|
|
1169
|
+
}
|
|
1016
1170
|
|
|
1017
|
-
|
|
1018
|
-
let
|
|
1019
|
-
let
|
|
1020
|
-
let
|
|
1021
|
-
let targetH = visible.height * fh
|
|
1171
|
+
static func tileWindowById(wid: UInt32, pid: Int32, to placement: PlacementSpec, on targetScreen: NSScreen? = nil) {
|
|
1172
|
+
let diag = DiagnosticLog.shared
|
|
1173
|
+
let screen = targetScreen ?? NSScreen.main ?? NSScreen.screens[0]
|
|
1174
|
+
let frame = tileFrame(for: placement, on: screen)
|
|
1022
1175
|
|
|
1023
|
-
|
|
1024
|
-
guard let primaryScreen = NSScreen.screens.first else { return }
|
|
1025
|
-
let primaryHeight = primaryScreen.frame.height
|
|
1026
|
-
let axX = targetX
|
|
1027
|
-
let axY = primaryHeight - targetY - targetH
|
|
1176
|
+
diag.info("tileWindowById: wid=\(wid) pid=\(pid) pos=\(placement.wireValue) screen=\(screen.localizedName) frame=(\(Int(frame.origin.x)),\(Int(frame.origin.y))) \(Int(frame.width))x\(Int(frame.height))")
|
|
1028
1177
|
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
return
|
|
1178
|
+
// Focus the app so windows on other Spaces come to the current one
|
|
1179
|
+
if let app = NSRunningApplication(processIdentifier: pid) {
|
|
1180
|
+
app.activate()
|
|
1033
1181
|
}
|
|
1034
1182
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
var newSize = CGSize(width: targetW, height: targetH)
|
|
1183
|
+
let moves = [(wid: wid, pid: pid, frame: frame)]
|
|
1184
|
+
batchMoveAndRaiseWindows(moves)
|
|
1038
1185
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1186
|
+
// Verify and retry once if needed
|
|
1187
|
+
let drifted = verifyMoves(moves)
|
|
1188
|
+
if !drifted.isEmpty {
|
|
1189
|
+
diag.info("tileWindowById: wid=\(wid) drifted, retrying...")
|
|
1190
|
+
usleep(100_000)
|
|
1191
|
+
batchMoveAndRaiseWindows(drifted)
|
|
1192
|
+
let stillDrifted = verifyMoves(drifted)
|
|
1193
|
+
if stillDrifted.isEmpty {
|
|
1194
|
+
diag.success("tileWindowById: wid=\(wid) retry succeeded")
|
|
1195
|
+
} else {
|
|
1196
|
+
diag.warn("tileWindowById: wid=\(wid) still drifted after retry")
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
diag.success("tileWindowById: tiled wid=\(wid) to \(placement.wireValue)")
|
|
1044
1200
|
}
|
|
1045
|
-
|
|
1046
|
-
diag.success("tileWindowById: tiled wid=\(wid) to \(position.rawValue)")
|
|
1047
1201
|
}
|
|
1048
1202
|
|
|
1049
1203
|
/// Distribute windows in a smart grid layout (delegates to batch operation)
|
|
@@ -1074,7 +1228,7 @@ enum WindowTiler {
|
|
|
1074
1228
|
}
|
|
1075
1229
|
|
|
1076
1230
|
/// Get NSRect (bottom-left origin) for a known CG window ID
|
|
1077
|
-
|
|
1231
|
+
static func cgWindowFrame(wid: UInt32) -> NSRect? {
|
|
1078
1232
|
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
|
|
1079
1233
|
for info in windowList {
|
|
1080
1234
|
if let num = info[kCGWindowNumber as String] as? UInt32, num == wid,
|
|
@@ -1128,6 +1282,7 @@ enum WindowTiler {
|
|
|
1128
1282
|
}
|
|
1129
1283
|
}
|
|
1130
1284
|
}
|
|
1285
|
+
DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
|
|
1131
1286
|
diag.success("raiseWindowsAndReactivate: raised \(windows.count) windows")
|
|
1132
1287
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
1133
1288
|
NSApp.activate(ignoringOtherApps: true)
|
|
@@ -1166,6 +1321,7 @@ enum WindowTiler {
|
|
|
1166
1321
|
} else {
|
|
1167
1322
|
doRaise()
|
|
1168
1323
|
}
|
|
1324
|
+
DesktopModel.shared.markInteraction(wid: wid)
|
|
1169
1325
|
}
|
|
1170
1326
|
|
|
1171
1327
|
// MARK: - Batch Window Operations
|
|
@@ -1288,24 +1444,93 @@ enum WindowTiler {
|
|
|
1288
1444
|
}
|
|
1289
1445
|
|
|
1290
1446
|
/// Raise and focus a single window by its CGWindowID.
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
let axWindows = windowsRef as? [AXUIElement] else { return }
|
|
1447
|
+
@discardableResult
|
|
1448
|
+
static func focusWindow(wid: UInt32, pid: Int32) -> Bool {
|
|
1449
|
+
return present(wid: wid, pid: pid)
|
|
1450
|
+
}
|
|
1296
1451
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1452
|
+
// MARK: - Present
|
|
1453
|
+
|
|
1454
|
+
/// Present a window: move it to the current space, bring it to front, optionally position it.
|
|
1455
|
+
/// This is the single entry point for "show me this window right now."
|
|
1456
|
+
@discardableResult
|
|
1457
|
+
static func present(wid: UInt32, pid: Int32, frame: CGRect? = nil) -> Bool {
|
|
1458
|
+
let diag = DiagnosticLog.shared
|
|
1459
|
+
|
|
1460
|
+
// 1. Move to current space if needed
|
|
1461
|
+
let windowSpaces = getSpacesForWindow(wid)
|
|
1462
|
+
let currentSpace = getCurrentSpace()
|
|
1463
|
+
if currentSpace != 0, !windowSpaces.isEmpty, !windowSpaces.contains(currentSpace) {
|
|
1464
|
+
diag.info("present: wid \(wid) on space \(windowSpaces), moving to current space \(currentSpace)")
|
|
1465
|
+
_ = moveViaCGS(wid: wid, fromSpaces: windowSpaces, toSpace: currentSpace)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// 2. Position if requested
|
|
1469
|
+
if let frame = frame {
|
|
1470
|
+
if let mainConn = _SLSMainConnectionID, let moveWindow = _SLSMoveWindow {
|
|
1471
|
+
let cid = mainConn()
|
|
1472
|
+
var origin = CGPoint(x: frame.origin.x, y: frame.origin.y)
|
|
1473
|
+
moveWindow(cid, wid, &origin)
|
|
1474
|
+
}
|
|
1475
|
+
// Resize via AX
|
|
1476
|
+
let appRef = AXUIElementCreateApplication(pid)
|
|
1477
|
+
var windowsRef: CFTypeRef?
|
|
1478
|
+
if AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef) == .success,
|
|
1479
|
+
let axWindows = windowsRef as? [AXUIElement] {
|
|
1480
|
+
for axWin in axWindows {
|
|
1481
|
+
var windowId: CGWindowID = 0
|
|
1482
|
+
if _AXUIElementGetWindow(axWin, &windowId) == .success, windowId == wid {
|
|
1483
|
+
var size = CGSize(width: frame.width, height: frame.height)
|
|
1484
|
+
let sizeValue = AXValueCreate(.cgSize, &size)!
|
|
1485
|
+
AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, sizeValue)
|
|
1486
|
+
break
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1303
1489
|
}
|
|
1304
1490
|
}
|
|
1305
1491
|
|
|
1492
|
+
// 3. Activate the app first (this may bring the wrong window forward)
|
|
1306
1493
|
if let app = NSRunningApplication(processIdentifier: pid) {
|
|
1307
1494
|
app.activate()
|
|
1308
1495
|
}
|
|
1496
|
+
|
|
1497
|
+
// 4. Then order OUR window to front — after activate so we get the last word
|
|
1498
|
+
if let mainConn = _SLSMainConnectionID, let orderWindow = _SLSOrderWindow {
|
|
1499
|
+
let cid = mainConn()
|
|
1500
|
+
let err = orderWindow(cid, wid, 1, 0)
|
|
1501
|
+
if err != .success {
|
|
1502
|
+
diag.warn("present: SLSOrderWindow failed for wid \(wid): \(err)")
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// 5. Set as main window via AX
|
|
1507
|
+
let appRef = AXUIElementCreateApplication(pid)
|
|
1508
|
+
var windowsRef: CFTypeRef?
|
|
1509
|
+
if AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef) == .success,
|
|
1510
|
+
let axWindows = windowsRef as? [AXUIElement] {
|
|
1511
|
+
for axWin in axWindows {
|
|
1512
|
+
var windowId: CGWindowID = 0
|
|
1513
|
+
if _AXUIElementGetWindow(axWin, &windowId) == .success, windowId == wid {
|
|
1514
|
+
AXUIElementSetAttributeValue(axWin, kAXMainAttribute as CFString, kCFBooleanTrue)
|
|
1515
|
+
AXUIElementPerformAction(axWin, kAXRaiseAction as CFString)
|
|
1516
|
+
break
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// 6. Re-raise after delays to defeat focus stealing from the caller.
|
|
1522
|
+
// Two passes: early catch (200ms) and late catch (600ms) for slower responses.
|
|
1523
|
+
for delay in [0.2, 0.6] {
|
|
1524
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
1525
|
+
if let mainConn = _SLSMainConnectionID, let orderWindow = _SLSOrderWindow {
|
|
1526
|
+
let cid = mainConn()
|
|
1527
|
+
orderWindow(cid, wid, 1, 0)
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
DesktopModel.shared.markInteraction(wid: wid)
|
|
1533
|
+
return true
|
|
1309
1534
|
}
|
|
1310
1535
|
|
|
1311
1536
|
/// Move AND raise windows in a single CG+AX pass (avoids duplicate lookups).
|
|
@@ -1383,6 +1608,7 @@ enum WindowTiler {
|
|
|
1383
1608
|
|
|
1384
1609
|
// Unfreeze screen rendering
|
|
1385
1610
|
if let cid { _ = _SLSReenableUpdate?(cid) }
|
|
1611
|
+
DesktopModel.shared.markInteraction(wids: moves.map(\.wid))
|
|
1386
1612
|
diag.success("batchMoveAndRaiseWindows: processed \(processed)/\(moves.count) windows")
|
|
1387
1613
|
}
|
|
1388
1614
|
|
|
@@ -1418,33 +1644,48 @@ enum WindowTiler {
|
|
|
1418
1644
|
}
|
|
1419
1645
|
}
|
|
1420
1646
|
|
|
1421
|
-
/// Compute grid slot rects in AX coordinates (top-left origin) for N windows
|
|
1422
|
-
|
|
1647
|
+
/// Compute grid slot rects in AX coordinates (top-left origin) for N windows.
|
|
1648
|
+
/// If `region` is provided (fractional x, y, w, h), slots are constrained to that sub-area of the screen.
|
|
1649
|
+
static func computeGridSlots(count: Int, screen: NSScreen, region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil) -> [CGRect] {
|
|
1423
1650
|
guard count > 0 else { return [] }
|
|
1424
1651
|
let visible = screen.visibleFrame
|
|
1425
1652
|
guard let primaryScreen = NSScreen.screens.first else { return [] }
|
|
1426
1653
|
let primaryHeight = primaryScreen.frame.height
|
|
1427
1654
|
|
|
1428
|
-
//
|
|
1429
|
-
let
|
|
1655
|
+
// Compute the target area — full visible frame or a fractional sub-region
|
|
1656
|
+
let targetArea: CGRect
|
|
1657
|
+
if let (rx, ry, rw, rh) = region {
|
|
1658
|
+
targetArea = CGRect(
|
|
1659
|
+
x: visible.origin.x + visible.width * rx,
|
|
1660
|
+
y: visible.origin.y + visible.height * (1.0 - ry - rh), // NSRect is bottom-left origin
|
|
1661
|
+
width: visible.width * rw,
|
|
1662
|
+
height: visible.height * rh
|
|
1663
|
+
)
|
|
1664
|
+
} else {
|
|
1665
|
+
targetArea = visible
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// AX Y of target area's top edge
|
|
1669
|
+
let axTop = primaryHeight - targetArea.maxY
|
|
1430
1670
|
let shape = gridShape(for: count)
|
|
1431
1671
|
let rowCount = shape.count
|
|
1432
|
-
let rowH =
|
|
1672
|
+
let rowH = targetArea.height / CGFloat(rowCount)
|
|
1433
1673
|
|
|
1434
1674
|
var slots: [CGRect] = []
|
|
1435
1675
|
for (row, cols) in shape.enumerated() {
|
|
1436
|
-
let colW =
|
|
1676
|
+
let colW = targetArea.width / CGFloat(cols)
|
|
1437
1677
|
let axY = axTop + CGFloat(row) * rowH
|
|
1438
1678
|
for col in 0..<cols {
|
|
1439
|
-
let x =
|
|
1679
|
+
let x = targetArea.origin.x + CGFloat(col) * colW
|
|
1440
1680
|
slots.append(CGRect(x: x, y: axY, width: colW, height: rowH))
|
|
1441
1681
|
}
|
|
1442
1682
|
}
|
|
1443
1683
|
return slots
|
|
1444
1684
|
}
|
|
1445
1685
|
|
|
1446
|
-
/// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process
|
|
1447
|
-
|
|
1686
|
+
/// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process.
|
|
1687
|
+
/// If `region` is provided (fractional x, y, w, h), the grid is constrained to that sub-area.
|
|
1688
|
+
static func batchRaiseAndDistribute(windows: [(wid: UInt32, pid: Int32)], region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil) {
|
|
1448
1689
|
guard !windows.isEmpty else { return }
|
|
1449
1690
|
let diag = DiagnosticLog.shared
|
|
1450
1691
|
|
|
@@ -1466,10 +1707,11 @@ enum WindowTiler {
|
|
|
1466
1707
|
diag.info("Grid layout: \(windows.count) windows → [\(desc)]")
|
|
1467
1708
|
diag.info(" Screen: \(screen.localizedName) \(Int(screenFrame.width))x\(Int(screenFrame.height))")
|
|
1468
1709
|
diag.info(" Visible: origin=(\(Int(visible.origin.x)),\(Int(visible.origin.y))) size=\(Int(visible.width))x\(Int(visible.height))")
|
|
1710
|
+
if let region { diag.info(" Region: x=\(region.0) y=\(region.1) w=\(region.2) h=\(region.3)") }
|
|
1469
1711
|
diag.info(" Primary height: \(Int(primaryHeight))")
|
|
1470
1712
|
|
|
1471
1713
|
// Pre-compute all target slots
|
|
1472
|
-
let slots = computeGridSlots(count: windows.count, screen: screen)
|
|
1714
|
+
let slots = computeGridSlots(count: windows.count, screen: screen, region: region)
|
|
1473
1715
|
guard slots.count == windows.count else {
|
|
1474
1716
|
diag.warn(" Slot count mismatch: \(slots.count) slots for \(windows.count) windows")
|
|
1475
1717
|
return
|
|
@@ -1593,6 +1835,7 @@ enum WindowTiler {
|
|
|
1593
1835
|
if !failed.isEmpty {
|
|
1594
1836
|
diag.warn("batchRaiseAndDistribute: failed wids=\(failed)")
|
|
1595
1837
|
}
|
|
1838
|
+
DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
|
|
1596
1839
|
diag.success("batchRaiseAndDistribute: moved \(moved)/\(windows.count) [\(desc) grid]")
|
|
1597
1840
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
1598
1841
|
NSApp.activate(ignoringOtherApps: true)
|
|
@@ -1670,6 +1913,10 @@ enum WindowTiler {
|
|
|
1670
1913
|
/// Tile the frontmost window of any app to a position using AX API.
|
|
1671
1914
|
/// Works for any application (Finder, Chrome, etc.), not just terminals.
|
|
1672
1915
|
static func tileFrontmostViaAX(to position: TilePosition) {
|
|
1916
|
+
tileFrontmostViaAX(to: .tile(position))
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
static func tileFrontmostViaAX(to placement: PlacementSpec) {
|
|
1673
1920
|
guard let frontApp = NSWorkspace.shared.frontmostApplication,
|
|
1674
1921
|
frontApp.bundleIdentifier != "com.arach.lattices" else { return }
|
|
1675
1922
|
|
|
@@ -1680,7 +1927,7 @@ enum WindowTiler {
|
|
|
1680
1927
|
let win = axWindow as! AXUIElement
|
|
1681
1928
|
|
|
1682
1929
|
let screen = screenForAXWindow(win)
|
|
1683
|
-
let target = tileFrame(for:
|
|
1930
|
+
let target = tileFrame(for: placement, on: screen)
|
|
1684
1931
|
|
|
1685
1932
|
// Disable enhanced UI on the APP element (not window) — breaks macOS tile lock
|
|
1686
1933
|
AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, false as CFTypeRef)
|
|
@@ -1736,6 +1983,20 @@ enum WindowTiler {
|
|
|
1736
1983
|
?? NSScreen.screens[0]
|
|
1737
1984
|
}
|
|
1738
1985
|
|
|
1986
|
+
static func screenForWindowFrame(_ frame: WindowFrame) -> NSScreen {
|
|
1987
|
+
guard let primaryScreen = NSScreen.screens.first else {
|
|
1988
|
+
return NSScreen.main ?? NSScreen.screens[0]
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
let centerX = frame.x + frame.w / 2
|
|
1992
|
+
let centerY = frame.y + frame.h / 2
|
|
1993
|
+
let nsCenterY = primaryScreen.frame.height - centerY
|
|
1994
|
+
|
|
1995
|
+
return NSScreen.screens.first(where: {
|
|
1996
|
+
$0.frame.contains(NSPoint(x: centerX, y: nsCenterY))
|
|
1997
|
+
}) ?? NSScreen.main ?? primaryScreen
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1739
2000
|
// MARK: - Private
|
|
1740
2001
|
|
|
1741
2002
|
private static func tileAppleScript(app: String, tag: String, bounds: (Int, Int, Int, Int)) {
|