@lattices/cli 0.4.6 → 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/AppShell/AppDelegate.swift +5 -0
- package/app/Sources/AppShell/KeyRecorderView.swift +1 -1
- package/app/Sources/AppShell/Preferences.swift +0 -2
- package/app/Sources/AppShell/SettingsView.swift +27 -2
- package/app/Sources/Core/Actions/HotkeyStore.swift +13 -1
- package/app/Sources/Core/Desktop/WindowTiler.swift +0 -2
- package/app/Sources/Core/Overlays/CommandMode/CommandModeState.swift +101 -0
- package/app/Sources/Core/Overlays/CommandMode/CommandModeView.swift +113 -4
- package/app/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +9 -5
- package/app/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +1 -0
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -1
- package/app/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +20 -7
- package/app/Sources/Core/Workspace/WorkspaceManager.swift +59 -4
- package/package.json +1 -1
package/app/Info.plist
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.7</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.7</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
17
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
18
|
+
<string>0.4.7</string>
|
|
19
19
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
20
|
+
<string>0.4.7</string>
|
|
21
21
|
<key>LSMinimumSystemVersion</key>
|
|
22
22
|
<string>13.0</string>
|
|
23
23
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -162,6 +162,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
162
162
|
}
|
|
163
163
|
store.register(action: .tileDistribute) { WindowTiler.distributeVisible(reactivateLattices: false) }
|
|
164
164
|
store.register(action: .tileTypeGrid) { WindowTiler.distributeVisibleByFrontmostType(reactivateLattices: false) }
|
|
165
|
+
store.register(action: .tileOrganize) {
|
|
166
|
+
let appName = DesktopModel.shared.frontmostWindow()?.app
|
|
167
|
+
?? NSWorkspace.shared.frontmostApplication?.localizedName
|
|
168
|
+
CommandModeWindow.shared.show(launchMode: .organize(appName: appName))
|
|
169
|
+
}
|
|
165
170
|
|
|
166
171
|
// Onboarding on first launch; otherwise just check permissions
|
|
167
172
|
if !OnboardingWindowController.shared.showIfNeeded() {
|
|
@@ -34,7 +34,7 @@ struct KeyRecorderView: View {
|
|
|
34
34
|
.frame(minWidth: 80, alignment: .leading)
|
|
35
35
|
} else if let binding = binding {
|
|
36
36
|
HStack(spacing: 4) {
|
|
37
|
-
ForEach(binding.
|
|
37
|
+
ForEach(binding.compactDisplayParts, id: \.self) { part in
|
|
38
38
|
keyBadge(part)
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -37,7 +37,6 @@ class Preferences: ObservableObject {
|
|
|
37
37
|
@Published var companionCockpitLayout: LatticesCompanionCockpitLayout {
|
|
38
38
|
didSet { persistCompanionCockpitLayout() }
|
|
39
39
|
}
|
|
40
|
-
|
|
41
40
|
@Published var mouseGesturesEnabled: Bool {
|
|
42
41
|
didSet { UserDefaults.standard.set(mouseGesturesEnabled, forKey: "mouseGestures.enabled") }
|
|
43
42
|
}
|
|
@@ -163,7 +162,6 @@ class Preferences: ObservableObject {
|
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
self.companionCockpitLayout = Self.loadCompanionCockpitLayout()
|
|
166
|
-
|
|
167
165
|
if UserDefaults.standard.object(forKey: "mouseGestures.enabled") != nil {
|
|
168
166
|
self.mouseGesturesEnabled = UserDefaults.standard.bool(forKey: "mouseGestures.enabled")
|
|
169
167
|
} else {
|
|
@@ -57,6 +57,7 @@ struct SettingsContentView: View {
|
|
|
57
57
|
@ObservedObject var prefs: Preferences
|
|
58
58
|
@ObservedObject var scanner: ProjectScanner
|
|
59
59
|
@ObservedObject var hotkeyStore: HotkeyStore = .shared
|
|
60
|
+
@ObservedObject var workspaceManager: WorkspaceManager = .shared
|
|
60
61
|
@ObservedObject var appUpdater: AppUpdater = .shared
|
|
61
62
|
@ObservedObject var mouseShortcutStore: MouseShortcutStore = .shared
|
|
62
63
|
var onBack: (() -> Void)? = nil
|
|
@@ -85,6 +86,13 @@ struct SettingsContentView: View {
|
|
|
85
86
|
page == .docs ? "Docs" : selectedTab.title
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
private var snapModifierBinding: Binding<SnapModifierKey> {
|
|
90
|
+
Binding(
|
|
91
|
+
get: { workspaceManager.snapZonesConfig.modifier ?? .command },
|
|
92
|
+
set: { workspaceManager.updateSnapModifier($0) }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
private var backBar: some View {
|
|
89
97
|
VStack(spacing: 0) {
|
|
90
98
|
HStack(spacing: 8) {
|
|
@@ -459,13 +467,28 @@ struct SettingsContentView: View {
|
|
|
459
467
|
.labelsHidden()
|
|
460
468
|
}
|
|
461
469
|
|
|
462
|
-
|
|
470
|
+
HStack {
|
|
471
|
+
Text("Snap modifier")
|
|
472
|
+
.font(Typo.mono(10))
|
|
473
|
+
.foregroundColor(Palette.textDim)
|
|
474
|
+
Spacer()
|
|
475
|
+
Picker("", selection: snapModifierBinding) {
|
|
476
|
+
ForEach(SnapModifierKey.allCases) { modifier in
|
|
477
|
+
Text(modifier.shortLabel).tag(modifier)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
.pickerStyle(.segmented)
|
|
481
|
+
.labelsHidden()
|
|
482
|
+
.frame(width: 220)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
Text("Dragging stays normal until you hold \(snapModifierBinding.wrappedValue.label). While that key is down, Lattices reveals snap targets and a live preview for the window you’re moving.")
|
|
463
486
|
.font(Typo.caption(9))
|
|
464
487
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
465
488
|
|
|
466
489
|
cardDivider
|
|
467
490
|
|
|
468
|
-
Text("
|
|
491
|
+
Text("Advanced landing-zone rules still live in ~/.lattices/snap-zones.json. Modifier changes here take effect on the next drag.")
|
|
469
492
|
.font(Typo.caption(9))
|
|
470
493
|
.foregroundColor(Palette.textMuted.opacity(0.7))
|
|
471
494
|
}
|
|
@@ -1541,6 +1564,8 @@ struct SettingsContentView: View {
|
|
|
1541
1564
|
.foregroundColor(Palette.textMuted)
|
|
1542
1565
|
.fixedSize(horizontal: false, vertical: true)
|
|
1543
1566
|
}
|
|
1567
|
+
|
|
1568
|
+
compactKeyRecorder(action: .tileOrganize)
|
|
1544
1569
|
}
|
|
1545
1570
|
}
|
|
1546
1571
|
|
|
@@ -31,7 +31,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
31
31
|
// Tiling
|
|
32
32
|
case tileLeft, tileRight, tileMaximize, tileCenter
|
|
33
33
|
case tileTopLeft, tileTopRight, tileBottomLeft, tileBottomRight
|
|
34
|
-
case tileTop, tileBottom, tileDistribute, tileTypeGrid
|
|
34
|
+
case tileTop, tileBottom, tileDistribute, tileTypeGrid, tileOrganize
|
|
35
35
|
case tileLeftThird, tileCenterThird, tileRightThird
|
|
36
36
|
|
|
37
37
|
var label: String {
|
|
@@ -71,6 +71,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
71
71
|
case .tileBottom: return "Bottom Half"
|
|
72
72
|
case .tileDistribute: return "Distribute"
|
|
73
73
|
case .tileTypeGrid: return "Grid Type"
|
|
74
|
+
case .tileOrganize: return "Organize Windows"
|
|
74
75
|
case .tileLeftThird: return "Left Third"
|
|
75
76
|
case .tileCenterThird: return "Center Third"
|
|
76
77
|
case .tileRightThird: return "Right Third"
|
|
@@ -127,6 +128,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
127
128
|
case .tileLeftThird: return 311
|
|
128
129
|
case .tileCenterThird: return 312
|
|
129
130
|
case .tileRightThird: return 313
|
|
131
|
+
case .tileOrganize: return 315
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
@@ -142,6 +144,15 @@ struct KeyBinding: Codable, Equatable {
|
|
|
142
144
|
let carbonModifiers: UInt32
|
|
143
145
|
var displayParts: [String]
|
|
144
146
|
|
|
147
|
+
var compactDisplayParts: [String] {
|
|
148
|
+
guard let key = displayParts.last else { return [] }
|
|
149
|
+
let modifiers = Set(displayParts.dropLast())
|
|
150
|
+
if modifiers == Set(["Ctrl", "Option", "Shift", "Cmd"]) {
|
|
151
|
+
return ["Hyper", key]
|
|
152
|
+
}
|
|
153
|
+
return displayParts
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
|
|
146
157
|
var mods: UInt32 = 0
|
|
147
158
|
if flags.contains(.command) { mods |= UInt32(cmdKey) }
|
|
@@ -266,6 +277,7 @@ class HotkeyStore: ObservableObject {
|
|
|
266
277
|
bind(.tileLeftThird, 18, ctrlOpt) // Ctrl+Opt+1
|
|
267
278
|
bind(.tileCenterThird, 19, ctrlOpt) // Ctrl+Opt+2
|
|
268
279
|
bind(.tileRightThird, 20, ctrlOpt) // Ctrl+Opt+3
|
|
280
|
+
bind(.tileOrganize, 31, ctrlOpt) // Ctrl+Opt+O
|
|
269
281
|
|
|
270
282
|
return d
|
|
271
283
|
}()
|
|
@@ -2118,7 +2118,6 @@ enum WindowTiler {
|
|
|
2118
2118
|
}
|
|
2119
2119
|
return nil
|
|
2120
2120
|
}
|
|
2121
|
-
|
|
2122
2121
|
private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
|
|
2123
2122
|
guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
|
|
2124
2123
|
return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
|
|
@@ -2134,7 +2133,6 @@ enum WindowTiler {
|
|
|
2134
2133
|
private static func formatCGPoint(_ point: CGPoint) -> String {
|
|
2135
2134
|
"\(Int(point.x)),\(Int(point.y))"
|
|
2136
2135
|
}
|
|
2137
|
-
|
|
2138
2136
|
private static func screenIndex(for cgPoint: CGPoint) -> Int? {
|
|
2139
2137
|
let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
|
|
2140
2138
|
let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
|
|
@@ -45,6 +45,11 @@ enum DesktopInventoryMode: Equatable {
|
|
|
45
45
|
case screenMap // m → interactive screen map editor
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
enum CommandModeLaunchMode: Equatable {
|
|
49
|
+
case normal
|
|
50
|
+
case organize(appName: String?)
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
// DisplayGeometry, ScreenMapWindowEntry, ScreenMapEditorState, ScreenMapActionLog
|
|
49
54
|
// are defined in ScreenMapState.swift
|
|
50
55
|
// MARK: - Filter Presets
|
|
@@ -132,10 +137,43 @@ final class CommandModeState: ObservableObject {
|
|
|
132
137
|
|
|
133
138
|
var onDismiss: (() -> Void)?
|
|
134
139
|
var onPanelResize: ((_ width: CGFloat, _ height: CGFloat) -> Void)?
|
|
140
|
+
private let launchMode: CommandModeLaunchMode
|
|
135
141
|
|
|
136
142
|
/// Tracks the last item navigated to, for consistent Shift+arrow multi-select
|
|
137
143
|
private var cursorWindowId: UInt32?
|
|
138
144
|
|
|
145
|
+
init(launchMode: CommandModeLaunchMode = .normal) {
|
|
146
|
+
self.launchMode = launchMode
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var isOrganizeFlow: Bool {
|
|
150
|
+
if case .organize = launchMode { return true }
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
var organizeSeedAppName: String? {
|
|
155
|
+
if case .organize(let appName) = launchMode { return appName }
|
|
156
|
+
return nil
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
var organizeSelectionSummary: String {
|
|
160
|
+
let count = selectedWindowIds.count
|
|
161
|
+
if let appName = organizeSeedAppName, !appName.isEmpty {
|
|
162
|
+
return "\(count) \(appName) window\(count == 1 ? "" : "s") selected"
|
|
163
|
+
}
|
|
164
|
+
return "\(count) window\(count == 1 ? "" : "s") selected"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
var organizeGuidance: String {
|
|
168
|
+
if selectedWindowIds.count > 1 {
|
|
169
|
+
return "Press D to organize. Cmd-click adds or removes windows. Shift-click extends the selection."
|
|
170
|
+
}
|
|
171
|
+
if selectedWindowIds.count == 1 {
|
|
172
|
+
return "Cmd-click another window to add it, then press D to organize the set."
|
|
173
|
+
}
|
|
174
|
+
return "Click windows to select them. Cmd-click adds or removes windows, and D organizes the selection."
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
// MARK: - Selection Helpers
|
|
140
178
|
|
|
141
179
|
/// Backwards-compat: returns single selected ID (first element)
|
|
@@ -369,6 +407,7 @@ final class CommandModeState: ObservableObject {
|
|
|
369
407
|
desktopMode = .browsing
|
|
370
408
|
gridPreviewPlacement = nil
|
|
371
409
|
phase = .desktopInventory
|
|
410
|
+
configureLaunchMode()
|
|
372
411
|
// Don't call onPanelResize here — caller handles initial sizing
|
|
373
412
|
}
|
|
374
413
|
|
|
@@ -576,6 +615,20 @@ final class CommandModeState: ObservableObject {
|
|
|
576
615
|
}
|
|
577
616
|
return true
|
|
578
617
|
|
|
618
|
+
case 2: // d → distribute selected
|
|
619
|
+
if isSearching && selectedWindowIds.isEmpty { return false }
|
|
620
|
+
if isSearching { deactivateSearch() }
|
|
621
|
+
guard !selectedWindowIds.isEmpty else {
|
|
622
|
+
flash("Select 2+ windows to organize")
|
|
623
|
+
return true
|
|
624
|
+
}
|
|
625
|
+
guard selectedWindowIds.count > 1 else {
|
|
626
|
+
flash("Add another window, then press D to organize")
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
distributeSelected()
|
|
630
|
+
return true
|
|
631
|
+
|
|
579
632
|
case 46: // m → screen map editor (standalone window)
|
|
580
633
|
if isSearching { deactivateSearch() }
|
|
581
634
|
ScreenMapWindowController.shared.show()
|
|
@@ -1050,6 +1103,54 @@ final class CommandModeState: ObservableObject {
|
|
|
1050
1103
|
}
|
|
1051
1104
|
}
|
|
1052
1105
|
|
|
1106
|
+
private func configureLaunchMode() {
|
|
1107
|
+
switch launchMode {
|
|
1108
|
+
case .normal:
|
|
1109
|
+
return
|
|
1110
|
+
case .organize(let appName):
|
|
1111
|
+
activePreset = .currentSpace
|
|
1112
|
+
seedSelectionForOrganization(appName: appName)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private func seedSelectionForOrganization(appName: String?) {
|
|
1117
|
+
let visibleWindows = flatWindowList.filter(\.isOnScreen)
|
|
1118
|
+
let targetApp = appName ?? visibleWindows.first?.appName
|
|
1119
|
+
let initialSelection = visibleWindows.filter { window in
|
|
1120
|
+
guard let name = window.appName, let targetApp else { return false }
|
|
1121
|
+
return name.localizedCaseInsensitiveCompare(targetApp) == .orderedSame
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
if initialSelection.isEmpty {
|
|
1125
|
+
flash("Select windows to organize. Cmd-click adds or removes windows; D distributes.")
|
|
1126
|
+
return
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
selectedWindowIds = Set(initialSelection.map(\.id))
|
|
1130
|
+
cursorWindowId = initialSelection.first?.id
|
|
1131
|
+
|
|
1132
|
+
if let targetApp {
|
|
1133
|
+
if initialSelection.count > 1 {
|
|
1134
|
+
flash("Selected \(initialSelection.count) \(targetApp) windows. Press D to organize.")
|
|
1135
|
+
} else {
|
|
1136
|
+
flash("Selected the \(targetApp) window. Cmd-click more windows, then press D.")
|
|
1137
|
+
}
|
|
1138
|
+
} else if initialSelection.count > 1 {
|
|
1139
|
+
flash("Selected \(initialSelection.count) windows. Press D to organize.")
|
|
1140
|
+
} else {
|
|
1141
|
+
flash("Selected 1 window. Cmd-click more windows, then press D.")
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
DispatchQueue.main.async { [weak self] in
|
|
1145
|
+
guard let self = self else { return }
|
|
1146
|
+
if self.selectedWindowIds.count > 1 {
|
|
1147
|
+
self.highlightAllSelected()
|
|
1148
|
+
} else {
|
|
1149
|
+
self.highlightSelectedWindow()
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1053
1154
|
// MARK: - Inventory Builder
|
|
1054
1155
|
|
|
1055
1156
|
private func buildInventory() -> CommandModeInventory {
|
|
@@ -200,10 +200,17 @@ struct CommandModeView: View {
|
|
|
200
200
|
|
|
201
201
|
private var header: some View {
|
|
202
202
|
HStack {
|
|
203
|
-
Text(isDesktopInventory ? "DESKTOP INVENTORY" : "COMMAND MODE")
|
|
203
|
+
Text(isDesktopInventory ? (state.isOrganizeFlow ? "ORGANIZE WINDOWS" : "DESKTOP INVENTORY") : "COMMAND MODE")
|
|
204
204
|
.font(Typo.monoBold(11))
|
|
205
205
|
.foregroundColor(Palette.text)
|
|
206
206
|
|
|
207
|
+
if isDesktopInventory && state.isOrganizeFlow {
|
|
208
|
+
bannerBadge("Current Space", tone: .neutral)
|
|
209
|
+
if let appName = state.organizeSeedAppName, !appName.isEmpty {
|
|
210
|
+
bannerBadge(appName, tone: .accent)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
if isDesktopInventory {
|
|
208
215
|
Button(action: { state.copyInventoryToClipboard() }) {
|
|
209
216
|
HStack(spacing: 3) {
|
|
@@ -275,6 +282,11 @@ struct CommandModeView: View {
|
|
|
275
282
|
|
|
276
283
|
private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
|
|
277
284
|
VStack(spacing: 0) {
|
|
285
|
+
if state.isOrganizeFlow {
|
|
286
|
+
organizeBanner
|
|
287
|
+
divider
|
|
288
|
+
}
|
|
289
|
+
|
|
278
290
|
if state.isSearching {
|
|
279
291
|
searchBar
|
|
280
292
|
} else {
|
|
@@ -358,6 +370,33 @@ struct CommandModeView: View {
|
|
|
358
370
|
}
|
|
359
371
|
}
|
|
360
372
|
|
|
373
|
+
private var organizeBanner: some View {
|
|
374
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
375
|
+
HStack(alignment: .center, spacing: 8) {
|
|
376
|
+
Image(systemName: "rectangle.3.group")
|
|
377
|
+
.font(.system(size: 11, weight: .medium))
|
|
378
|
+
.foregroundColor(Palette.running)
|
|
379
|
+
Text(state.organizeSelectionSummary)
|
|
380
|
+
.font(Typo.monoBold(10))
|
|
381
|
+
.foregroundColor(Palette.text)
|
|
382
|
+
Spacer()
|
|
383
|
+
if state.selectedWindowIds.count > 1 {
|
|
384
|
+
bannerBadge("Ready", tone: .accent)
|
|
385
|
+
} else {
|
|
386
|
+
bannerBadge("Add More", tone: .neutral)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
Text(state.organizeGuidance)
|
|
391
|
+
.font(Typo.mono(10))
|
|
392
|
+
.foregroundColor(Palette.textDim)
|
|
393
|
+
.lineLimit(2)
|
|
394
|
+
}
|
|
395
|
+
.padding(.horizontal, 14)
|
|
396
|
+
.padding(.vertical, 8)
|
|
397
|
+
.background(Palette.running.opacity(0.06))
|
|
398
|
+
}
|
|
399
|
+
|
|
361
400
|
private var filterPillBar: some View {
|
|
362
401
|
HStack(spacing: 6) {
|
|
363
402
|
ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
|
|
@@ -835,9 +874,17 @@ struct CommandModeView: View {
|
|
|
835
874
|
if indented {
|
|
836
875
|
Spacer().frame(width: 8)
|
|
837
876
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
877
|
+
Group {
|
|
878
|
+
if isSelected {
|
|
879
|
+
Image(systemName: "checkmark.circle.fill")
|
|
880
|
+
.font(.system(size: 10, weight: .semibold))
|
|
881
|
+
.foregroundColor(Palette.running)
|
|
882
|
+
} else {
|
|
883
|
+
Text(isLattices ? "●" : "•")
|
|
884
|
+
.font(.system(size: 7))
|
|
885
|
+
.foregroundColor(isLattices ? Palette.running : Palette.textDim)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
841
888
|
if let app = appLabel {
|
|
842
889
|
Text(app)
|
|
843
890
|
.font(Typo.monoBold(10))
|
|
@@ -1242,6 +1289,9 @@ struct CommandModeView: View {
|
|
|
1242
1289
|
chordHint(key: "↩", label: "select & front")
|
|
1243
1290
|
chordHint(key: "⌘A", label: "select all")
|
|
1244
1291
|
chordHint(key: "⇧↑↓", label: "multi-select")
|
|
1292
|
+
if state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
|
|
1293
|
+
chordHint(key: "d", label: "organize")
|
|
1294
|
+
}
|
|
1245
1295
|
if !state.selectedWindowIds.isEmpty {
|
|
1246
1296
|
chordHint(key: "t", label: "tile")
|
|
1247
1297
|
}
|
|
@@ -1279,9 +1329,42 @@ struct CommandModeView: View {
|
|
|
1279
1329
|
.foregroundColor(Palette.running)
|
|
1280
1330
|
}
|
|
1281
1331
|
}
|
|
1332
|
+
} else if isDesktopInventory && state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
|
|
1333
|
+
HStack(spacing: 12) {
|
|
1334
|
+
chordHint(key: "d", label: "organize")
|
|
1335
|
+
chordHint(key: "⌘-click", label: "add/remove")
|
|
1336
|
+
chordHint(key: "⇧-click", label: "range")
|
|
1337
|
+
chordHint(key: "↩", label: "front")
|
|
1338
|
+
chordHint(key: "esc", label: "cancel")
|
|
1339
|
+
Spacer()
|
|
1340
|
+
Text("\(state.selectedWindowIds.count) selected")
|
|
1341
|
+
.font(Typo.mono(9))
|
|
1342
|
+
.foregroundColor(Palette.running)
|
|
1343
|
+
}
|
|
1344
|
+
} else if isDesktopInventory && state.isOrganizeFlow && !state.selectedWindowIds.isEmpty {
|
|
1345
|
+
HStack(spacing: 12) {
|
|
1346
|
+
chordHint(key: "⌘-click", label: "add more")
|
|
1347
|
+
chordHint(key: "d", label: "need 2+")
|
|
1348
|
+
chordHint(key: "↩", label: "front")
|
|
1349
|
+
chordHint(key: "esc", label: "cancel")
|
|
1350
|
+
Spacer()
|
|
1351
|
+
Text(state.organizeSelectionSummary)
|
|
1352
|
+
.font(Typo.mono(9))
|
|
1353
|
+
.foregroundColor(Palette.textDim)
|
|
1354
|
+
}
|
|
1355
|
+
} else if isDesktopInventory && state.isOrganizeFlow {
|
|
1356
|
+
HStack(spacing: 12) {
|
|
1357
|
+
chordHint(key: "click", label: "select")
|
|
1358
|
+
chordHint(key: "⌘-click", label: "add/remove")
|
|
1359
|
+
chordHint(key: "/", label: "search")
|
|
1360
|
+
chordHint(key: "esc", label: "cancel")
|
|
1361
|
+
Spacer()
|
|
1362
|
+
}
|
|
1282
1363
|
} else if isDesktopInventory && state.selectedWindowIds.count > 1 {
|
|
1283
1364
|
// Multi-selection active
|
|
1284
1365
|
HStack(spacing: 12) {
|
|
1366
|
+
chordHint(key: "s", label: "grid preview")
|
|
1367
|
+
chordHint(key: "d", label: "distribute")
|
|
1285
1368
|
chordHint(key: "s", label: "grid preview")
|
|
1286
1369
|
chordHint(key: "↩", label: "front")
|
|
1287
1370
|
chordHint(key: "t", label: "grid region")
|
|
@@ -1302,6 +1385,7 @@ struct CommandModeView: View {
|
|
|
1302
1385
|
} else if isDesktopInventory && !state.selectedWindowIds.isEmpty {
|
|
1303
1386
|
// Single selection active — browsing hints with direct shortcuts
|
|
1304
1387
|
HStack(spacing: 12) {
|
|
1388
|
+
chordHint(key: "d", label: "organize")
|
|
1305
1389
|
chordHint(key: "s", label: "show")
|
|
1306
1390
|
chordHint(key: "↩", label: "front")
|
|
1307
1391
|
chordHint(key: "f", label: "focus+close")
|
|
@@ -1367,6 +1451,31 @@ struct CommandModeView: View {
|
|
|
1367
1451
|
}
|
|
1368
1452
|
}
|
|
1369
1453
|
|
|
1454
|
+
private enum BannerTone {
|
|
1455
|
+
case neutral
|
|
1456
|
+
case accent
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
private func bannerBadge(_ text: String, tone: BannerTone) -> some View {
|
|
1460
|
+
let foreground = tone == .accent ? Palette.running : Palette.textDim
|
|
1461
|
+
let fill = tone == .accent ? Palette.running.opacity(0.10) : Palette.surface
|
|
1462
|
+
let stroke = tone == .accent ? Palette.running.opacity(0.30) : Palette.border
|
|
1463
|
+
|
|
1464
|
+
return Text(text)
|
|
1465
|
+
.font(Typo.mono(8))
|
|
1466
|
+
.foregroundColor(foreground)
|
|
1467
|
+
.padding(.horizontal, 6)
|
|
1468
|
+
.padding(.vertical, 3)
|
|
1469
|
+
.background(
|
|
1470
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1471
|
+
.fill(fill)
|
|
1472
|
+
.overlay(
|
|
1473
|
+
RoundedRectangle(cornerRadius: 8)
|
|
1474
|
+
.strokeBorder(stroke, lineWidth: 0.5)
|
|
1475
|
+
)
|
|
1476
|
+
)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1370
1479
|
private func actionButton(key: String, label: String, action: @escaping () -> Void) -> some View {
|
|
1371
1480
|
Button(action: action) {
|
|
1372
1481
|
HStack(spacing: 4) {
|
|
@@ -43,15 +43,19 @@ final class CommandModeWindow {
|
|
|
43
43
|
|
|
44
44
|
var isVisible: Bool { isOpen }
|
|
45
45
|
|
|
46
|
-
func toggle() {
|
|
46
|
+
func toggle(launchMode: CommandModeLaunchMode = .normal) {
|
|
47
47
|
if isOpen {
|
|
48
|
-
|
|
48
|
+
if launchMode == .normal {
|
|
49
|
+
dismiss()
|
|
50
|
+
} else {
|
|
51
|
+
show(launchMode: launchMode)
|
|
52
|
+
}
|
|
49
53
|
} else {
|
|
50
|
-
show()
|
|
54
|
+
show(launchMode: launchMode)
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
func show() {
|
|
58
|
+
func show(launchMode: CommandModeLaunchMode = .normal) {
|
|
55
59
|
// Always rebuild for fresh state
|
|
56
60
|
dismiss()
|
|
57
61
|
isOpen = true
|
|
@@ -61,7 +65,7 @@ final class CommandModeWindow {
|
|
|
61
65
|
CommandPaletteWindow.shared.dismiss()
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
let state = CommandModeState()
|
|
68
|
+
let state = CommandModeState(launchMode: launchMode)
|
|
65
69
|
state.onDismiss = { [weak self] in
|
|
66
70
|
self?.dismiss()
|
|
67
71
|
}
|
|
@@ -255,6 +255,7 @@ struct CheatSheetView: View {
|
|
|
255
255
|
shortcutRow(action: .tileCenter)
|
|
256
256
|
shortcutRow(action: .tileDistribute)
|
|
257
257
|
shortcutRow(action: .tileTypeGrid)
|
|
258
|
+
shortcutRow(action: .tileOrganize)
|
|
258
259
|
|
|
259
260
|
// Hovered shortcut detail
|
|
260
261
|
if let hovered = hoveredAction, let binding = hotkeyStore.bindings[hovered] {
|
|
@@ -2035,7 +2035,6 @@ final class ScreenMapController: ObservableObject {
|
|
|
2035
2035
|
|
|
2036
2036
|
func handleKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
|
|
2037
2037
|
let diag = DiagnosticLog.shared
|
|
2038
|
-
diag.info("[ScreenMap] key: \(keyCode)")
|
|
2039
2038
|
|
|
2040
2039
|
// Tiling mode intercepts keys before anything else
|
|
2041
2040
|
if editor?.isTilingMode == true {
|
|
@@ -1805,6 +1805,7 @@ struct ScreenMapView: View {
|
|
|
1805
1805
|
let displays = editor?.displays ?? []
|
|
1806
1806
|
let zoomLevel = editor?.zoomLevel ?? 1.0
|
|
1807
1807
|
let panOffset = editor?.panOffset ?? .zero
|
|
1808
|
+
let canvasShape = RoundedRectangle(cornerRadius: 6, style: .continuous)
|
|
1808
1809
|
|
|
1809
1810
|
return GeometryReader { geo in
|
|
1810
1811
|
let metrics = CanvasMetrics(editor: editor, displays: displays, viewportSize: geo.size)
|
|
@@ -1852,12 +1853,14 @@ struct ScreenMapView: View {
|
|
|
1852
1853
|
}
|
|
1853
1854
|
.padding(8)
|
|
1854
1855
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
1856
|
+
.contentShape(canvasShape)
|
|
1857
|
+
.clipShape(canvasShape)
|
|
1855
1858
|
.clipped()
|
|
1856
1859
|
.background(
|
|
1857
1860
|
ZStack {
|
|
1858
|
-
|
|
1861
|
+
canvasShape
|
|
1859
1862
|
.fill(Color.black.opacity(0.25))
|
|
1860
|
-
|
|
1863
|
+
canvasShape
|
|
1861
1864
|
.strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
|
|
1862
1865
|
Canvas { context, size in
|
|
1863
1866
|
let spacing: CGFloat = 20
|
|
@@ -2742,11 +2745,14 @@ struct ScreenMapView: View {
|
|
|
2742
2745
|
}
|
|
2743
2746
|
// Track space key for canvas drag-to-pan
|
|
2744
2747
|
if event.keyCode == 49 && !controller.isSearchActive {
|
|
2745
|
-
if event.type == .keyDown
|
|
2746
|
-
isSpaceHeld
|
|
2747
|
-
|
|
2748
|
+
if event.type == .keyDown {
|
|
2749
|
+
if !isSpaceHeld {
|
|
2750
|
+
isSpaceHeld = true
|
|
2751
|
+
NSCursor.openHand.push()
|
|
2752
|
+
}
|
|
2748
2753
|
return nil
|
|
2749
2754
|
} else if event.type == .keyUp {
|
|
2755
|
+
guard isSpaceHeld else { return nil }
|
|
2750
2756
|
isSpaceHeld = false
|
|
2751
2757
|
spaceDragStart = nil
|
|
2752
2758
|
NSCursor.pop()
|
|
@@ -2788,16 +2794,18 @@ struct ScreenMapView: View {
|
|
|
2788
2794
|
mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { event in
|
|
2789
2795
|
guard let eventWindow = event.window,
|
|
2790
2796
|
eventWindow === ScreenMapWindowController.shared.nsWindow else { return event }
|
|
2797
|
+
let flippedPt = flippedScreenPoint(event)
|
|
2791
2798
|
|
|
2792
2799
|
// Space+click → begin canvas pan
|
|
2793
|
-
if isSpaceHeld,
|
|
2800
|
+
if isSpaceHeld,
|
|
2801
|
+
isCanvasPoint(flippedPt),
|
|
2802
|
+
let editor = controller.editor {
|
|
2794
2803
|
spaceDragStart = event.locationInWindow
|
|
2795
2804
|
spaceDragPanStart = editor.panOffset
|
|
2796
2805
|
NSCursor.closedHand.push()
|
|
2797
2806
|
return nil
|
|
2798
2807
|
}
|
|
2799
2808
|
|
|
2800
|
-
let flippedPt = flippedScreenPoint(event)
|
|
2801
2809
|
if let editor = controller.editor,
|
|
2802
2810
|
let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor),
|
|
2803
2811
|
hoveredWindowId == hit.id {
|
|
@@ -3042,6 +3050,7 @@ struct ScreenMapView: View {
|
|
|
3042
3050
|
// MARK: - Hit Test / Coordinate Conversion
|
|
3043
3051
|
|
|
3044
3052
|
private func canvasHit(flippedScreenPt: CGPoint, editor: ScreenMapEditorState) -> CanvasHit? {
|
|
3053
|
+
guard isCanvasPoint(flippedScreenPt) else { return nil }
|
|
3045
3054
|
let projection = CanvasProjection(editor: editor)
|
|
3046
3055
|
guard projection.scale > 0 else { return nil }
|
|
3047
3056
|
let canvasLocal = CGPoint(
|
|
@@ -3059,6 +3068,10 @@ struct ScreenMapView: View {
|
|
|
3059
3068
|
return nil
|
|
3060
3069
|
}
|
|
3061
3070
|
|
|
3071
|
+
private func isCanvasPoint(_ point: CGPoint) -> Bool {
|
|
3072
|
+
CGRect(origin: screenMapCanvasOrigin, size: screenMapCanvasSize).contains(point)
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3062
3075
|
private func detectDragMode(mapPoint: CGPoint, windowMapRect: CGRect) -> CanvasDragMode {
|
|
3063
3076
|
let w = windowMapRect.width
|
|
3064
3077
|
let h = windowMapRect.height
|
|
@@ -66,12 +66,40 @@ struct GridFile: Codable {
|
|
|
66
66
|
let snapZones: SnapZonesConfig?
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
enum SnapModifierKey: String, Codable, Equatable {
|
|
69
|
+
enum SnapModifierKey: String, Codable, Equatable, CaseIterable, Identifiable {
|
|
70
70
|
case command
|
|
71
71
|
case option
|
|
72
72
|
case control
|
|
73
73
|
case shift
|
|
74
74
|
|
|
75
|
+
var id: String { rawValue }
|
|
76
|
+
|
|
77
|
+
var label: String {
|
|
78
|
+
switch self {
|
|
79
|
+
case .command:
|
|
80
|
+
return "Command"
|
|
81
|
+
case .option:
|
|
82
|
+
return "Option"
|
|
83
|
+
case .control:
|
|
84
|
+
return "Control"
|
|
85
|
+
case .shift:
|
|
86
|
+
return "Shift"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var shortLabel: String {
|
|
91
|
+
switch self {
|
|
92
|
+
case .command:
|
|
93
|
+
return "Cmd"
|
|
94
|
+
case .option:
|
|
95
|
+
return "Opt"
|
|
96
|
+
case .control:
|
|
97
|
+
return "Ctrl"
|
|
98
|
+
case .shift:
|
|
99
|
+
return "Shift"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
var eventFlags: NSEvent.ModifierFlags {
|
|
76
104
|
switch self {
|
|
77
105
|
case .command:
|
|
@@ -98,9 +126,6 @@ enum SnapModifierKey: String, Codable, Equatable {
|
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
var label: String {
|
|
102
|
-
rawValue.capitalized
|
|
103
|
-
}
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
enum SnapZoneTriggerSpec: Codable, Equatable {
|
|
@@ -459,6 +484,36 @@ class WorkspaceManager: ObservableObject {
|
|
|
459
484
|
self.snapZonesConfig = snapZones
|
|
460
485
|
}
|
|
461
486
|
|
|
487
|
+
func updateSnapModifier(_ modifier: SnapModifierKey) {
|
|
488
|
+
let updated = SnapZonesConfig(
|
|
489
|
+
enabled: snapZonesConfig.enabled,
|
|
490
|
+
modifier: modifier,
|
|
491
|
+
zoneOpacity: snapZonesConfig.zoneOpacity,
|
|
492
|
+
highlightOpacity: snapZonesConfig.highlightOpacity,
|
|
493
|
+
previewOpacity: snapZonesConfig.previewOpacity,
|
|
494
|
+
cornerRadius: snapZonesConfig.cornerRadius,
|
|
495
|
+
rules: snapZonesConfig.rules
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
do {
|
|
499
|
+
let url = URL(fileURLWithPath: snapZonesConfigPath)
|
|
500
|
+
try FileManager.default.createDirectory(
|
|
501
|
+
at: url.deletingLastPathComponent(),
|
|
502
|
+
withIntermediateDirectories: true
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
let encoder = JSONEncoder()
|
|
506
|
+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
507
|
+
let data = try encoder.encode(updated)
|
|
508
|
+
try data.write(to: url, options: .atomic)
|
|
509
|
+
|
|
510
|
+
loadGridConfig()
|
|
511
|
+
DiagnosticLog.shared.info("WorkspaceManager: updated snap modifier to \(modifier.rawValue)")
|
|
512
|
+
} catch {
|
|
513
|
+
DiagnosticLog.shared.error("WorkspaceManager: failed to write snap-zones.json — \(error.localizedDescription)")
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
462
517
|
/// Resolve a tile string to fractions: check user presets first, then built-in TilePosition
|
|
463
518
|
func resolveTileFractions(_ tile: String) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
|
|
464
519
|
resolvePlacement(tile)?.fractions
|
package/package.json
CHANGED