@lattices/cli 0.4.5 → 0.4.6
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} +4 -0
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +2 -1
- 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 -108
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +488 -55
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -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 -1
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
- 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/{KeyRecorderView.swift → AppShell/KeyRecorderView.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/{Preferences.swift → AppShell/Preferences.swift} +0 -0
- /package/app/Sources/{SettingsView.swift → AppShell/SettingsView.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/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.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/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.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/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.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,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
|
}
|
package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift}
RENAMED
|
@@ -91,11 +91,14 @@ final class CommandModeState: ObservableObject {
|
|
|
91
91
|
@Published var inventory = CommandModeInventory(activeLayer: nil, layerCount: 0, items: [])
|
|
92
92
|
@Published var chords: [Chord] = []
|
|
93
93
|
@Published var desktopSnapshot: DesktopInventorySnapshot?
|
|
94
|
-
@Published var selectedWindowIds: Set<UInt32> = []
|
|
94
|
+
@Published var selectedWindowIds: Set<UInt32> = [] {
|
|
95
|
+
didSet { syncSharedSelection() }
|
|
96
|
+
}
|
|
95
97
|
@Published var desktopMode: DesktopInventoryMode = .browsing
|
|
96
98
|
@Published var activePreset: FilterPreset? = nil
|
|
97
99
|
@Published var searchQuery: String = ""
|
|
98
100
|
@Published var isSearching: Bool = false
|
|
101
|
+
@Published var gridPreviewPlacement: PlacementSpec? = nil
|
|
99
102
|
|
|
100
103
|
// MARK: - Marquee Drag State
|
|
101
104
|
@Published var isDragging: Bool = false
|
|
@@ -140,6 +143,26 @@ final class CommandModeState: ObservableObject {
|
|
|
140
143
|
selectedWindowIds.first
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
var selectedWindowSummaryText: String {
|
|
147
|
+
let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
|
|
148
|
+
let labels = windows.compactMap { $0.appName }.uniquePrefix(3)
|
|
149
|
+
guard !labels.isEmpty else { return "" }
|
|
150
|
+
if windows.count > labels.count {
|
|
151
|
+
return labels.joined(separator: " • ") + " +\(windows.count - labels.count)"
|
|
152
|
+
}
|
|
153
|
+
return labels.joined(separator: " • ")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
var gridPreviewRegionLabel: String {
|
|
157
|
+
guard let placement = gridPreviewPlacement else { return "Full Screen" }
|
|
158
|
+
switch placement {
|
|
159
|
+
case .tile(let position):
|
|
160
|
+
return position.label
|
|
161
|
+
default:
|
|
162
|
+
return placement.wireValue
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
func isSelected(_ id: UInt32) -> Bool {
|
|
144
167
|
selectedWindowIds.contains(id)
|
|
145
168
|
}
|
|
@@ -344,6 +367,7 @@ final class CommandModeState: ObservableObject {
|
|
|
344
367
|
desktopSnapshot = buildDesktopInventory()
|
|
345
368
|
clearSelection()
|
|
346
369
|
desktopMode = .browsing
|
|
370
|
+
gridPreviewPlacement = nil
|
|
347
371
|
phase = .desktopInventory
|
|
348
372
|
// Don't call onPanelResize here — caller handles initial sizing
|
|
349
373
|
}
|
|
@@ -535,6 +559,7 @@ final class CommandModeState: ObservableObject {
|
|
|
535
559
|
if isSearching && selectedWindowIds.isEmpty { return false }
|
|
536
560
|
if isSearching { deactivateSearch() }
|
|
537
561
|
if !selectedWindowIds.isEmpty {
|
|
562
|
+
gridPreviewPlacement = nil
|
|
538
563
|
desktopMode = .gridPreview
|
|
539
564
|
}
|
|
540
565
|
return true
|
|
@@ -614,15 +639,54 @@ final class CommandModeState: ObservableObject {
|
|
|
614
639
|
|
|
615
640
|
private func handleGridPreviewKey(_ keyCode: UInt16) -> Bool {
|
|
616
641
|
switch keyCode {
|
|
617
|
-
case 53: // Escape —
|
|
618
|
-
|
|
642
|
+
case 53: // Escape — cancel preview, keep selection
|
|
643
|
+
gridPreviewPlacement = nil
|
|
644
|
+
desktopMode = .browsing
|
|
619
645
|
return true
|
|
620
646
|
|
|
621
647
|
case 36, 1: // Enter or s → apply the layout
|
|
622
|
-
showAndDistributeSelected()
|
|
648
|
+
showAndDistributeSelected(in: gridPreviewPlacement)
|
|
649
|
+
gridPreviewPlacement = nil
|
|
623
650
|
desktopMode = .browsing
|
|
624
651
|
return true
|
|
625
652
|
|
|
653
|
+
case 123:
|
|
654
|
+
gridPreviewPlacement = .tile(.left)
|
|
655
|
+
return true
|
|
656
|
+
case 124:
|
|
657
|
+
gridPreviewPlacement = .tile(.right)
|
|
658
|
+
return true
|
|
659
|
+
case 126:
|
|
660
|
+
gridPreviewPlacement = .tile(.top)
|
|
661
|
+
return true
|
|
662
|
+
case 125:
|
|
663
|
+
gridPreviewPlacement = .tile(.bottom)
|
|
664
|
+
return true
|
|
665
|
+
case 18:
|
|
666
|
+
gridPreviewPlacement = .tile(.topLeft)
|
|
667
|
+
return true
|
|
668
|
+
case 19:
|
|
669
|
+
gridPreviewPlacement = .tile(.topRight)
|
|
670
|
+
return true
|
|
671
|
+
case 20:
|
|
672
|
+
gridPreviewPlacement = .tile(.bottomLeft)
|
|
673
|
+
return true
|
|
674
|
+
case 21:
|
|
675
|
+
gridPreviewPlacement = .tile(.bottomRight)
|
|
676
|
+
return true
|
|
677
|
+
case 23:
|
|
678
|
+
gridPreviewPlacement = .tile(.leftThird)
|
|
679
|
+
return true
|
|
680
|
+
case 22:
|
|
681
|
+
gridPreviewPlacement = .tile(.centerThird)
|
|
682
|
+
return true
|
|
683
|
+
case 26:
|
|
684
|
+
gridPreviewPlacement = .tile(.rightThird)
|
|
685
|
+
return true
|
|
686
|
+
case 8:
|
|
687
|
+
gridPreviewPlacement = .tile(.center)
|
|
688
|
+
return true
|
|
689
|
+
|
|
626
690
|
default:
|
|
627
691
|
return true
|
|
628
692
|
}
|
|
@@ -795,20 +859,8 @@ final class CommandModeState: ObservableObject {
|
|
|
795
859
|
private func tileAllSelected(to position: TilePosition) {
|
|
796
860
|
let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
|
|
797
861
|
guard !windows.isEmpty else { return }
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if windows.count >= 2 && (position == .left || position == .right) {
|
|
801
|
-
distributeSelectedHorizontally()
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
DiagnosticLog.shared.info("Tile all \(windows.count): \(position.rawValue)")
|
|
806
|
-
for win in windows {
|
|
807
|
-
WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: position)
|
|
808
|
-
}
|
|
809
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
810
|
-
self?.desktopSnapshot = self?.buildDesktopInventory()
|
|
811
|
-
}
|
|
862
|
+
DiagnosticLog.shared.info("Grid selected \(windows.count): \(position.rawValue)")
|
|
863
|
+
showAndDistributeSelected(in: .tile(position))
|
|
812
864
|
}
|
|
813
865
|
|
|
814
866
|
private func distributeSelectedHorizontally() {
|
|
@@ -849,28 +901,36 @@ final class CommandModeState: ObservableObject {
|
|
|
849
901
|
}
|
|
850
902
|
|
|
851
903
|
/// Show all selected windows AND distribute in smart grid — single batch operation
|
|
852
|
-
func showAndDistributeSelected() {
|
|
904
|
+
func showAndDistributeSelected(in placement: PlacementSpec? = nil) {
|
|
853
905
|
let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
|
|
854
906
|
guard !windows.isEmpty else { return }
|
|
855
907
|
savePositions(for: windows)
|
|
856
|
-
WindowTiler.batchRaiseAndDistribute(
|
|
908
|
+
WindowTiler.batchRaiseAndDistribute(
|
|
909
|
+
windows: windows.map { (wid: $0.id, pid: $0.pid) },
|
|
910
|
+
region: placement?.fractions
|
|
911
|
+
)
|
|
857
912
|
let shape = WindowTiler.gridShape(for: windows.count)
|
|
858
913
|
let grid = shape.map(String.init).joined(separator: "+")
|
|
859
|
-
|
|
914
|
+
let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
|
|
915
|
+
flash("\(windows.count) windows\(region) [\(grid)]")
|
|
860
916
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
861
917
|
self?.desktopSnapshot = self?.buildDesktopInventory()
|
|
862
918
|
}
|
|
863
919
|
}
|
|
864
920
|
|
|
865
921
|
/// Distribute selected in smart grid without raising
|
|
866
|
-
func distributeSelected() {
|
|
922
|
+
func distributeSelected(in placement: PlacementSpec? = nil) {
|
|
867
923
|
let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
|
|
868
924
|
guard !windows.isEmpty else { return }
|
|
869
925
|
savePositions(for: windows)
|
|
870
|
-
WindowTiler.batchRaiseAndDistribute(
|
|
926
|
+
WindowTiler.batchRaiseAndDistribute(
|
|
927
|
+
windows: windows.map { (wid: $0.id, pid: $0.pid) },
|
|
928
|
+
region: placement?.fractions
|
|
929
|
+
)
|
|
871
930
|
let shape = WindowTiler.gridShape(for: windows.count)
|
|
872
931
|
let grid = shape.map(String.init).joined(separator: "+")
|
|
873
|
-
|
|
932
|
+
let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
|
|
933
|
+
flash("\(windows.count) windows\(region) [\(grid)]")
|
|
874
934
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
875
935
|
self?.desktopSnapshot = self?.buildDesktopInventory()
|
|
876
936
|
}
|
|
@@ -960,6 +1020,36 @@ final class CommandModeState: ObservableObject {
|
|
|
960
1020
|
onDismiss?()
|
|
961
1021
|
}
|
|
962
1022
|
|
|
1023
|
+
private func syncSharedSelection() {
|
|
1024
|
+
guard !selectedWindowIds.isEmpty else {
|
|
1025
|
+
WindowSelectionStore.shared.clear(source: "desktop-inventory")
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
let summaries = flatWindowList
|
|
1030
|
+
.filter { selectedWindowIds.contains($0.id) }
|
|
1031
|
+
.map {
|
|
1032
|
+
SelectedWindowSummary(
|
|
1033
|
+
wid: $0.id,
|
|
1034
|
+
app: $0.appName ?? "Window",
|
|
1035
|
+
title: $0.title,
|
|
1036
|
+
latticesSession: $0.latticesSession
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
guard !summaries.isEmpty else { return }
|
|
1041
|
+
WindowSelectionStore.shared.setSelection(summaries, source: "desktop-inventory")
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private func regionLabel(for placement: PlacementSpec) -> String {
|
|
1045
|
+
switch placement {
|
|
1046
|
+
case .tile(let position):
|
|
1047
|
+
return position.label
|
|
1048
|
+
default:
|
|
1049
|
+
return placement.wireValue
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
963
1053
|
// MARK: - Inventory Builder
|
|
964
1054
|
|
|
965
1055
|
private func buildInventory() -> CommandModeInventory {
|
|
@@ -1360,3 +1450,16 @@ final class CommandModeState: ObservableObject {
|
|
|
1360
1450
|
return chords
|
|
1361
1451
|
}
|
|
1362
1452
|
}
|
|
1453
|
+
|
|
1454
|
+
private extension Sequence where Element == String {
|
|
1455
|
+
func uniquePrefix(_ count: Int) -> [String] {
|
|
1456
|
+
var seen = Set<String>()
|
|
1457
|
+
var result: [String] = []
|
|
1458
|
+
for item in self where !seen.contains(item) {
|
|
1459
|
+
seen.insert(item)
|
|
1460
|
+
result.append(item)
|
|
1461
|
+
if result.count == count { break }
|
|
1462
|
+
}
|
|
1463
|
+
return result
|
|
1464
|
+
}
|
|
1465
|
+
}
|