@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,392 @@
|
|
|
1
|
+
# Component Extraction Roadmap
|
|
2
|
+
|
|
3
|
+
This note turns a codebase review into an incremental component plan for `lattices`.
|
|
4
|
+
|
|
5
|
+
The goal is not to rewrite the app around a new architecture in one move. The goal is to extract a few reusable primitives that make future features cheaper:
|
|
6
|
+
|
|
7
|
+
- a cmdcmd-style visual window or session switcher
|
|
8
|
+
- one shared action model across hotkeys, palette, voice, daemon, and companion
|
|
9
|
+
- less duplicated window lookup, space lookup, and preview logic
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
Three pressure points showed up repeatedly:
|
|
14
|
+
|
|
15
|
+
1. `WindowTiler.swift` acts like several libraries at once.
|
|
16
|
+
2. action definitions exist in multiple parallel forms.
|
|
17
|
+
3. overlay and panel shells are repeatedly rebuilt per surface.
|
|
18
|
+
|
|
19
|
+
The result is that new UX surfaces often have to re-solve the same problems:
|
|
20
|
+
|
|
21
|
+
- how to find a target window
|
|
22
|
+
- how to map a user intent to a canonical action
|
|
23
|
+
- how to show a floating interactive surface
|
|
24
|
+
- how to capture and render previews
|
|
25
|
+
|
|
26
|
+
## Current duplication seams
|
|
27
|
+
|
|
28
|
+
### 1. Window and session lookup
|
|
29
|
+
|
|
30
|
+
The same session-tagged window matching idea appears in multiple places:
|
|
31
|
+
|
|
32
|
+
- `DesktopModel.windowForSession(...)`
|
|
33
|
+
- tag parsing during desktop polling
|
|
34
|
+
- CG window lookup paths in `WindowTiler`
|
|
35
|
+
- AX window lookup paths in `WindowTiler`
|
|
36
|
+
|
|
37
|
+
This is the strongest candidate for a single reusable locator.
|
|
38
|
+
|
|
39
|
+
### 2. Space topology and window membership
|
|
40
|
+
|
|
41
|
+
Display-space maps, current-space discovery, and window-space membership are rebuilt in several flows instead of being queried from one read model.
|
|
42
|
+
|
|
43
|
+
This makes space-aware features harder than they need to be:
|
|
44
|
+
|
|
45
|
+
- move window to space
|
|
46
|
+
- present window on the current space
|
|
47
|
+
- show where a session already lives
|
|
48
|
+
- build a visual desktop map
|
|
49
|
+
|
|
50
|
+
### 3. Window presentation and motion
|
|
51
|
+
|
|
52
|
+
`tile`, `present`, `batchMoveAndRaiseWindows`, and related paths all contain their own versions of:
|
|
53
|
+
|
|
54
|
+
- resolve target
|
|
55
|
+
- move or resize
|
|
56
|
+
- raise
|
|
57
|
+
- activate app
|
|
58
|
+
- mark interaction
|
|
59
|
+
|
|
60
|
+
That sequencing should be owned by one operation layer.
|
|
61
|
+
|
|
62
|
+
### 4. Preview capture and preview rendering
|
|
63
|
+
|
|
64
|
+
The codebase already has useful preview pieces, but they live in separate pockets:
|
|
65
|
+
|
|
66
|
+
- `WindowPreviewStore` in `HUDRightBar.swift`
|
|
67
|
+
- preview placeholder and preview card variants in HUD
|
|
68
|
+
- separate preview capture in `ScreenMapState.swift`
|
|
69
|
+
|
|
70
|
+
This is a strong signal that preview should become its own reusable subsystem.
|
|
71
|
+
|
|
72
|
+
### 5. Action definitions
|
|
73
|
+
|
|
74
|
+
Action and intent metadata currently live in several places:
|
|
75
|
+
|
|
76
|
+
- `HotkeyStore.swift`
|
|
77
|
+
- `PaletteCommand.swift`
|
|
78
|
+
- `IntentEngine.swift`
|
|
79
|
+
- `Intents/LatticeIntent.swift`
|
|
80
|
+
- `LatticesApi.swift`
|
|
81
|
+
|
|
82
|
+
The sharpest duplication is that `IntentEngine.swift` and `Intents/LatticeIntent.swift` each define their own intent schema.
|
|
83
|
+
|
|
84
|
+
### 6. Overlay and panel shells
|
|
85
|
+
|
|
86
|
+
There is already a useful shared primitive for normal app windows in `AppWindowShell.swift`, but overlay surfaces still rebuild similar shell code:
|
|
87
|
+
|
|
88
|
+
- `CommandPaletteWindow.swift`
|
|
89
|
+
- `OmniSearchWindow.swift`
|
|
90
|
+
- `VoiceCommandWindow.swift`
|
|
91
|
+
- `LauncherHUD.swift`
|
|
92
|
+
|
|
93
|
+
The repeated shell concerns are:
|
|
94
|
+
|
|
95
|
+
- `NSPanel` setup
|
|
96
|
+
- blur and rounded-mask container setup
|
|
97
|
+
- screen placement
|
|
98
|
+
- activation and dismissal behavior
|
|
99
|
+
- event monitor lifecycle
|
|
100
|
+
|
|
101
|
+
## Proposed reusable components
|
|
102
|
+
|
|
103
|
+
This is the target component map.
|
|
104
|
+
|
|
105
|
+
### Desktop substrate
|
|
106
|
+
|
|
107
|
+
#### `SessionWindowLocator`
|
|
108
|
+
|
|
109
|
+
Responsibility:
|
|
110
|
+
|
|
111
|
+
- resolve a lattices session, title tag, app target, or explicit window id into a canonical window target
|
|
112
|
+
- try fast cache lookup first
|
|
113
|
+
- fall back through CG and AX in one place
|
|
114
|
+
|
|
115
|
+
Why:
|
|
116
|
+
|
|
117
|
+
- removes repeated session-tag matching logic
|
|
118
|
+
- gives palette, daemon, voice, HUD, and future switchers the same targeting rules
|
|
119
|
+
|
|
120
|
+
#### `SpaceTopologySnapshot`
|
|
121
|
+
|
|
122
|
+
Responsibility:
|
|
123
|
+
|
|
124
|
+
- expose a single read model for displays, spaces, current space, and window-to-space membership
|
|
125
|
+
|
|
126
|
+
Why:
|
|
127
|
+
|
|
128
|
+
- prevents repeated recomputation of display-space facts
|
|
129
|
+
- makes space-aware UIs easier to build
|
|
130
|
+
|
|
131
|
+
#### `WindowPresenter`
|
|
132
|
+
|
|
133
|
+
Responsibility:
|
|
134
|
+
|
|
135
|
+
- own the canonical move, resize, raise, activate, and interaction-marking flow
|
|
136
|
+
- support both single-window and batched operations
|
|
137
|
+
|
|
138
|
+
Why:
|
|
139
|
+
|
|
140
|
+
- centralizes the side-effect sequence
|
|
141
|
+
- makes future planners and higher-level actions less fragile
|
|
142
|
+
|
|
143
|
+
#### `WindowPreviewProvider`
|
|
144
|
+
|
|
145
|
+
Responsibility:
|
|
146
|
+
|
|
147
|
+
- capture, cache, and serve still previews or live previews for windows
|
|
148
|
+
- separate capture policy from UI rendering
|
|
149
|
+
|
|
150
|
+
Why:
|
|
151
|
+
|
|
152
|
+
- avoids HUD and Screen Map each inventing preview behavior
|
|
153
|
+
- directly supports a visual selector or session fan-out
|
|
154
|
+
|
|
155
|
+
### Action substrate
|
|
156
|
+
|
|
157
|
+
#### `ActionRegistry`
|
|
158
|
+
|
|
159
|
+
Responsibility:
|
|
160
|
+
|
|
161
|
+
- define canonical verbs once
|
|
162
|
+
- own parameter metadata, user-facing labels, phrase templates, and execution hooks
|
|
163
|
+
|
|
164
|
+
Minimal shape:
|
|
165
|
+
|
|
166
|
+
```swift
|
|
167
|
+
enum ActionID: String {
|
|
168
|
+
case openPalette
|
|
169
|
+
case openSearch
|
|
170
|
+
case focusWindow
|
|
171
|
+
case placeWindow
|
|
172
|
+
case launchProject
|
|
173
|
+
case switchLayer
|
|
174
|
+
case killSession
|
|
175
|
+
case refreshProjects
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
struct ActionParam {
|
|
179
|
+
let name: String
|
|
180
|
+
let type: ActionParamType
|
|
181
|
+
let required: Bool
|
|
182
|
+
let values: [String]?
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
struct ActionDef {
|
|
186
|
+
let id: ActionID
|
|
187
|
+
let title: String
|
|
188
|
+
let params: [ActionParam]
|
|
189
|
+
let hotkey: HotkeyMeta?
|
|
190
|
+
let palette: PaletteMeta?
|
|
191
|
+
let phrases: [String]
|
|
192
|
+
let run: (ActionContext) throws -> JSON
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Why:
|
|
197
|
+
|
|
198
|
+
- one action identity across hotkeys, palette, voice, daemon, and companion
|
|
199
|
+
- palette rows become runtime bindings of a verb to a target, not bespoke actions
|
|
200
|
+
|
|
201
|
+
#### `ActionContext`
|
|
202
|
+
|
|
203
|
+
Responsibility:
|
|
204
|
+
|
|
205
|
+
- carry structured arguments plus source information like `hotkey`, `palette`, `voice-local`, `daemon`, or `companion`
|
|
206
|
+
|
|
207
|
+
Why:
|
|
208
|
+
|
|
209
|
+
- makes execution and logging more consistent
|
|
210
|
+
|
|
211
|
+
### Overlay substrate
|
|
212
|
+
|
|
213
|
+
#### `OverlayPanelShell`
|
|
214
|
+
|
|
215
|
+
Responsibility:
|
|
216
|
+
|
|
217
|
+
- build a reusable floating `NSPanel` shell from configuration
|
|
218
|
+
- own blur or plain background, corner radius, window level, collection behavior, and hosting setup
|
|
219
|
+
|
|
220
|
+
Why:
|
|
221
|
+
|
|
222
|
+
- extracts the shared Spotlight-style panel construction path
|
|
223
|
+
|
|
224
|
+
#### `OverlayPlacement`
|
|
225
|
+
|
|
226
|
+
Responsibility:
|
|
227
|
+
|
|
228
|
+
- centralize placement policies like centered, spotlight offset, top-center, or mouse-screen placement
|
|
229
|
+
|
|
230
|
+
Why:
|
|
231
|
+
|
|
232
|
+
- removes repeated `visibleFrame` math
|
|
233
|
+
|
|
234
|
+
#### `OverlayLifecycleController`
|
|
235
|
+
|
|
236
|
+
Responsibility:
|
|
237
|
+
|
|
238
|
+
- own local event monitors, Escape dismissal, deactivate behavior, and cleanup
|
|
239
|
+
|
|
240
|
+
Why:
|
|
241
|
+
|
|
242
|
+
- reduces panel-specific lifecycle glue
|
|
243
|
+
|
|
244
|
+
### UI primitives
|
|
245
|
+
|
|
246
|
+
#### `WindowPreviewCard`
|
|
247
|
+
|
|
248
|
+
Responsibility:
|
|
249
|
+
|
|
250
|
+
- render a window preview, loading state, and unavailable state consistently
|
|
251
|
+
|
|
252
|
+
Why:
|
|
253
|
+
|
|
254
|
+
- low-risk first UI extraction
|
|
255
|
+
- immediately reduces duplicated HUD preview rendering
|
|
256
|
+
|
|
257
|
+
## Recommended extraction order
|
|
258
|
+
|
|
259
|
+
The sequence below favors leverage without taking unnecessary risk.
|
|
260
|
+
|
|
261
|
+
### Slice 1: `WindowPreviewCard`
|
|
262
|
+
|
|
263
|
+
Extract the repeated preview body and placeholder logic from HUD into a shared SwiftUI component.
|
|
264
|
+
|
|
265
|
+
Why first:
|
|
266
|
+
|
|
267
|
+
- UI-only
|
|
268
|
+
- already duplicated
|
|
269
|
+
- does not disturb CGS, AX, or window mutation paths
|
|
270
|
+
|
|
271
|
+
### Slice 2: `OverlayPanelShell`
|
|
272
|
+
|
|
273
|
+
Extract the shared panel-construction path from `CommandPaletteWindow` and `OmniSearchWindow`.
|
|
274
|
+
|
|
275
|
+
Why second:
|
|
276
|
+
|
|
277
|
+
- those two surfaces are the cleanest near-duplicates
|
|
278
|
+
- builds a reusable shell for a future visual selector
|
|
279
|
+
|
|
280
|
+
### Slice 3: unify intent schema
|
|
281
|
+
|
|
282
|
+
Remove the parallel intent-definition structures by expanding or reusing the types in `Intents/LatticeIntent.swift` and pointing them at existing execution handlers.
|
|
283
|
+
|
|
284
|
+
Why third:
|
|
285
|
+
|
|
286
|
+
- high leverage
|
|
287
|
+
- removes one entire duplicate definition system
|
|
288
|
+
- proves the registry shape before migrating hotkeys or palette
|
|
289
|
+
|
|
290
|
+
### Slice 4: `SessionWindowLocator`
|
|
291
|
+
|
|
292
|
+
Centralize session-tagged lookup across DesktopModel and WindowTiler.
|
|
293
|
+
|
|
294
|
+
Why fourth:
|
|
295
|
+
|
|
296
|
+
- strongest desktop duplication seam
|
|
297
|
+
- unlocks cleaner action execution and better future switcher targeting
|
|
298
|
+
|
|
299
|
+
### Slice 5: `SpaceTopologySnapshot`
|
|
300
|
+
|
|
301
|
+
Create one query layer for display and space topology.
|
|
302
|
+
|
|
303
|
+
Why fifth:
|
|
304
|
+
|
|
305
|
+
- stabilizes space-aware features before touching more motion logic
|
|
306
|
+
|
|
307
|
+
### Slice 6: `WindowPresenter`
|
|
308
|
+
|
|
309
|
+
Unify move, resize, raise, activate, and interaction-marking flows.
|
|
310
|
+
|
|
311
|
+
Why sixth:
|
|
312
|
+
|
|
313
|
+
- this is higher risk because it sits directly on side effects
|
|
314
|
+
- it is safer after lookup and topology are centralized
|
|
315
|
+
|
|
316
|
+
### Slice 7: `WindowPreviewProvider`
|
|
317
|
+
|
|
318
|
+
Lift preview capture and caching out of HUD-specific code and reconcile it with Screen Map preview capture.
|
|
319
|
+
|
|
320
|
+
Why seventh:
|
|
321
|
+
|
|
322
|
+
- more useful after overlay shell and preview card exist
|
|
323
|
+
- becomes the substrate for a visual window or session chooser
|
|
324
|
+
|
|
325
|
+
## Features this should unlock
|
|
326
|
+
|
|
327
|
+
Once the components above exist, the app can add new surfaces with much less bespoke code.
|
|
328
|
+
|
|
329
|
+
### cmdcmd-style visual switcher
|
|
330
|
+
|
|
331
|
+
Use:
|
|
332
|
+
|
|
333
|
+
- `OverlayPanelShell`
|
|
334
|
+
- `OverlayPlacement`
|
|
335
|
+
- `SessionWindowLocator`
|
|
336
|
+
- `WindowPreviewProvider`
|
|
337
|
+
- `WindowPresenter`
|
|
338
|
+
|
|
339
|
+
Possible behavior:
|
|
340
|
+
|
|
341
|
+
- fan out lattices sessions or all windows
|
|
342
|
+
- show live or cached previews
|
|
343
|
+
- focus, tile, move to space, or close from one surface
|
|
344
|
+
|
|
345
|
+
### Shared action surfaces
|
|
346
|
+
|
|
347
|
+
Use:
|
|
348
|
+
|
|
349
|
+
- `ActionRegistry`
|
|
350
|
+
- `ActionContext`
|
|
351
|
+
|
|
352
|
+
Possible behavior:
|
|
353
|
+
|
|
354
|
+
- define `placeWindow` once
|
|
355
|
+
- trigger it from voice, hotkey, palette, daemon, or companion
|
|
356
|
+
- keep labels and phrases aligned across surfaces
|
|
357
|
+
|
|
358
|
+
### Stronger planning and preview
|
|
359
|
+
|
|
360
|
+
Use:
|
|
361
|
+
|
|
362
|
+
- `ActionRegistry`
|
|
363
|
+
- `WindowPresenter`
|
|
364
|
+
- `SpaceTopologySnapshot`
|
|
365
|
+
|
|
366
|
+
Possible behavior:
|
|
367
|
+
|
|
368
|
+
- preview a multi-window action before applying it
|
|
369
|
+
- build transactional-feeling UI around batched movement
|
|
370
|
+
|
|
371
|
+
## Things not to do yet
|
|
372
|
+
|
|
373
|
+
- do not rewrite `WindowTiler.swift` in one shot
|
|
374
|
+
- do not migrate every overlay surface onto one abstraction immediately
|
|
375
|
+
- do not force the palette to become purely registry-generated before the action model is proven
|
|
376
|
+
|
|
377
|
+
The safer path is:
|
|
378
|
+
|
|
379
|
+
1. extract small reusable pieces
|
|
380
|
+
2. move one production surface onto them
|
|
381
|
+
3. verify behavior
|
|
382
|
+
4. repeat
|
|
383
|
+
|
|
384
|
+
## Summary
|
|
385
|
+
|
|
386
|
+
The strongest architectural opportunity here is not one big framework. It is three small substrates:
|
|
387
|
+
|
|
388
|
+
- desktop targeting and motion
|
|
389
|
+
- action definition and routing
|
|
390
|
+
- overlay panel construction
|
|
391
|
+
|
|
392
|
+
If those become reusable, `lattices` gets a much cleaner path to new features without making every new surface solve the same desktop and execution problems again.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattices/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Agentic window manager for macOS — programmable workspace, smart layouts, managed tmux sessions, and a 35+-method agent API",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lattices": "./bin/lattices.ts",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "bun --cwd docs-site dev",
|
|
31
|
+
"test:e2e": "node --experimental-strip-types --test test/e2e-daemon.test.mjs",
|
|
32
|
+
"test:e2e:voice": "node --experimental-strip-types test/eval-voice.js",
|
|
31
33
|
"typecheck": "tsc --noEmit",
|
|
32
34
|
"build:app-bundle": "bash ./bin/lattices-dev build",
|
|
33
35
|
"prepack": "bash ./bin/lattices-dev build"
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import SwiftUI
|
|
3
|
-
|
|
4
|
-
/// NSPanel subclass that accepts key events even without a titlebar
|
|
5
|
-
private class KeyablePanel: NSPanel {
|
|
6
|
-
override var canBecomeKey: Bool { true }
|
|
7
|
-
override var canBecomeMain: Bool { true }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
final class CommandPaletteWindow {
|
|
11
|
-
static let shared = CommandPaletteWindow()
|
|
12
|
-
|
|
13
|
-
private var panel: NSPanel?
|
|
14
|
-
private var scanner: ProjectScanner?
|
|
15
|
-
|
|
16
|
-
func configure(scanner: ProjectScanner) {
|
|
17
|
-
self.scanner = scanner
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
var isVisible: Bool { panel?.isVisible ?? false }
|
|
21
|
-
|
|
22
|
-
func toggle() {
|
|
23
|
-
if let p = panel, p.isVisible {
|
|
24
|
-
dismiss()
|
|
25
|
-
} else {
|
|
26
|
-
show()
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func show() {
|
|
31
|
-
// Always rebuild for fresh command state
|
|
32
|
-
dismiss()
|
|
33
|
-
|
|
34
|
-
guard let scanner = scanner else { return }
|
|
35
|
-
|
|
36
|
-
// Ensure projects are up to date (full scan if list is empty,
|
|
37
|
-
// e.g. palette opened via hotkey before main popover appeared)
|
|
38
|
-
if scanner.projects.isEmpty {
|
|
39
|
-
scanner.scan()
|
|
40
|
-
} else {
|
|
41
|
-
scanner.refreshStatus()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let commands = CommandBuilder.build(scanner: scanner)
|
|
45
|
-
let view = CommandPaletteView(commands: commands) { [weak self] in
|
|
46
|
-
self?.dismiss()
|
|
47
|
-
}
|
|
48
|
-
.preferredColorScheme(.dark)
|
|
49
|
-
|
|
50
|
-
let hosting = NSHostingView(rootView: view)
|
|
51
|
-
hosting.translatesAutoresizingMaskIntoConstraints = false
|
|
52
|
-
|
|
53
|
-
let panel = KeyablePanel(
|
|
54
|
-
contentRect: NSRect(x: 0, y: 0, width: 540, height: 440),
|
|
55
|
-
styleMask: [.nonactivatingPanel],
|
|
56
|
-
backing: .buffered,
|
|
57
|
-
defer: false
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
panel.isOpaque = false
|
|
61
|
-
panel.backgroundColor = .clear
|
|
62
|
-
panel.hasShadow = true
|
|
63
|
-
panel.level = .floating
|
|
64
|
-
panel.isMovableByWindowBackground = true
|
|
65
|
-
panel.hidesOnDeactivate = true
|
|
66
|
-
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
67
|
-
panel.isReleasedWhenClosed = false
|
|
68
|
-
|
|
69
|
-
// Use NSVisualEffectView as contentView with a maskImage to communicate
|
|
70
|
-
// the rounded shape to the window server (layer.cornerRadius only clips
|
|
71
|
-
// at the view level — the window backing store stays rectangular)
|
|
72
|
-
let cornerRadius: CGFloat = 14
|
|
73
|
-
|
|
74
|
-
let effectView = NSVisualEffectView()
|
|
75
|
-
effectView.blendingMode = .behindWindow
|
|
76
|
-
effectView.material = .popover
|
|
77
|
-
effectView.state = .active
|
|
78
|
-
effectView.wantsLayer = true
|
|
79
|
-
effectView.maskImage = Self.maskImage(cornerRadius: cornerRadius)
|
|
80
|
-
|
|
81
|
-
panel.contentView = effectView
|
|
82
|
-
|
|
83
|
-
effectView.addSubview(hosting)
|
|
84
|
-
NSLayoutConstraint.activate([
|
|
85
|
-
hosting.leadingAnchor.constraint(equalTo: effectView.leadingAnchor),
|
|
86
|
-
hosting.trailingAnchor.constraint(equalTo: effectView.trailingAnchor),
|
|
87
|
-
hosting.topAnchor.constraint(equalTo: effectView.topAnchor),
|
|
88
|
-
hosting.bottomAnchor.constraint(equalTo: effectView.bottomAnchor),
|
|
89
|
-
])
|
|
90
|
-
|
|
91
|
-
// Center horizontally, slightly above vertical center (Spotlight-style)
|
|
92
|
-
if let screen = NSScreen.main {
|
|
93
|
-
let screenFrame = screen.visibleFrame
|
|
94
|
-
let x = screenFrame.midX - 270
|
|
95
|
-
let y = screenFrame.midY - 220 + (screenFrame.height * 0.1)
|
|
96
|
-
panel.setFrameOrigin(NSPoint(x: x, y: y))
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
panel.makeKeyAndOrderFront(nil)
|
|
100
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
101
|
-
|
|
102
|
-
self.panel = panel
|
|
103
|
-
AppDelegate.updateActivationPolicy()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
func dismiss() {
|
|
107
|
-
panel?.orderOut(nil)
|
|
108
|
-
panel = nil
|
|
109
|
-
AppDelegate.updateActivationPolicy()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/// Stretchable mask image for rounded corners — capInsets preserve the
|
|
113
|
-
/// corner arcs while the center stretches to any window size
|
|
114
|
-
private static func maskImage(cornerRadius: CGFloat) -> NSImage {
|
|
115
|
-
let edgeLength = 2.0 * cornerRadius + 1.0
|
|
116
|
-
let maskImage = NSImage(
|
|
117
|
-
size: NSSize(width: edgeLength, height: edgeLength),
|
|
118
|
-
flipped: false
|
|
119
|
-
) { rect in
|
|
120
|
-
let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
|
|
121
|
-
NSColor.black.set()
|
|
122
|
-
path.fill()
|
|
123
|
-
return true
|
|
124
|
-
}
|
|
125
|
-
maskImage.capInsets = NSEdgeInsets(
|
|
126
|
-
top: cornerRadius,
|
|
127
|
-
left: cornerRadius,
|
|
128
|
-
bottom: cornerRadius,
|
|
129
|
-
right: cornerRadius
|
|
130
|
-
)
|
|
131
|
-
maskImage.resizingMode = .stretch
|
|
132
|
-
return maskImage
|
|
133
|
-
}
|
|
134
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import SwiftUI
|
|
3
|
-
|
|
4
|
-
private final class OmniSearchPanel: NSPanel {
|
|
5
|
-
override var canBecomeKey: Bool { true }
|
|
6
|
-
override var canBecomeMain: Bool { true }
|
|
7
|
-
|
|
8
|
-
override func sendEvent(_ event: NSEvent) {
|
|
9
|
-
if event.type == .leftMouseDown || event.type == .rightMouseDown {
|
|
10
|
-
if !NSApp.isActive {
|
|
11
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
12
|
-
}
|
|
13
|
-
if !isKeyWindow {
|
|
14
|
-
makeKey()
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
super.sendEvent(event)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private final class OmniSearchHostingView<Content: View>: NSHostingView<Content> {
|
|
22
|
-
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
|
|
23
|
-
override var focusRingType: NSFocusRingType { get { .none } set {} }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
final class OmniSearchWindow {
|
|
27
|
-
static let shared = OmniSearchWindow()
|
|
28
|
-
|
|
29
|
-
private var panel: NSPanel?
|
|
30
|
-
private var keyMonitor: Any?
|
|
31
|
-
private var state: OmniSearchState?
|
|
32
|
-
|
|
33
|
-
var isVisible: Bool { panel?.isVisible ?? false }
|
|
34
|
-
|
|
35
|
-
func toggle() {
|
|
36
|
-
if isVisible {
|
|
37
|
-
dismiss()
|
|
38
|
-
} else {
|
|
39
|
-
show()
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func show() {
|
|
44
|
-
if let p = panel, p.isVisible {
|
|
45
|
-
p.makeKeyAndOrderFront(nil)
|
|
46
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Fresh state each time
|
|
51
|
-
let searchState = OmniSearchState()
|
|
52
|
-
state = searchState
|
|
53
|
-
|
|
54
|
-
let view = OmniSearchView(state: searchState) { [weak self] in
|
|
55
|
-
self?.dismiss()
|
|
56
|
-
}
|
|
57
|
-
.preferredColorScheme(.dark)
|
|
58
|
-
|
|
59
|
-
let hosting = OmniSearchHostingView(rootView: view)
|
|
60
|
-
hosting.translatesAutoresizingMaskIntoConstraints = false
|
|
61
|
-
|
|
62
|
-
let p = OmniSearchPanel(
|
|
63
|
-
contentRect: NSRect(x: 0, y: 0, width: 520, height: 480),
|
|
64
|
-
styleMask: [.titled, .closable, .resizable, .utilityWindow, .nonactivatingPanel],
|
|
65
|
-
backing: .buffered,
|
|
66
|
-
defer: false
|
|
67
|
-
)
|
|
68
|
-
p.title = "Search"
|
|
69
|
-
p.titlebarAppearsTransparent = true
|
|
70
|
-
p.titleVisibility = .hidden
|
|
71
|
-
p.isMovableByWindowBackground = true
|
|
72
|
-
p.level = .floating
|
|
73
|
-
p.isOpaque = false
|
|
74
|
-
p.backgroundColor = NSColor(red: 0.11, green: 0.11, blue: 0.12, alpha: 1.0)
|
|
75
|
-
p.hasShadow = true
|
|
76
|
-
p.appearance = NSAppearance(named: .darkAqua)
|
|
77
|
-
p.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
78
|
-
p.minSize = NSSize(width: 400, height: 300)
|
|
79
|
-
p.maxSize = NSSize(width: 700, height: 700)
|
|
80
|
-
p.hidesOnDeactivate = false
|
|
81
|
-
p.becomesKeyOnlyIfNeeded = false
|
|
82
|
-
|
|
83
|
-
let effectView = NSVisualEffectView()
|
|
84
|
-
effectView.blendingMode = .behindWindow
|
|
85
|
-
effectView.material = .popover
|
|
86
|
-
effectView.state = .active
|
|
87
|
-
effectView.wantsLayer = true
|
|
88
|
-
effectView.maskImage = Self.maskImage(cornerRadius: 14)
|
|
89
|
-
p.contentView = effectView
|
|
90
|
-
|
|
91
|
-
effectView.addSubview(hosting)
|
|
92
|
-
NSLayoutConstraint.activate([
|
|
93
|
-
hosting.leadingAnchor.constraint(equalTo: effectView.leadingAnchor),
|
|
94
|
-
hosting.trailingAnchor.constraint(equalTo: effectView.trailingAnchor),
|
|
95
|
-
hosting.topAnchor.constraint(equalTo: effectView.topAnchor),
|
|
96
|
-
hosting.bottomAnchor.constraint(equalTo: effectView.bottomAnchor),
|
|
97
|
-
])
|
|
98
|
-
|
|
99
|
-
// Center on screen
|
|
100
|
-
if let screen = NSScreen.main {
|
|
101
|
-
let visibleFrame = screen.visibleFrame
|
|
102
|
-
let x = visibleFrame.midX - 260
|
|
103
|
-
let y = visibleFrame.midY + 60 // slightly above center
|
|
104
|
-
p.setFrameOrigin(NSPoint(x: x, y: y))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
p.makeKeyAndOrderFront(nil)
|
|
108
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
109
|
-
panel = p
|
|
110
|
-
|
|
111
|
-
// Key monitor: Escape → dismiss, arrow keys → navigate, Enter → activate
|
|
112
|
-
keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
113
|
-
guard self?.panel?.isKeyWindow == true else { return event }
|
|
114
|
-
|
|
115
|
-
switch event.keyCode {
|
|
116
|
-
case 53: // Escape
|
|
117
|
-
self?.dismiss()
|
|
118
|
-
return nil
|
|
119
|
-
case 125: // ↓
|
|
120
|
-
self?.state?.moveSelection(1)
|
|
121
|
-
return nil
|
|
122
|
-
case 126: // ↑
|
|
123
|
-
self?.state?.moveSelection(-1)
|
|
124
|
-
return nil
|
|
125
|
-
case 36: // Enter
|
|
126
|
-
self?.state?.activateSelected()
|
|
127
|
-
self?.dismiss()
|
|
128
|
-
return nil
|
|
129
|
-
default:
|
|
130
|
-
return event
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
func dismiss() {
|
|
136
|
-
panel?.orderOut(nil)
|
|
137
|
-
panel = nil
|
|
138
|
-
state = nil
|
|
139
|
-
if let monitor = keyMonitor {
|
|
140
|
-
NSEvent.removeMonitor(monitor)
|
|
141
|
-
keyMonitor = nil
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private static func maskImage(cornerRadius: CGFloat) -> NSImage {
|
|
146
|
-
let edgeLength = 2.0 * cornerRadius + 1.0
|
|
147
|
-
let maskImage = NSImage(
|
|
148
|
-
size: NSSize(width: edgeLength, height: edgeLength),
|
|
149
|
-
flipped: false
|
|
150
|
-
) { rect in
|
|
151
|
-
let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
|
|
152
|
-
NSColor.black.set()
|
|
153
|
-
path.fill()
|
|
154
|
-
return true
|
|
155
|
-
}
|
|
156
|
-
maskImage.capInsets = NSEdgeInsets(
|
|
157
|
-
top: cornerRadius,
|
|
158
|
-
left: cornerRadius,
|
|
159
|
-
bottom: cornerRadius,
|
|
160
|
-
right: cornerRadius
|
|
161
|
-
)
|
|
162
|
-
maskImage.resizingMode = .stretch
|
|
163
|
-
return maskImage
|
|
164
|
-
}
|
|
165
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|