@lattices/cli 0.4.5 → 0.4.7
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/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/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +9 -0
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
- package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +1 -1
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -2
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +27 -2
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +15 -2
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -110
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +228 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +601 -59
- package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +9 -5
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -2
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +20 -7
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +59 -4
- package/docs/component-extraction-roadmap.md +392 -0
- package/package.json +3 -1
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/OmniSearchWindow.swift +0 -165
- /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
- /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
- /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
- /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
- /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
- /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
- /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
- /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
- /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
- /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
- /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
- /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
- /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
- /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
- /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
- /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
- /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
- /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
- /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
- /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
- /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
- /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
- /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
- /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
- /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
- /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ApplicationServices
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
|
|
5
|
+
struct LocatedWindow {
|
|
6
|
+
let wid: UInt32
|
|
7
|
+
let pid: pid_t
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
enum SessionWindowLocator {
|
|
11
|
+
static func tag(for session: String) -> String {
|
|
12
|
+
Terminal.windowTag(for: session)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static func extractSessionName(from title: String) -> String? {
|
|
16
|
+
guard let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) else {
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
let match = String(title[range])
|
|
20
|
+
return String(match.dropFirst(10).dropLast(1))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static func matches(session: String, title: String, extractedSessionName: String? = nil) -> Bool {
|
|
24
|
+
if extractedSessionName == session {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
return title.contains(tag(for: session))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func cachedWindow(forSession session: String, in windows: [UInt32: WindowEntry]) -> WindowEntry? {
|
|
31
|
+
windows.values.first { entry in
|
|
32
|
+
matches(session: session, title: entry.title, extractedSessionName: entry.latticesSession)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static func cachedWindow(forSession session: String, desktopModel: DesktopModel = .shared) -> WindowEntry? {
|
|
37
|
+
cachedWindow(forSession: session, in: desktopModel.windows)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static func findCGWindow(tag: String) -> LocatedWindow? {
|
|
41
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for info in windowList {
|
|
46
|
+
if let name = info[kCGWindowName as String] as? String,
|
|
47
|
+
name.contains(tag),
|
|
48
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
49
|
+
let pid = info[kCGWindowOwnerPID as String] as? pid_t {
|
|
50
|
+
return LocatedWindow(wid: wid, pid: pid)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static func findWindow(session: String, terminal: Terminal) -> LocatedWindow? {
|
|
57
|
+
findWindow(tag: tag(for: session), terminal: terminal)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static func findWindow(tag: String, terminal: Terminal) -> LocatedWindow? {
|
|
61
|
+
if let match = findCGWindow(tag: tag) {
|
|
62
|
+
return match
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if let ax = findAXWindow(terminal: terminal, tag: tag),
|
|
66
|
+
let wid = matchCGWindow(pid: ax.pid, axWindow: ax.window) {
|
|
67
|
+
return LocatedWindow(wid: wid, pid: ax.pid)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static func findAXWindow(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
|
|
74
|
+
let diag = DiagnosticLog.shared
|
|
75
|
+
guard let app = NSWorkspace.shared.runningApplications.first(where: {
|
|
76
|
+
$0.bundleIdentifier == terminal.bundleId
|
|
77
|
+
}) else {
|
|
78
|
+
diag.error("SessionWindowLocator.findAXWindow: \(terminal.rawValue) (\(terminal.bundleId)) not running")
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let pid = app.processIdentifier
|
|
83
|
+
let appRef = AXUIElementCreateApplication(pid)
|
|
84
|
+
var windowsRef: CFTypeRef?
|
|
85
|
+
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
86
|
+
guard err == .success, let windows = windowsRef as? [AXUIElement] else {
|
|
87
|
+
diag.error("SessionWindowLocator.findAXWindow: AX error \(err.rawValue) — Accessibility not granted?")
|
|
88
|
+
return nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
diag.info("SessionWindowLocator.findAXWindow: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
|
|
92
|
+
for win in windows {
|
|
93
|
+
var titleRef: CFTypeRef?
|
|
94
|
+
AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
|
|
95
|
+
let title = titleRef as? String ?? "<no title>"
|
|
96
|
+
if title.contains(tag) {
|
|
97
|
+
diag.success("SessionWindowLocator.findAXWindow: matched \"\(title)\"")
|
|
98
|
+
return (pid, win)
|
|
99
|
+
} else {
|
|
100
|
+
diag.info(" skip: \"\(title)\"")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
diag.warn("SessionWindowLocator.findAXWindow: no window matched tag \(tag)")
|
|
105
|
+
return nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
|
|
109
|
+
var posRef: CFTypeRef?
|
|
110
|
+
var sizeRef: CFTypeRef?
|
|
111
|
+
AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
|
|
112
|
+
AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
|
|
113
|
+
guard let pv = posRef, let sv = sizeRef else { return nil }
|
|
114
|
+
|
|
115
|
+
var pos = CGPoint.zero
|
|
116
|
+
var size = CGSize.zero
|
|
117
|
+
AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
|
|
118
|
+
AXValueGetValue(sv as! AXValue, .cgSize, &size)
|
|
119
|
+
|
|
120
|
+
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for info in windowList {
|
|
125
|
+
guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
|
|
126
|
+
wPid == pid,
|
|
127
|
+
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
128
|
+
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
|
|
129
|
+
var rect = CGRect.zero
|
|
130
|
+
if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
|
|
131
|
+
if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
|
|
132
|
+
abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
|
|
133
|
+
return wid
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return nil
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
struct WindowPreviewCardStyle {
|
|
5
|
+
var containerCornerRadius: CGFloat = 10
|
|
6
|
+
var imageCornerRadius: CGFloat = 8
|
|
7
|
+
var imagePadding: CGFloat = 8
|
|
8
|
+
var background: Color = Palette.surface.opacity(0.8)
|
|
9
|
+
var border: Color = Palette.border
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
struct WindowPreviewCard<Overlay: View>: View {
|
|
13
|
+
let image: NSImage?
|
|
14
|
+
let isLoading: Bool
|
|
15
|
+
let appName: String
|
|
16
|
+
var loadingTitle: String = "Capturing preview"
|
|
17
|
+
var unavailableTitle: String = "Preview unavailable"
|
|
18
|
+
var style: WindowPreviewCardStyle = WindowPreviewCardStyle()
|
|
19
|
+
var holdingPreviousPreview: Bool = false
|
|
20
|
+
@ViewBuilder let overlay: () -> Overlay
|
|
21
|
+
|
|
22
|
+
var body: some View {
|
|
23
|
+
ZStack {
|
|
24
|
+
RoundedRectangle(cornerRadius: style.containerCornerRadius)
|
|
25
|
+
.fill(style.background)
|
|
26
|
+
.overlay(
|
|
27
|
+
RoundedRectangle(cornerRadius: style.containerCornerRadius)
|
|
28
|
+
.strokeBorder(style.border, lineWidth: 0.5)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if let image {
|
|
32
|
+
Image(nsImage: image)
|
|
33
|
+
.resizable()
|
|
34
|
+
.aspectRatio(contentMode: .fit)
|
|
35
|
+
.clipShape(RoundedRectangle(cornerRadius: style.imageCornerRadius))
|
|
36
|
+
.padding(style.imagePadding)
|
|
37
|
+
.opacity(holdingPreviousPreview ? 0.88 : 1)
|
|
38
|
+
} else if isLoading {
|
|
39
|
+
WindowPreviewPlaceholder(
|
|
40
|
+
icon: "photo",
|
|
41
|
+
title: loadingTitle,
|
|
42
|
+
subtitle: appName
|
|
43
|
+
)
|
|
44
|
+
} else {
|
|
45
|
+
WindowPreviewPlaceholder(
|
|
46
|
+
icon: "eye.slash",
|
|
47
|
+
title: unavailableTitle,
|
|
48
|
+
subtitle: appName
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
overlay()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
extension WindowPreviewCard where Overlay == EmptyView {
|
|
58
|
+
init(
|
|
59
|
+
image: NSImage?,
|
|
60
|
+
isLoading: Bool,
|
|
61
|
+
appName: String,
|
|
62
|
+
loadingTitle: String = "Capturing preview",
|
|
63
|
+
unavailableTitle: String = "Preview unavailable",
|
|
64
|
+
style: WindowPreviewCardStyle = WindowPreviewCardStyle(),
|
|
65
|
+
holdingPreviousPreview: Bool = false
|
|
66
|
+
) {
|
|
67
|
+
self.init(
|
|
68
|
+
image: image,
|
|
69
|
+
isLoading: isLoading,
|
|
70
|
+
appName: appName,
|
|
71
|
+
loadingTitle: loadingTitle,
|
|
72
|
+
unavailableTitle: unavailableTitle,
|
|
73
|
+
style: style,
|
|
74
|
+
holdingPreviousPreview: holdingPreviousPreview,
|
|
75
|
+
overlay: { EmptyView() }
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private struct WindowPreviewPlaceholder: View {
|
|
81
|
+
let icon: String
|
|
82
|
+
let title: String
|
|
83
|
+
let subtitle: String
|
|
84
|
+
|
|
85
|
+
var body: some View {
|
|
86
|
+
VStack(spacing: 8) {
|
|
87
|
+
Image(systemName: icon)
|
|
88
|
+
.font(.system(size: 18, weight: .medium))
|
|
89
|
+
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
90
|
+
Text(title)
|
|
91
|
+
.font(Typo.monoBold(10))
|
|
92
|
+
.foregroundColor(Palette.textMuted)
|
|
93
|
+
Text(subtitle)
|
|
94
|
+
.font(Typo.mono(9))
|
|
95
|
+
.foregroundColor(Palette.textDim)
|
|
96
|
+
.lineLimit(1)
|
|
97
|
+
}
|
|
98
|
+
.padding(16)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
final class WindowPreviewStore: ObservableObject {
|
|
5
|
+
static let shared = WindowPreviewStore()
|
|
6
|
+
|
|
7
|
+
@Published private var images: [UInt32: NSImage] = [:]
|
|
8
|
+
@Published private var loading: Set<UInt32> = []
|
|
9
|
+
|
|
10
|
+
private var lastAttemptAt: [UInt32: Date] = [:]
|
|
11
|
+
private var accessOrder: [UInt32] = []
|
|
12
|
+
private let maxCached = 15
|
|
13
|
+
private let queue = DispatchQueue(label: "com.arach.lattices.window-preview", qos: .userInitiated)
|
|
14
|
+
private let previewMaxSize = NSSize(width: 360, height: 190)
|
|
15
|
+
|
|
16
|
+
func image(for wid: UInt32) -> NSImage? {
|
|
17
|
+
if images[wid] != nil {
|
|
18
|
+
touchLRU(wid)
|
|
19
|
+
}
|
|
20
|
+
return images[wid]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func hasSettled(_ wid: UInt32) -> Bool {
|
|
24
|
+
images[wid] != nil || (lastAttemptAt[wid] != nil && !loading.contains(wid))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func isLoading(_ wid: UInt32) -> Bool {
|
|
28
|
+
loading.contains(wid)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func prewarm(windows: [WindowEntry], limit: Int = 4) {
|
|
32
|
+
for window in windows.prefix(limit) {
|
|
33
|
+
load(window: window)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func load(window: WindowEntry) {
|
|
38
|
+
if images[window.wid] != nil || loading.contains(window.wid) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let now = Date()
|
|
43
|
+
if let lastAttemptAt = lastAttemptAt[window.wid], now.timeIntervalSince(lastAttemptAt) < 1.0 {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
lastAttemptAt[window.wid] = now
|
|
47
|
+
|
|
48
|
+
loading.insert(window.wid)
|
|
49
|
+
let wid = window.wid
|
|
50
|
+
let frame = window.frame
|
|
51
|
+
let startedAt = Date()
|
|
52
|
+
|
|
53
|
+
queue.async { [weak self] in
|
|
54
|
+
guard let self else { return }
|
|
55
|
+
|
|
56
|
+
let cgImage = CGWindowListCreateImage(
|
|
57
|
+
.null,
|
|
58
|
+
.optionIncludingWindow,
|
|
59
|
+
CGWindowID(wid),
|
|
60
|
+
[.boundsIgnoreFraming, .nominalResolution]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
let image = cgImage.map {
|
|
64
|
+
NSImage(
|
|
65
|
+
cgImage: $0,
|
|
66
|
+
size: self.previewSize(for: frame)
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
DispatchQueue.main.async {
|
|
71
|
+
self.loading.remove(wid)
|
|
72
|
+
let elapsedMs = Int(Date().timeIntervalSince(startedAt) * 1000)
|
|
73
|
+
if let image {
|
|
74
|
+
self.images[wid] = image
|
|
75
|
+
self.touchLRU(wid)
|
|
76
|
+
self.evictIfNeeded()
|
|
77
|
+
if elapsedMs >= 80 {
|
|
78
|
+
DiagnosticLog.shared.info("HUDPreview: captured wid=\(wid) in \(elapsedMs)ms")
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
DiagnosticLog.shared.info("HUDPreview: capture unavailable wid=\(wid) after \(elapsedMs)ms")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private func touchLRU(_ wid: UInt32) {
|
|
88
|
+
accessOrder.removeAll { $0 == wid }
|
|
89
|
+
accessOrder.append(wid)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func evictIfNeeded() {
|
|
93
|
+
while images.count > maxCached, let oldest = accessOrder.first {
|
|
94
|
+
accessOrder.removeFirst()
|
|
95
|
+
images.removeValue(forKey: oldest)
|
|
96
|
+
lastAttemptAt.removeValue(forKey: oldest)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private func previewSize(for frame: WindowFrame) -> NSSize {
|
|
101
|
+
let width = max(CGFloat(frame.w), CGFloat(1))
|
|
102
|
+
let height = max(CGFloat(frame.h), CGFloat(1))
|
|
103
|
+
let scale = min(
|
|
104
|
+
previewMaxSize.width / width,
|
|
105
|
+
previewMaxSize.height / height,
|
|
106
|
+
CGFloat(1)
|
|
107
|
+
)
|
|
108
|
+
return NSSize(
|
|
109
|
+
width: max(CGFloat(1), width * scale),
|
|
110
|
+
height: max(CGFloat(1), height * scale)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import Combine
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
struct SelectedWindowSummary: Identifiable, Equatable {
|
|
5
|
+
let wid: UInt32
|
|
6
|
+
let app: String
|
|
7
|
+
let title: String
|
|
8
|
+
let latticesSession: String?
|
|
9
|
+
|
|
10
|
+
var id: UInt32 { wid }
|
|
11
|
+
|
|
12
|
+
var displayTitle: String {
|
|
13
|
+
if !title.isEmpty { return title }
|
|
14
|
+
if let latticesSession, !latticesSession.isEmpty { return latticesSession }
|
|
15
|
+
return app
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
final class WindowSelectionStore: ObservableObject {
|
|
20
|
+
static let shared = WindowSelectionStore()
|
|
21
|
+
|
|
22
|
+
@Published private(set) var windows: [SelectedWindowSummary] = []
|
|
23
|
+
@Published private(set) var source: String?
|
|
24
|
+
@Published private(set) var updatedAt: Date?
|
|
25
|
+
|
|
26
|
+
private init() {}
|
|
27
|
+
|
|
28
|
+
var isActive: Bool { !windows.isEmpty }
|
|
29
|
+
var windowIds: [UInt32] { windows.map(\.wid) }
|
|
30
|
+
var count: Int { windows.count }
|
|
31
|
+
var sourceLabel: String? {
|
|
32
|
+
switch source {
|
|
33
|
+
case "desktop-inventory": return "window selector"
|
|
34
|
+
case "screen-map": return "screen map"
|
|
35
|
+
case let value?: return value
|
|
36
|
+
case nil: return nil
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func setSelection(_ windows: [SelectedWindowSummary], source: String) {
|
|
41
|
+
let unique = Array(
|
|
42
|
+
Dictionary(uniqueKeysWithValues: windows.map { ($0.wid, $0) }).values
|
|
43
|
+
).sorted { lhs, rhs in
|
|
44
|
+
if lhs.app == rhs.app { return lhs.wid < rhs.wid }
|
|
45
|
+
return lhs.app.localizedCaseInsensitiveCompare(rhs.app) == .orderedAscending
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
DispatchQueue.main.async {
|
|
49
|
+
self.windows = unique
|
|
50
|
+
self.source = source
|
|
51
|
+
self.updatedAt = Date()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func clear(source: String? = nil) {
|
|
56
|
+
DispatchQueue.main.async {
|
|
57
|
+
if let source, let current = self.source, current != source {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
self.windows = []
|
|
61
|
+
self.source = source ?? self.source
|
|
62
|
+
self.updatedAt = Date()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func summary(maxItems: Int = 3) -> String {
|
|
67
|
+
guard !windows.isEmpty else { return "No selection" }
|
|
68
|
+
let titles = windows.prefix(maxItems).map { item in
|
|
69
|
+
item.app == item.displayTitle ? item.app : "\(item.app): \(item.displayTitle)"
|
|
70
|
+
}
|
|
71
|
+
if windows.count > maxItems {
|
|
72
|
+
return titles.joined(separator: " • ") + " +\(windows.count - maxItems)"
|
|
73
|
+
}
|
|
74
|
+
return titles.joined(separator: " • ")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -671,18 +671,7 @@ enum WindowTiler {
|
|
|
671
671
|
|
|
672
672
|
/// Find a window by its title tag and return its CGWindowID and owner PID
|
|
673
673
|
static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
|
|
674
|
-
|
|
675
|
-
return nil
|
|
676
|
-
}
|
|
677
|
-
for info in windowList {
|
|
678
|
-
if let name = info[kCGWindowName as String] as? String,
|
|
679
|
-
name.contains(tag),
|
|
680
|
-
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
681
|
-
let pid = info[kCGWindowOwnerPID as String] as? pid_t {
|
|
682
|
-
return (wid, pid)
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
return nil
|
|
674
|
+
SessionWindowLocator.findCGWindow(tag: tag).map { ($0.wid, $0.pid) }
|
|
686
675
|
}
|
|
687
676
|
|
|
688
677
|
/// Get the space ID(s) a window is on
|
|
@@ -760,13 +749,9 @@ enum WindowTiler {
|
|
|
760
749
|
|
|
761
750
|
// Find the window — CG first, then AX→CG fallback
|
|
762
751
|
let wid: UInt32
|
|
763
|
-
if let
|
|
764
|
-
wid =
|
|
765
|
-
diag.info("moveWindowToSpace:
|
|
766
|
-
} else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
|
|
767
|
-
let w = matchCGWindow(pid: pid, axWindow: axWindow) {
|
|
768
|
-
wid = w
|
|
769
|
-
diag.info("moveWindowToSpace: found via AX→CG wid=\(w)")
|
|
752
|
+
if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
|
|
753
|
+
wid = match.wid
|
|
754
|
+
diag.info("moveWindowToSpace: located wid=\(match.wid) pid=\(match.pid)")
|
|
770
755
|
} else {
|
|
771
756
|
diag.warn("moveWindowToSpace: window not found for tag \(tag) — switching view only")
|
|
772
757
|
switchToSpace(spaceId: spaceId)
|
|
@@ -834,39 +819,31 @@ enum WindowTiler {
|
|
|
834
819
|
let tag = Terminal.windowTag(for: session)
|
|
835
820
|
|
|
836
821
|
// Path 1: CG window lookup (needs Screen Recording permission for window names)
|
|
837
|
-
if let
|
|
838
|
-
diag.success("Path 1 (
|
|
839
|
-
navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
|
|
822
|
+
if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
|
|
823
|
+
diag.success("Path 1/2 (locator): found wid=\(match.wid) pid=\(match.pid)")
|
|
824
|
+
navigateToKnownWindow(wid: match.wid, pid: match.pid, tag: tag, session: session, terminal: terminal)
|
|
840
825
|
diag.finish(t)
|
|
841
826
|
return
|
|
842
827
|
}
|
|
843
|
-
diag.warn("
|
|
828
|
+
diag.warn("SessionWindowLocator failed — trying direct AX fallback")
|
|
844
829
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if let
|
|
850
|
-
|
|
851
|
-
|
|
830
|
+
if let (pid, axWindow) = SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag) {
|
|
831
|
+
diag.success("Direct AX fallback: found window for \(terminal.rawValue) pid=\(pid)")
|
|
832
|
+
AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
|
|
833
|
+
AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
|
|
834
|
+
if let app = NSRunningApplication(processIdentifier: pid) {
|
|
835
|
+
app.activate()
|
|
836
|
+
}
|
|
837
|
+
if let frame = axWindowFrame(axWindow) {
|
|
838
|
+
diag.info("Highlighting via AX frame: \(frame)")
|
|
839
|
+
DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
|
|
852
840
|
} else {
|
|
853
|
-
diag.
|
|
854
|
-
AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
|
|
855
|
-
AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
|
|
856
|
-
if let app = NSRunningApplication(processIdentifier: pid) {
|
|
857
|
-
app.activate()
|
|
858
|
-
}
|
|
859
|
-
if let frame = axWindowFrame(axWindow) {
|
|
860
|
-
diag.info("Highlighting via AX frame: \(frame)")
|
|
861
|
-
DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
|
|
862
|
-
} else {
|
|
863
|
-
diag.error("axWindowFrame returned nil — no highlight")
|
|
864
|
-
}
|
|
841
|
+
diag.error("axWindowFrame returned nil — no highlight")
|
|
865
842
|
}
|
|
866
843
|
diag.finish(t)
|
|
867
844
|
return
|
|
868
845
|
}
|
|
869
|
-
diag.warn("
|
|
846
|
+
diag.warn("Direct AX fallback failed — no Accessibility?")
|
|
870
847
|
|
|
871
848
|
// Path 3: AppleScript / bare activate fallback
|
|
872
849
|
diag.warn("Path 3: falling back to AppleScript/activate")
|
|
@@ -894,70 +871,12 @@ enum WindowTiler {
|
|
|
894
871
|
}
|
|
895
872
|
}
|
|
896
873
|
|
|
897
|
-
/// Find a terminal window by title tag using AX API (requires Accessibility permission)
|
|
898
874
|
private static func findWindowViaAX(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
|
|
899
|
-
|
|
900
|
-
guard let app = NSWorkspace.shared.runningApplications.first(where: {
|
|
901
|
-
$0.bundleIdentifier == terminal.bundleId
|
|
902
|
-
}) else {
|
|
903
|
-
diag.error("findWindowViaAX: \(terminal.rawValue) (\(terminal.bundleId)) not running")
|
|
904
|
-
return nil
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
let pid = app.processIdentifier
|
|
908
|
-
let appRef = AXUIElementCreateApplication(pid)
|
|
909
|
-
var windowsRef: CFTypeRef?
|
|
910
|
-
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
911
|
-
guard err == .success, let windows = windowsRef as? [AXUIElement] else {
|
|
912
|
-
diag.error("findWindowViaAX: AX error \(err.rawValue) — Accessibility not granted?")
|
|
913
|
-
return nil
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
diag.info("findWindowViaAX: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
|
|
917
|
-
for win in windows {
|
|
918
|
-
var titleRef: CFTypeRef?
|
|
919
|
-
AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
|
|
920
|
-
let title = titleRef as? String ?? "<no title>"
|
|
921
|
-
if title.contains(tag) {
|
|
922
|
-
diag.success("findWindowViaAX: matched \"\(title)\"")
|
|
923
|
-
return (pid, win)
|
|
924
|
-
} else {
|
|
925
|
-
diag.info(" skip: \"\(title)\"")
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
diag.warn("findWindowViaAX: no window matched tag \(tag)")
|
|
929
|
-
return nil
|
|
875
|
+
SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag)
|
|
930
876
|
}
|
|
931
877
|
|
|
932
|
-
/// Match an AX window to its CG window ID using PID + bounds comparison
|
|
933
878
|
private static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
|
|
934
|
-
|
|
935
|
-
var sizeRef: CFTypeRef?
|
|
936
|
-
AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
|
|
937
|
-
AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
|
|
938
|
-
guard let pv = posRef, let sv = sizeRef else { return nil }
|
|
939
|
-
|
|
940
|
-
var pos = CGPoint.zero
|
|
941
|
-
var size = CGSize.zero
|
|
942
|
-
AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
|
|
943
|
-
AXValueGetValue(sv as! AXValue, .cgSize, &size)
|
|
944
|
-
|
|
945
|
-
guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
|
|
946
|
-
|
|
947
|
-
for info in windowList {
|
|
948
|
-
guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
|
|
949
|
-
wPid == pid,
|
|
950
|
-
let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
951
|
-
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
|
|
952
|
-
var rect = CGRect.zero
|
|
953
|
-
if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
|
|
954
|
-
if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
|
|
955
|
-
abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
|
|
956
|
-
return wid
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
return nil
|
|
879
|
+
SessionWindowLocator.matchCGWindow(pid: pid, axWindow: axWindow)
|
|
961
880
|
}
|
|
962
881
|
|
|
963
882
|
/// Get NSRect from an AX window element (AX uses top-left origin, convert to NS bottom-left)
|
|
@@ -1107,11 +1026,8 @@ enum WindowTiler {
|
|
|
1107
1026
|
|
|
1108
1027
|
// Find the window
|
|
1109
1028
|
let wid: UInt32
|
|
1110
|
-
if let
|
|
1111
|
-
wid =
|
|
1112
|
-
} else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
|
|
1113
|
-
let w = matchCGWindow(pid: pid, axWindow: axWindow) {
|
|
1114
|
-
wid = w
|
|
1029
|
+
if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
|
|
1030
|
+
wid = match.wid
|
|
1115
1031
|
} else {
|
|
1116
1032
|
return nil
|
|
1117
1033
|
}
|
|
@@ -2202,7 +2118,6 @@ enum WindowTiler {
|
|
|
2202
2118
|
}
|
|
2203
2119
|
return nil
|
|
2204
2120
|
}
|
|
2205
|
-
|
|
2206
2121
|
private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
|
|
2207
2122
|
guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
|
|
2208
2123
|
return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
|
|
@@ -2218,7 +2133,6 @@ enum WindowTiler {
|
|
|
2218
2133
|
private static func formatCGPoint(_ point: CGPoint) -> String {
|
|
2219
2134
|
"\(Int(point.x)),\(Int(point.y))"
|
|
2220
2135
|
}
|
|
2221
|
-
|
|
2222
2136
|
private static func screenIndex(for cgPoint: CGPoint) -> Int? {
|
|
2223
2137
|
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
2224
2138
|
let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
|