@lattices/cli 0.4.1 → 0.4.5
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 +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/ActionRow.swift +43 -26
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +91 -30
- package/app/Sources/AppShellView.swift +2 -0
- package/app/Sources/AppTypeClassifier.swift +36 -0
- package/app/Sources/AppUpdater.swift +92 -0
- package/app/Sources/CheatSheetHUD.swift +1 -0
- package/app/Sources/CliActionLauncher.swift +50 -0
- package/app/Sources/CommandModeView.swift +4 -24
- package/app/Sources/CompanionActivityLog.swift +70 -0
- package/app/Sources/CompanionKeyboardController.swift +141 -0
- package/app/Sources/DesktopModel.swift +4 -0
- package/app/Sources/HandsOffSession.swift +53 -16
- package/app/Sources/HomeDashboardView.swift +18 -10
- package/app/Sources/HotkeyStore.swift +8 -5
- package/app/Sources/IntentEngine.swift +7 -1
- package/app/Sources/LatticesApi.swift +125 -4
- package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/LatticesDeckHost.swift +1463 -0
- package/app/Sources/LatticesRuntime.swift +61 -0
- package/app/Sources/MainView.swift +398 -186
- package/app/Sources/MouseFinder.swift +335 -30
- package/app/Sources/MouseGestureConfig.swift +364 -0
- package/app/Sources/MouseGestureController.swift +1203 -0
- package/app/Sources/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/MouseInputEventViewer.swift +272 -0
- package/app/Sources/MouseShortcutStore.swift +107 -0
- package/app/Sources/OmniSearchView.swift +136 -2
- package/app/Sources/OmniSearchWindow.swift +65 -5
- package/app/Sources/OnboardingView.swift +30 -16
- package/app/Sources/PaletteCommand.swift +26 -6
- package/app/Sources/PermissionChecker.swift +76 -2
- package/app/Sources/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/PiAuthPromptCard.swift +90 -0
- package/app/Sources/PiChatDock.swift +137 -74
- package/app/Sources/PiChatSession.swift +608 -108
- package/app/Sources/PiInstallCallout.swift +86 -0
- package/app/Sources/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/PiWorkspaceView.swift +174 -77
- package/app/Sources/Preferences.swift +78 -0
- package/app/Sources/ScreenMapState.swift +91 -31
- package/app/Sources/ScreenMapView.swift +510 -524
- package/app/Sources/ScreenMapWindowController.swift +12 -4
- package/app/Sources/SettingsView.swift +869 -152
- package/app/Sources/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/VoiceCommandWindow.swift +23 -2
- package/app/Sources/WindowDragSnapController.swift +628 -0
- package/app/Sources/WindowTiler.swift +328 -65
- package/app/Sources/WorkspaceManager.swift +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +65 -1
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -2
|
@@ -51,10 +51,10 @@ final class WindowHighlight {
|
|
|
51
51
|
private var fadeTimer: Timer?
|
|
52
52
|
|
|
53
53
|
/// Flash a green border overlay at the given screen frame
|
|
54
|
-
func flash(frame: NSRect, duration: TimeInterval =
|
|
54
|
+
func flash(frame: NSRect, duration: TimeInterval = 0.9) {
|
|
55
55
|
dismiss()
|
|
56
56
|
|
|
57
|
-
let inset: CGFloat = -
|
|
57
|
+
let inset: CGFloat = -6 // slightly larger than the window
|
|
58
58
|
let expandedFrame = frame.insetBy(dx: inset, dy: inset)
|
|
59
59
|
|
|
60
60
|
let window = NSWindow(
|
|
@@ -110,22 +110,28 @@ final class WindowHighlight {
|
|
|
110
110
|
|
|
111
111
|
private class HighlightBorderView: NSView {
|
|
112
112
|
override func draw(_ dirtyRect: NSRect) {
|
|
113
|
-
let borderWidth: CGFloat =
|
|
113
|
+
let borderWidth: CGFloat = 3
|
|
114
114
|
let cornerRadius: CGFloat = 12
|
|
115
115
|
|
|
116
116
|
// Outer glow
|
|
117
117
|
let glowRect = bounds.insetBy(dx: 1, dy: 1)
|
|
118
118
|
let glowPath = NSBezierPath(roundedRect: glowRect, xRadius: cornerRadius + 2, yRadius: cornerRadius + 2)
|
|
119
|
-
glowPath.lineWidth = borderWidth +
|
|
120
|
-
NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.
|
|
119
|
+
glowPath.lineWidth = borderWidth + 2
|
|
120
|
+
NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.07).setStroke()
|
|
121
121
|
glowPath.stroke()
|
|
122
122
|
|
|
123
123
|
// Main border
|
|
124
124
|
let rect = bounds.insetBy(dx: borderWidth / 2 + 2, dy: borderWidth / 2 + 2)
|
|
125
125
|
let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
|
|
126
126
|
path.lineWidth = borderWidth
|
|
127
|
-
NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.
|
|
127
|
+
NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.58).setStroke()
|
|
128
128
|
path.stroke()
|
|
129
|
+
|
|
130
|
+
let innerRect = rect.insetBy(dx: 3, dy: 3)
|
|
131
|
+
let innerPath = NSBezierPath(roundedRect: innerRect, xRadius: max(cornerRadius - 3, 6), yRadius: max(cornerRadius - 3, 6))
|
|
132
|
+
innerPath.lineWidth = 1
|
|
133
|
+
NSColor.white.withAlphaComponent(0.10).setStroke()
|
|
134
|
+
innerPath.stroke()
|
|
129
135
|
}
|
|
130
136
|
}
|
|
131
137
|
|
|
@@ -588,6 +594,81 @@ enum WindowTiler {
|
|
|
588
594
|
return Int(getActive(mainConn()))
|
|
589
595
|
}
|
|
590
596
|
|
|
597
|
+
static func adjacentSpace(in spaces: [SpaceInfo], currentSpaceId: Int, offset: Int) -> SpaceInfo? {
|
|
598
|
+
guard let currentIndex = spaces.firstIndex(where: { $0.id == currentSpaceId }) else { return nil }
|
|
599
|
+
let targetIndex = currentIndex + offset
|
|
600
|
+
guard spaces.indices.contains(targetIndex) else { return nil }
|
|
601
|
+
return spaces[targetIndex]
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private struct AdjacentSpaceContext {
|
|
605
|
+
let point: CGPoint
|
|
606
|
+
let display: DisplaySpaces
|
|
607
|
+
let activeSpaceId: Int
|
|
608
|
+
let currentSpaceId: Int
|
|
609
|
+
let target: SpaceInfo?
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private static func adjacentSpaceContext(offset: Int, from cgPoint: CGPoint? = nil) -> AdjacentSpaceContext? {
|
|
613
|
+
let point = cgPoint ?? currentMouseCGPoint()
|
|
614
|
+
guard let display = displaySpaces(containing: point) else { return nil }
|
|
615
|
+
let activeSpaceId = getCurrentSpace()
|
|
616
|
+
let currentSpaceId = resolvedCurrentSpaceId(for: display, activeSpaceId: activeSpaceId)
|
|
617
|
+
let target = adjacentSpace(in: display.spaces, currentSpaceId: currentSpaceId, offset: offset)
|
|
618
|
+
return AdjacentSpaceContext(
|
|
619
|
+
point: point,
|
|
620
|
+
display: display,
|
|
621
|
+
activeSpaceId: activeSpaceId,
|
|
622
|
+
currentSpaceId: currentSpaceId,
|
|
623
|
+
target: target
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
static func adjacentSpaceTarget(offset: Int, from cgPoint: CGPoint? = nil) -> SpaceInfo? {
|
|
628
|
+
adjacentSpaceContext(offset: offset, from: cgPoint)?.target
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
@discardableResult
|
|
632
|
+
static func switchToAdjacentSpace(offset: Int, from cgPoint: CGPoint? = nil) -> Bool {
|
|
633
|
+
guard let context = adjacentSpaceContext(offset: offset, from: cgPoint) else {
|
|
634
|
+
DiagnosticLog.shared.warn("switchToAdjacentSpace: no adjacent space for offset \(offset) from \(formatCGPoint(cgPoint ?? currentMouseCGPoint()))")
|
|
635
|
+
return false
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let spaces = context.display.spaces.map(\.id)
|
|
639
|
+
let targetText = context.target.map { String($0.id) } ?? "none"
|
|
640
|
+
DiagnosticLog.shared.info(
|
|
641
|
+
"switchToAdjacentSpace: offset=\(offset) point=\(formatCGPoint(context.point)) displayId=\(context.display.displayId) active=\(context.activeSpaceId) displayCurrent=\(context.display.currentSpaceId) resolved=\(context.currentSpaceId) target=\(targetText) spaces=\(spaces)"
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
if getDisplaySpaces().count == 1 {
|
|
645
|
+
if let finalSpaceId = switchToAdjacentSpaceViaSystemShortcut(
|
|
646
|
+
offset: offset,
|
|
647
|
+
displayId: context.display.displayId,
|
|
648
|
+
initialSpaceId: context.currentSpaceId
|
|
649
|
+
) {
|
|
650
|
+
if let target = context.target, finalSpaceId != target.id {
|
|
651
|
+
DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId) (expected \(target.id))")
|
|
652
|
+
} else {
|
|
653
|
+
DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId)")
|
|
654
|
+
}
|
|
655
|
+
return true
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
guard let target = context.target else {
|
|
659
|
+
DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId) and there is no adjacent space")
|
|
660
|
+
return false
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId), falling back to SkyLight target \(target.id)")
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
guard let target = context.target else { return false }
|
|
667
|
+
let switched = switchToSpace(spaceId: target.id)
|
|
668
|
+
DiagnosticLog.shared.info("switchToAdjacentSpace: SkyLight \(switched ? "reached" : "missed") target \(target.id)")
|
|
669
|
+
return switched
|
|
670
|
+
}
|
|
671
|
+
|
|
591
672
|
/// Find a window by its title tag and return its CGWindowID and owner PID
|
|
592
673
|
static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
|
|
593
674
|
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
@@ -614,10 +695,12 @@ enum WindowTiler {
|
|
|
614
695
|
return result.map { $0.intValue }
|
|
615
696
|
}
|
|
616
697
|
|
|
617
|
-
/// Switch a display to a specific Space
|
|
618
|
-
|
|
698
|
+
/// Switch a display to a specific Space.
|
|
699
|
+
/// Returns true once the requested Space becomes current.
|
|
700
|
+
@discardableResult
|
|
701
|
+
static func switchToSpace(spaceId: Int, verify: Bool = true) -> Bool {
|
|
619
702
|
guard let mainConn = CGS.mainConnectionID,
|
|
620
|
-
let setSpace = CGS.setCurrentSpace else { return }
|
|
703
|
+
let setSpace = CGS.setCurrentSpace else { return false }
|
|
621
704
|
|
|
622
705
|
let cid = mainConn()
|
|
623
706
|
|
|
@@ -625,10 +708,37 @@ enum WindowTiler {
|
|
|
625
708
|
let allDisplays = getDisplaySpaces()
|
|
626
709
|
for display in allDisplays {
|
|
627
710
|
if display.spaces.contains(where: { $0.id == spaceId }) {
|
|
711
|
+
let initialSpace = display.currentSpaceId
|
|
712
|
+
if initialSpace == spaceId {
|
|
713
|
+
DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) already on target \(spaceId)")
|
|
714
|
+
return true
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
DiagnosticLog.shared.info(
|
|
718
|
+
"switchToSpace: requesting \(spaceId) on display \(display.displayIndex) id=\(display.displayId) from \(initialSpace)"
|
|
719
|
+
)
|
|
628
720
|
setSpace(cid, display.displayId as CFString, UInt64(spaceId))
|
|
629
|
-
return
|
|
721
|
+
guard verify else { return true }
|
|
722
|
+
|
|
723
|
+
let deadline = Date().addingTimeInterval(0.45)
|
|
724
|
+
while Date() < deadline {
|
|
725
|
+
usleep(30_000)
|
|
726
|
+
let current = getDisplaySpaces().first(where: { $0.displayId == display.displayId })?.currentSpaceId ?? 0
|
|
727
|
+
if current == spaceId {
|
|
728
|
+
DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) confirmed target \(spaceId)")
|
|
729
|
+
return true
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
DiagnosticLog.shared.warn(
|
|
734
|
+
"switchToSpace: requested \(spaceId) on display \(display.displayIndex) from \(initialSpace), but current Space did not change"
|
|
735
|
+
)
|
|
736
|
+
return false
|
|
630
737
|
}
|
|
631
738
|
}
|
|
739
|
+
|
|
740
|
+
DiagnosticLog.shared.warn("switchToSpace: couldn't resolve display for space \(spaceId)")
|
|
741
|
+
return false
|
|
632
742
|
}
|
|
633
743
|
|
|
634
744
|
// MARK: - Move Window Between Spaces
|
|
@@ -1206,14 +1316,11 @@ enum WindowTiler {
|
|
|
1206
1316
|
}
|
|
1207
1317
|
|
|
1208
1318
|
/// Distribute ALL visible non-Lattices windows into a smart grid on the screen with the most windows.
|
|
1209
|
-
static func distributeVisible() {
|
|
1319
|
+
static func distributeVisible(reactivateLattices: Bool = true) {
|
|
1210
1320
|
let diag = DiagnosticLog.shared
|
|
1211
1321
|
let t = diag.startTimed("distributeVisible")
|
|
1212
1322
|
|
|
1213
|
-
let
|
|
1214
|
-
let visible = allEntries.filter { entry in
|
|
1215
|
-
entry.isOnScreen && entry.app != "Lattices" && entry.frame.w > 50 && entry.frame.h > 50
|
|
1216
|
-
}
|
|
1323
|
+
let visible = visibleDistributableWindows()
|
|
1217
1324
|
|
|
1218
1325
|
guard !visible.isEmpty else {
|
|
1219
1326
|
diag.info("distributeVisible: no visible windows to distribute")
|
|
@@ -1223,7 +1330,59 @@ enum WindowTiler {
|
|
|
1223
1330
|
|
|
1224
1331
|
let windows = visible.map { (wid: $0.wid, pid: $0.pid) }
|
|
1225
1332
|
diag.info("distributeVisible: \(windows.count) windows")
|
|
1226
|
-
batchRaiseAndDistribute(windows: windows)
|
|
1333
|
+
batchRaiseAndDistribute(windows: windows, reactivateLattices: reactivateLattices)
|
|
1334
|
+
diag.finish(t)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/// Distribute visible windows matching the frontmost app's broader type.
|
|
1338
|
+
/// Example: when the active app is a terminal, grid all visible terminal windows on that display.
|
|
1339
|
+
static func distributeVisibleByFrontmostType(reactivateLattices: Bool = true) {
|
|
1340
|
+
let diag = DiagnosticLog.shared
|
|
1341
|
+
let t = diag.startTimed("distributeVisibleByFrontmostType")
|
|
1342
|
+
|
|
1343
|
+
let visible = visibleDistributableWindows()
|
|
1344
|
+
guard !visible.isEmpty else {
|
|
1345
|
+
diag.info("distributeVisibleByFrontmostType: no visible windows to distribute")
|
|
1346
|
+
diag.finish(t)
|
|
1347
|
+
return
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
let frontmostAppName = NSWorkspace.shared.frontmostApplication?.localizedName
|
|
1351
|
+
let anchor = frontmostAppName.flatMap { name in
|
|
1352
|
+
visible.first { $0.app.localizedCaseInsensitiveCompare(name) == .orderedSame }
|
|
1353
|
+
} ?? visible.first
|
|
1354
|
+
|
|
1355
|
+
guard let anchor else {
|
|
1356
|
+
diag.info("distributeVisibleByFrontmostType: no anchor window resolved")
|
|
1357
|
+
diag.finish(t)
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
let grouping = AppTypeClassifier.grouping(for: anchor.app)
|
|
1362
|
+
let anchorScreen = screenForWindowFrame(anchor.frame)
|
|
1363
|
+
let anchorScreenId = screenID(for: anchorScreen)
|
|
1364
|
+
|
|
1365
|
+
let sameScreenMatches = visible.filter { entry in
|
|
1366
|
+
AppTypeClassifier.matches(entry.app, grouping: grouping) &&
|
|
1367
|
+
screenID(for: screenForWindowFrame(entry.frame)) == anchorScreenId
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
let matches = sameScreenMatches.isEmpty
|
|
1371
|
+
? visible.filter { AppTypeClassifier.matches($0.app, grouping: grouping) }
|
|
1372
|
+
: sameScreenMatches
|
|
1373
|
+
|
|
1374
|
+
guard !matches.isEmpty else {
|
|
1375
|
+
diag.info("distributeVisibleByFrontmostType: no matches for \(grouping.label)")
|
|
1376
|
+
diag.finish(t)
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
let ordered = sortWindowsForGrid(matches)
|
|
1381
|
+
diag.info("distributeVisibleByFrontmostType: grouping=\(grouping.label) count=\(ordered.count) screen=\(anchorScreen.localizedName)")
|
|
1382
|
+
batchRaiseAndDistribute(
|
|
1383
|
+
windows: ordered.map { (wid: $0.wid, pid: $0.pid) },
|
|
1384
|
+
reactivateLattices: reactivateLattices
|
|
1385
|
+
)
|
|
1227
1386
|
diag.finish(t)
|
|
1228
1387
|
}
|
|
1229
1388
|
|
|
@@ -1685,7 +1844,11 @@ enum WindowTiler {
|
|
|
1685
1844
|
|
|
1686
1845
|
/// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process.
|
|
1687
1846
|
/// If `region` is provided (fractional x, y, w, h), the grid is constrained to that sub-area.
|
|
1688
|
-
static func batchRaiseAndDistribute(
|
|
1847
|
+
static func batchRaiseAndDistribute(
|
|
1848
|
+
windows: [(wid: UInt32, pid: Int32)],
|
|
1849
|
+
region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil,
|
|
1850
|
+
reactivateLattices: Bool = true
|
|
1851
|
+
) {
|
|
1689
1852
|
guard !windows.isEmpty else { return }
|
|
1690
1853
|
let diag = DiagnosticLog.shared
|
|
1691
1854
|
|
|
@@ -1744,91 +1907,99 @@ enum WindowTiler {
|
|
|
1744
1907
|
}
|
|
1745
1908
|
|
|
1746
1909
|
// Group by pid for AX queries, keep slot mapping
|
|
1747
|
-
var
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
byPid[win.pid, default: []].append((wid: win.wid, target: slots[i]))
|
|
1910
|
+
var byPid: [Int32: [(slotIdx: Int, wid: UInt32, target: CGRect)]] = [:]
|
|
1911
|
+
let moves: [(wid: UInt32, pid: Int32, frame: CGRect)] = windows.enumerated().map { index, win in
|
|
1912
|
+
let target = slots[index]
|
|
1913
|
+
byPid[win.pid, default: []].append((slotIdx: index, wid: win.wid, target: target))
|
|
1914
|
+
return (wid: win.wid, pid: win.pid, frame: target)
|
|
1753
1915
|
}
|
|
1754
1916
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
// Pass 1: Move all windows to target positions (no raise yet)
|
|
1917
|
+
// Pass 1: Move all windows using exact wid→AX mapping.
|
|
1758
1918
|
var moved = 0
|
|
1759
1919
|
var failed: [UInt32] = []
|
|
1760
1920
|
var resolvedAXElements: [(slotIdx: Int, el: AXUIElement)] = [] // for raise pass
|
|
1921
|
+
var activatedPids = Set<Int32>()
|
|
1922
|
+
|
|
1923
|
+
let cid = _SLSMainConnectionID?()
|
|
1924
|
+
if let cid { _ = _SLSDisableUpdate?(cid) }
|
|
1761
1925
|
|
|
1762
1926
|
for (pid, windowMoves) in byPid {
|
|
1763
1927
|
let appRef = AXUIElementCreateApplication(pid)
|
|
1928
|
+
AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, false as CFTypeRef)
|
|
1929
|
+
|
|
1764
1930
|
var windowsRef: CFTypeRef?
|
|
1765
1931
|
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
1766
1932
|
guard err == .success, let axWindows = windowsRef as? [AXUIElement] else {
|
|
1767
1933
|
diag.warn(" AX query failed for pid=\(pid) err=\(err.rawValue)")
|
|
1768
1934
|
failed.append(contentsOf: windowMoves.map(\.wid))
|
|
1935
|
+
AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
|
|
1769
1936
|
continue
|
|
1770
1937
|
}
|
|
1771
1938
|
|
|
1772
|
-
var
|
|
1939
|
+
var axByWid: [UInt32: AXUIElement] = [:]
|
|
1773
1940
|
for axWin in axWindows {
|
|
1774
|
-
var
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
var pos = CGPoint.zero; var size = CGSize.zero
|
|
1779
|
-
AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
|
|
1780
|
-
AXValueGetValue(sv as! AXValue, .cgSize, &size)
|
|
1781
|
-
axCache.append(AXWin(el: axWin, pos: pos, size: size))
|
|
1941
|
+
var windowId: CGWindowID = 0
|
|
1942
|
+
if _AXUIElementGetWindow(axWin, &windowId) == .success {
|
|
1943
|
+
axByWid[windowId] = axWin
|
|
1944
|
+
}
|
|
1782
1945
|
}
|
|
1783
1946
|
|
|
1784
1947
|
for wm in windowMoves {
|
|
1785
|
-
guard let
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
guard let ax = axCache.first(where: {
|
|
1791
|
-
abs(cgRect.origin.x - $0.pos.x) < 2 && abs(cgRect.origin.y - $0.pos.y) < 2 &&
|
|
1792
|
-
abs(cgRect.width - $0.size.width) < 2 && abs(cgRect.height - $0.size.height) < 2
|
|
1793
|
-
}) else {
|
|
1794
|
-
diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX match among \(axCache.count) AX windows")
|
|
1795
|
-
for (j, axw) in axCache.enumerated() {
|
|
1796
|
-
diag.info(" AX[\(j)]: pos=(\(Int(axw.pos.x)),\(Int(axw.pos.y))) size=\(Int(axw.size.width))x\(Int(axw.size.height))")
|
|
1948
|
+
guard let axWin = axByWid[wm.wid] else {
|
|
1949
|
+
if let cgRect = cgFrames[wm.wid] {
|
|
1950
|
+
diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX wid match")
|
|
1951
|
+
} else {
|
|
1952
|
+
diag.warn(" wid=\(wm.wid): no CG frame and no AX wid match")
|
|
1797
1953
|
}
|
|
1798
1954
|
failed.append(wm.wid)
|
|
1799
1955
|
continue
|
|
1800
1956
|
}
|
|
1801
1957
|
|
|
1802
|
-
let slotIdx = widToSlot[wm.wid] ?? -1
|
|
1803
|
-
// Move only — raise comes later
|
|
1804
1958
|
var newPos = CGPoint(x: wm.target.origin.x, y: wm.target.origin.y)
|
|
1805
1959
|
var newSize = CGSize(width: wm.target.width, height: wm.target.height)
|
|
1806
|
-
let
|
|
1807
|
-
AXUIElementSetAttributeValue(
|
|
1960
|
+
let sizeErr1 = AXValueCreate(.cgSize, &newSize).map {
|
|
1961
|
+
AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
|
|
1962
|
+
}
|
|
1963
|
+
let posErr = AXValueCreate(.cgPoint, &newPos).map {
|
|
1964
|
+
AXUIElementSetAttributeValue(axWin, kAXPositionAttribute as CFString, $0)
|
|
1808
1965
|
}
|
|
1809
|
-
let
|
|
1810
|
-
AXUIElementSetAttributeValue(
|
|
1966
|
+
let sizeErr2 = AXValueCreate(.cgSize, &newSize).map {
|
|
1967
|
+
AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
|
|
1811
1968
|
}
|
|
1812
|
-
diag.info(" Move[\(slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) posErr=\(
|
|
1813
|
-
resolvedAXElements.append((slotIdx: slotIdx, el:
|
|
1969
|
+
diag.info(" Move[\(wm.slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) sizeErr1=\(sizeErr1?.rawValue ?? -1) posErr=\(posErr?.rawValue ?? -1) sizeErr2=\(sizeErr2?.rawValue ?? -1)")
|
|
1970
|
+
resolvedAXElements.append((slotIdx: wm.slotIdx, el: axWin))
|
|
1814
1971
|
moved += 1
|
|
1815
1972
|
}
|
|
1973
|
+
|
|
1974
|
+
if !activatedPids.contains(pid) {
|
|
1975
|
+
if let app = NSRunningApplication(processIdentifier: pid) {
|
|
1976
|
+
app.activate()
|
|
1977
|
+
activatedPids.insert(pid)
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
|
|
1816
1982
|
}
|
|
1817
1983
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1984
|
+
if let cid { _ = _SLSReenableUpdate?(cid) }
|
|
1985
|
+
|
|
1986
|
+
// Pass 2: Raise all windows in slot order after app activation so final z-order matches the grid.
|
|
1820
1987
|
resolvedAXElements.sort { $0.slotIdx < $1.slotIdx }
|
|
1821
1988
|
for item in resolvedAXElements {
|
|
1822
1989
|
AXUIElementPerformAction(item.el, kAXRaiseAction as CFString)
|
|
1990
|
+
AXUIElementSetAttributeValue(item.el, kAXMainAttribute as CFString, kCFBooleanTrue)
|
|
1823
1991
|
}
|
|
1824
1992
|
diag.info(" Raised \(resolvedAXElements.count) windows in slot order")
|
|
1825
1993
|
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1994
|
+
// Verify and retry drifted windows once using the battle-tested batch mover.
|
|
1995
|
+
let drifted = verifyMoves(moves)
|
|
1996
|
+
if !drifted.isEmpty {
|
|
1997
|
+
diag.warn(" Drifted after distribute: \(drifted.map(\.wid)) — retrying exact move path")
|
|
1998
|
+
usleep(100_000)
|
|
1999
|
+
batchMoveAndRaiseWindows(drifted)
|
|
2000
|
+
let stillDrifted = verifyMoves(drifted)
|
|
2001
|
+
if !stillDrifted.isEmpty {
|
|
2002
|
+
diag.warn(" Still drifted after retry: \(stillDrifted.map(\.wid))")
|
|
1832
2003
|
}
|
|
1833
2004
|
}
|
|
1834
2005
|
|
|
@@ -1837,8 +2008,10 @@ enum WindowTiler {
|
|
|
1837
2008
|
}
|
|
1838
2009
|
DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
|
|
1839
2010
|
diag.success("batchRaiseAndDistribute: moved \(moved)/\(windows.count) [\(desc) grid]")
|
|
1840
|
-
|
|
1841
|
-
|
|
2011
|
+
if reactivateLattices {
|
|
2012
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
2013
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
2014
|
+
}
|
|
1842
2015
|
}
|
|
1843
2016
|
}
|
|
1844
2017
|
|
|
@@ -1997,8 +2170,98 @@ enum WindowTiler {
|
|
|
1997
2170
|
}) ?? NSScreen.main ?? primaryScreen
|
|
1998
2171
|
}
|
|
1999
2172
|
|
|
2173
|
+
private static func currentMouseCGPoint() -> CGPoint {
|
|
2174
|
+
let mouseLocation = NSEvent.mouseLocation
|
|
2175
|
+
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
2176
|
+
return CGPoint(x: mouseLocation.x, y: primaryHeight - mouseLocation.y)
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
private static func switchToAdjacentSpaceViaSystemShortcut(offset: Int, displayId: String, initialSpaceId: Int) -> Int? {
|
|
2180
|
+
let keyCode: CGKeyCode = offset < 0 ? 123 : 124
|
|
2181
|
+
let script = """
|
|
2182
|
+
tell application "System Events"
|
|
2183
|
+
key code \(keyCode) using control down
|
|
2184
|
+
end tell
|
|
2185
|
+
return "ok"
|
|
2186
|
+
"""
|
|
2187
|
+
let result = ProcessQuery.shell(["/usr/bin/osascript", "-e", script])
|
|
2188
|
+
if result != "ok" {
|
|
2189
|
+
DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut script did not complete for offset \(offset)")
|
|
2190
|
+
}
|
|
2191
|
+
return waitForSpaceChange(displayId: displayId, initialSpaceId: initialSpaceId, timeout: 1.2)
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
private static func waitForSpaceChange(displayId: String, initialSpaceId: Int, timeout: TimeInterval) -> Int? {
|
|
2195
|
+
let deadline = Date().addingTimeInterval(timeout)
|
|
2196
|
+
while Date() < deadline {
|
|
2197
|
+
usleep(30_000)
|
|
2198
|
+
let current = getDisplaySpaces().first(where: { $0.displayId == displayId })?.currentSpaceId ?? 0
|
|
2199
|
+
if current != 0, current != initialSpaceId {
|
|
2200
|
+
return current
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return nil
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
|
|
2207
|
+
guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
|
|
2208
|
+
return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
private static func resolvedCurrentSpaceId(for display: DisplaySpaces, activeSpaceId: Int) -> Int {
|
|
2212
|
+
if display.spaces.contains(where: { $0.id == activeSpaceId }) {
|
|
2213
|
+
return activeSpaceId
|
|
2214
|
+
}
|
|
2215
|
+
return display.currentSpaceId
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
private static func formatCGPoint(_ point: CGPoint) -> String {
|
|
2219
|
+
"\(Int(point.x)),\(Int(point.y))"
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
private static func screenIndex(for cgPoint: CGPoint) -> Int? {
|
|
2223
|
+
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
2224
|
+
let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
|
|
2225
|
+
return NSScreen.screens.firstIndex(where: { $0.frame.contains(nsPoint) })
|
|
2226
|
+
?? (NSScreen.main != nil ? 0 : nil)
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2000
2229
|
// MARK: - Private
|
|
2001
2230
|
|
|
2231
|
+
private static func visibleDistributableWindows() -> [WindowEntry] {
|
|
2232
|
+
DesktopModel.shared.allWindows().filter { entry in
|
|
2233
|
+
entry.isOnScreen &&
|
|
2234
|
+
entry.app != "Lattices" &&
|
|
2235
|
+
entry.frame.w > 50 &&
|
|
2236
|
+
entry.frame.h > 50
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
private static func sortWindowsForGrid(_ windows: [WindowEntry]) -> [WindowEntry] {
|
|
2241
|
+
windows.sorted { lhs, rhs in
|
|
2242
|
+
let rowTolerance = 40.0
|
|
2243
|
+
let yDelta = lhs.frame.y - rhs.frame.y
|
|
2244
|
+
if abs(yDelta) > rowTolerance {
|
|
2245
|
+
return lhs.frame.y < rhs.frame.y
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
let xDelta = lhs.frame.x - rhs.frame.x
|
|
2249
|
+
if abs(xDelta) > rowTolerance {
|
|
2250
|
+
return lhs.frame.x < rhs.frame.x
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
return lhs.zIndex < rhs.zIndex
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
private static func screenID(for screen: NSScreen) -> String {
|
|
2258
|
+
let key = NSDeviceDescriptionKey("NSScreenNumber")
|
|
2259
|
+
if let number = screen.deviceDescription[key] as? NSNumber {
|
|
2260
|
+
return number.stringValue
|
|
2261
|
+
}
|
|
2262
|
+
return screen.localizedName
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2002
2265
|
private static func tileAppleScript(app: String, tag: String, bounds: (Int, Int, Int, Int)) {
|
|
2003
2266
|
let (x1, y1, x2, y2) = bounds
|
|
2004
2267
|
let script = """
|