@lattices/cli 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/app/Info.plist +13 -2
- package/app/Lattices.app/Contents/Info.plist +13 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Sources/AppShell/App.swift +7 -1
- package/app/Sources/AppShell/AppDelegate.swift +64 -1
- package/app/Sources/AppShell/AppShellView.swift +10 -0
- package/app/Sources/AppShell/AppUpdater.swift +216 -4
- package/app/Sources/AppShell/CliActionLauncher.swift +2 -2
- package/app/Sources/AppShell/MainView.swift +1 -1
- package/app/Sources/AppShell/Preferences.swift +29 -1
- package/app/Sources/AppShell/SettingsView.swift +576 -61
- package/app/Sources/AppShell/SettingsWindow.swift +4 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +23 -7
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +35 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +1 -1
- package/app/Sources/Core/Input/KeyboardRemapConfig.swift +69 -0
- package/app/Sources/Core/Input/KeyboardRemapController.swift +184 -0
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +84 -0
- package/app/Sources/Core/Workspace/SessionManager.swift +1 -1
- package/app/Sources/Core/Workspace/WorkspaceManager.swift +3 -3
- package/bin/lattices-app.ts +11 -0
- package/bin/lattices-dev +11 -0
- package/bin/lattices.ts +57 -17
- package/docs/app.md +30 -2
- package/docs/companion-deck.md +29 -0
- package/docs/concepts.md +5 -5
- package/docs/config.md +34 -9
- package/docs/layers.md +1 -1
- package/docs/overview.md +1 -1
- package/docs/quickstart.md +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,11 +81,11 @@ Close your laptop, reboot, come back a week later — your editor, dev
|
|
|
81
81
|
server, and test runner are exactly where you left them.
|
|
82
82
|
|
|
83
83
|
```sh
|
|
84
|
-
cd my-project && lattices
|
|
84
|
+
cd my-project && lattices start
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
No config? It
|
|
88
|
-
|
|
87
|
+
No config? It opens a shell in the project and, when it can, starts your
|
|
88
|
+
detected dev command in a second pane.
|
|
89
89
|
|
|
90
90
|
### Configuration
|
|
91
91
|
|
|
@@ -95,7 +95,7 @@ Drop a `.lattices.json` in your project root:
|
|
|
95
95
|
{
|
|
96
96
|
"ensure": true,
|
|
97
97
|
"panes": [
|
|
98
|
-
{ "name": "
|
|
98
|
+
{ "name": "shell", "size": 60 },
|
|
99
99
|
{ "name": "server", "cmd": "pnpm dev" },
|
|
100
100
|
{ "name": "tests", "cmd": "pnpm test --watch" }
|
|
101
101
|
]
|
|
@@ -108,7 +108,7 @@ Drop a `.lattices.json` in your project root:
|
|
|
108
108
|
2 panes 3+ panes
|
|
109
109
|
|
|
110
110
|
┌──────────┬───────┐ ┌──────────┬───────┐
|
|
111
|
-
│
|
|
111
|
+
│ shell │server │ │ shell │server │
|
|
112
112
|
│ (60%) │(40%) │ │ (60%) ├───────┤
|
|
113
113
|
└──────────┴───────┘ │ │tests │
|
|
114
114
|
└──────────┴───────┘
|
|
@@ -193,7 +193,9 @@ desktop the same way you do.
|
|
|
193
193
|
## CLI
|
|
194
194
|
|
|
195
195
|
```
|
|
196
|
-
lattices
|
|
196
|
+
lattices Show workspace status and common commands
|
|
197
|
+
lattices start Create or reattach to current project session
|
|
198
|
+
lattices tmux Alias for lattices start
|
|
197
199
|
lattices init Generate .lattices.json
|
|
198
200
|
lattices ls List active sessions
|
|
199
201
|
lattices kill [name] Kill a session
|
package/app/Info.plist
CHANGED
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
<string>AppIcon</string>
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
|
+
<key>CFBundleURLTypes</key>
|
|
18
|
+
<array>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>CFBundleURLName</key>
|
|
21
|
+
<string>com.arach.lattices</string>
|
|
22
|
+
<key>CFBundleURLSchemes</key>
|
|
23
|
+
<array>
|
|
24
|
+
<string>lattices</string>
|
|
25
|
+
</array>
|
|
26
|
+
</dict>
|
|
27
|
+
</array>
|
|
17
28
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
29
|
+
<string>0.4.8</string>
|
|
19
30
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
31
|
+
<string>0.4.8</string>
|
|
21
32
|
<key>LSMinimumSystemVersion</key>
|
|
22
33
|
<string>13.0</string>
|
|
23
34
|
<key>LSUIElement</key>
|
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
<string>AppIcon</string>
|
|
15
15
|
<key>CFBundlePackageType</key>
|
|
16
16
|
<string>APPL</string>
|
|
17
|
+
<key>CFBundleURLTypes</key>
|
|
18
|
+
<array>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>CFBundleURLName</key>
|
|
21
|
+
<string>com.arach.lattices</string>
|
|
22
|
+
<key>CFBundleURLSchemes</key>
|
|
23
|
+
<array>
|
|
24
|
+
<string>lattices</string>
|
|
25
|
+
</array>
|
|
26
|
+
</dict>
|
|
27
|
+
</array>
|
|
17
28
|
<key>CFBundleVersion</key>
|
|
18
|
-
<string>0.4.
|
|
29
|
+
<string>0.4.9</string>
|
|
19
30
|
<key>CFBundleShortVersionString</key>
|
|
20
|
-
<string>0.4.
|
|
31
|
+
<string>0.4.9</string>
|
|
21
32
|
<key>LSMinimumSystemVersion</key>
|
|
22
33
|
<string>13.0</string>
|
|
23
34
|
<key>LSUIElement</key>
|
|
Binary file
|
|
@@ -5,7 +5,13 @@ struct LatticesApp: App {
|
|
|
5
5
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
|
|
6
6
|
|
|
7
7
|
var body: some Scene {
|
|
8
|
-
Settings {
|
|
8
|
+
Settings {
|
|
9
|
+
SettingsContentView(
|
|
10
|
+
prefs: Preferences.shared,
|
|
11
|
+
scanner: ProjectScanner.shared
|
|
12
|
+
)
|
|
13
|
+
.frame(width: 900, height: 640)
|
|
14
|
+
}
|
|
9
15
|
.commands {
|
|
10
16
|
CommandGroup(after: .appInfo) {
|
|
11
17
|
Button("Update Lattices…") {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AppKit
|
|
2
|
+
import Carbon
|
|
2
3
|
import SwiftUI
|
|
3
4
|
|
|
4
5
|
extension Notification.Name {
|
|
@@ -69,6 +70,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
69
70
|
Self.shared = self
|
|
70
71
|
NSApp.setActivationPolicy(.accessory)
|
|
71
72
|
NSApp.appearance = NSAppearance(named: .darkAqua)
|
|
73
|
+
registerDeepLinkHandler()
|
|
72
74
|
|
|
73
75
|
// --- Status item ---
|
|
74
76
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
|
@@ -124,6 +126,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
124
126
|
store.register(action: .omniSearch) { OmniSearchWindow.shared.toggle() }
|
|
125
127
|
WindowDragSnapController.shared.start()
|
|
126
128
|
MouseGestureController.shared.start()
|
|
129
|
+
KeyboardRemapController.shared.start()
|
|
127
130
|
|
|
128
131
|
// Session layer cycling
|
|
129
132
|
store.register(action: .layerNext) { SessionLayerStore.shared.cycleNext() }
|
|
@@ -183,10 +186,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
183
186
|
ProcessModel.shared.start()
|
|
184
187
|
LatticesApi.setup()
|
|
185
188
|
DaemonServer.shared.start()
|
|
186
|
-
|
|
189
|
+
if Preferences.shared.companionBridgeEnabled {
|
|
190
|
+
LatticesCompanionBridgeServer.shared.start()
|
|
191
|
+
} else {
|
|
192
|
+
diag.info("CompanionBridge: disabled by preference")
|
|
193
|
+
}
|
|
187
194
|
AgentPool.shared.start()
|
|
188
195
|
diag.finish(tBoot)
|
|
189
196
|
|
|
197
|
+
Task {
|
|
198
|
+
await AppUpdater.shared.checkIfNeeded()
|
|
199
|
+
}
|
|
200
|
+
|
|
190
201
|
// --diagnostics flag: auto-open diagnostics panel on launch
|
|
191
202
|
if CommandLine.arguments.contains("--diagnostics") {
|
|
192
203
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
@@ -203,6 +214,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
func applicationWillTerminate(_ notification: Notification) {
|
|
217
|
+
KeyboardRemapController.shared.stop()
|
|
206
218
|
LatticesCompanionBridgeServer.shared.stop()
|
|
207
219
|
DaemonServer.shared.stop()
|
|
208
220
|
}
|
|
@@ -333,6 +345,57 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
|
333
345
|
@objc private func menuSettings() { SettingsWindowController.shared.show() }
|
|
334
346
|
@objc private func menuQuit() { NSApp.terminate(nil) }
|
|
335
347
|
|
|
348
|
+
// MARK: - Deep Links
|
|
349
|
+
|
|
350
|
+
private func registerDeepLinkHandler() {
|
|
351
|
+
NSAppleEventManager.shared().setEventHandler(
|
|
352
|
+
self,
|
|
353
|
+
andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
|
|
354
|
+
forEventClass: AEEventClass(kInternetEventClass),
|
|
355
|
+
andEventID: AEEventID(kAEGetURL)
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@objc private func handleGetURLEvent(
|
|
360
|
+
_ event: NSAppleEventDescriptor,
|
|
361
|
+
withReplyEvent replyEvent: NSAppleEventDescriptor
|
|
362
|
+
) {
|
|
363
|
+
guard
|
|
364
|
+
let value = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
|
|
365
|
+
let url = URL(string: value)
|
|
366
|
+
else {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
handleDeepLink(url)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private func handleDeepLink(_ url: URL) {
|
|
373
|
+
guard url.scheme?.localizedCaseInsensitiveCompare("lattices") == .orderedSame else {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let host = url.host?.lowercased()
|
|
378
|
+
let action = url.pathComponents
|
|
379
|
+
.first { $0 != "/" }?
|
|
380
|
+
.lowercased()
|
|
381
|
+
|
|
382
|
+
guard host == "companion" else {
|
|
383
|
+
SettingsWindowController.shared.show()
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
switch action {
|
|
388
|
+
case "enable", "start":
|
|
389
|
+
Preferences.shared.companionBridgeEnabled = true
|
|
390
|
+
SettingsWindowController.shared.showCompanion()
|
|
391
|
+
case "disable", "stop":
|
|
392
|
+
Preferences.shared.companionBridgeEnabled = false
|
|
393
|
+
SettingsWindowController.shared.showCompanion()
|
|
394
|
+
default:
|
|
395
|
+
SettingsWindowController.shared.showCompanion()
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
336
399
|
private static func showWorkspaceInspector() {
|
|
337
400
|
guard let entry = DesktopModel.shared.frontmostWindow(),
|
|
338
401
|
entry.app != "Lattices" else {
|
|
@@ -8,6 +8,7 @@ enum AppPage: String, CaseIterable {
|
|
|
8
8
|
case desktopInventory
|
|
9
9
|
case pi
|
|
10
10
|
case settings
|
|
11
|
+
case companionSettings
|
|
11
12
|
case docs
|
|
12
13
|
|
|
13
14
|
var label: String {
|
|
@@ -17,6 +18,7 @@ enum AppPage: String, CaseIterable {
|
|
|
17
18
|
case .desktopInventory: return "Desktop Inventory"
|
|
18
19
|
case .pi: return "Pi"
|
|
19
20
|
case .settings: return "Settings"
|
|
21
|
+
case .companionSettings:return "Settings"
|
|
20
22
|
case .docs: return "Docs"
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -28,6 +30,7 @@ enum AppPage: String, CaseIterable {
|
|
|
28
30
|
case .desktopInventory: return "macwindow.on.rectangle"
|
|
29
31
|
case .pi: return "terminal"
|
|
30
32
|
case .settings: return "gearshape"
|
|
33
|
+
case .companionSettings:return "ipad.and.iphone"
|
|
31
34
|
case .docs: return "book"
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -129,6 +132,13 @@ struct AppShellView: View {
|
|
|
129
132
|
scanner: ProjectScanner.shared,
|
|
130
133
|
onBack: { windowController.activePage = .screenMap; controller.enter() }
|
|
131
134
|
)
|
|
135
|
+
case .companionSettings:
|
|
136
|
+
SettingsContentView(
|
|
137
|
+
page: .companionSettings,
|
|
138
|
+
prefs: Preferences.shared,
|
|
139
|
+
scanner: ProjectScanner.shared,
|
|
140
|
+
onBack: { windowController.activePage = .screenMap; controller.enter() }
|
|
141
|
+
)
|
|
132
142
|
case .docs:
|
|
133
143
|
SettingsContentView(
|
|
134
144
|
page: .docs,
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import Combine
|
|
3
3
|
import Foundation
|
|
4
|
+
import SwiftUI
|
|
5
|
+
|
|
6
|
+
struct LatticesUpdateInfo: Equatable {
|
|
7
|
+
let version: String
|
|
8
|
+
let downloadURL: URL
|
|
9
|
+
let releaseNotes: String
|
|
10
|
+
let publishedAt: Date
|
|
11
|
+
let htmlURL: URL
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
@MainActor
|
|
6
15
|
final class AppUpdater: ObservableObject {
|
|
@@ -8,6 +17,16 @@ final class AppUpdater: ObservableObject {
|
|
|
8
17
|
|
|
9
18
|
@Published private(set) var isUpdating = false
|
|
10
19
|
@Published private(set) var statusMessage: String?
|
|
20
|
+
@Published private(set) var availableUpdate: LatticesUpdateInfo?
|
|
21
|
+
@Published private(set) var isChecking = false
|
|
22
|
+
@Published private(set) var lastChecked: Date?
|
|
23
|
+
@Published private(set) var lastError: String?
|
|
24
|
+
|
|
25
|
+
@AppStorage("appUpdater.autoCheck") var autoCheckEnabled = true
|
|
26
|
+
@AppStorage("appUpdater.lastCheckTime") private var lastCheckTimeInterval: Double = 0
|
|
27
|
+
@AppStorage("appUpdater.skippedVersion") private var skippedVersion = ""
|
|
28
|
+
|
|
29
|
+
private let checkInterval: TimeInterval = 24 * 60 * 60
|
|
11
30
|
|
|
12
31
|
private init() {}
|
|
13
32
|
|
|
@@ -27,6 +46,62 @@ final class AppUpdater: ObservableObject {
|
|
|
27
46
|
return nil
|
|
28
47
|
}
|
|
29
48
|
|
|
49
|
+
func checkIfNeeded() async {
|
|
50
|
+
guard autoCheckEnabled else { return }
|
|
51
|
+
|
|
52
|
+
let now = Date()
|
|
53
|
+
let lastCheck = Date(timeIntervalSince1970: lastCheckTimeInterval)
|
|
54
|
+
if now.timeIntervalSince(lastCheck) < checkInterval { return }
|
|
55
|
+
|
|
56
|
+
await check()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func check() async {
|
|
60
|
+
guard !isChecking else { return }
|
|
61
|
+
|
|
62
|
+
isChecking = true
|
|
63
|
+
lastError = nil
|
|
64
|
+
|
|
65
|
+
defer {
|
|
66
|
+
isChecking = false
|
|
67
|
+
lastChecked = Date()
|
|
68
|
+
lastCheckTimeInterval = Date().timeIntervalSince1970
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
do {
|
|
72
|
+
let release = try await fetchLatestRelease()
|
|
73
|
+
guard let update = parseRelease(release), isNewerVersion(update.version) else {
|
|
74
|
+
availableUpdate = nil
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if update.version == skippedVersion {
|
|
79
|
+
availableUpdate = nil
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
availableUpdate = update
|
|
84
|
+
} catch UpdateCheckError.noRelease {
|
|
85
|
+
availableUpdate = nil
|
|
86
|
+
} catch {
|
|
87
|
+
lastError = error.localizedDescription
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func skipCurrentUpdate() {
|
|
92
|
+
guard let update = availableUpdate else { return }
|
|
93
|
+
skippedVersion = update.version
|
|
94
|
+
availableUpdate = nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func viewCurrentRelease() {
|
|
98
|
+
if let update = availableUpdate {
|
|
99
|
+
NSWorkspace.shared.open(update.htmlURL)
|
|
100
|
+
} else if let url = URL(string: "https://github.com/arach/lattices/releases/latest") {
|
|
101
|
+
NSWorkspace.shared.open(url)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
30
105
|
func promptForUpdate() {
|
|
31
106
|
guard canUpdate else {
|
|
32
107
|
presentAlert(
|
|
@@ -36,11 +111,50 @@ final class AppUpdater: ObservableObject {
|
|
|
36
111
|
return
|
|
37
112
|
}
|
|
38
113
|
|
|
114
|
+
guard availableUpdate != nil else {
|
|
115
|
+
Task {
|
|
116
|
+
await check()
|
|
117
|
+
if availableUpdate != nil {
|
|
118
|
+
presentUpdateConfirmation()
|
|
119
|
+
} else if let error = lastError {
|
|
120
|
+
presentAlert(
|
|
121
|
+
title: "Could Not Check for Updates",
|
|
122
|
+
message: error
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
presentAlert(
|
|
126
|
+
title: "Lattices Is Up to Date",
|
|
127
|
+
message: "You’re running \(currentVersion), which is the latest published release."
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
presentUpdateConfirmation()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func presentUpdateConfirmation() {
|
|
39
138
|
let alert = NSAlert()
|
|
40
139
|
alert.alertStyle = .informational
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
140
|
+
if let update = availableUpdate {
|
|
141
|
+
alert.messageText = "Update Lattices?"
|
|
142
|
+
alert.informativeText = """
|
|
143
|
+
Current version: \(currentVersion)
|
|
144
|
+
New version: \(update.version)
|
|
145
|
+
|
|
146
|
+
Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
|
|
147
|
+
"""
|
|
148
|
+
} else {
|
|
149
|
+
alert.messageText = "Check and update Lattices?"
|
|
150
|
+
alert.informativeText = """
|
|
151
|
+
Current version: \(currentVersion)
|
|
152
|
+
New version: latest published release
|
|
153
|
+
|
|
154
|
+
Lattices will download the signed release, quit briefly, replace the app, and relaunch when the update is ready.
|
|
155
|
+
"""
|
|
156
|
+
}
|
|
157
|
+
alert.addButton(withTitle: availableUpdate == nil ? "Check & Update" : "Update")
|
|
44
158
|
alert.addButton(withTitle: "Cancel")
|
|
45
159
|
|
|
46
160
|
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
|
@@ -72,7 +186,11 @@ final class AppUpdater: ObservableObject {
|
|
|
72
186
|
do {
|
|
73
187
|
try proc.run()
|
|
74
188
|
isUpdating = true
|
|
75
|
-
|
|
189
|
+
if let update = availableUpdate {
|
|
190
|
+
statusMessage = "Preparing Lattices \(update.version). The app will relaunch when the update is ready."
|
|
191
|
+
} else {
|
|
192
|
+
statusMessage = "Preparing the latest Lattices release. The app will relaunch when the update is ready."
|
|
193
|
+
}
|
|
76
194
|
} catch {
|
|
77
195
|
presentAlert(
|
|
78
196
|
title: "Update Failed",
|
|
@@ -89,4 +207,98 @@ final class AppUpdater: ObservableObject {
|
|
|
89
207
|
alert.addButton(withTitle: "OK")
|
|
90
208
|
alert.runModal()
|
|
91
209
|
}
|
|
210
|
+
|
|
211
|
+
private func fetchLatestRelease() async throws -> GitHubRelease {
|
|
212
|
+
let url = URL(string: "https://api.github.com/repos/arach/lattices/releases/latest")!
|
|
213
|
+
var request = URLRequest(url: url)
|
|
214
|
+
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
|
|
215
|
+
request.setValue("Lattices/\(currentVersion)", forHTTPHeaderField: "User-Agent")
|
|
216
|
+
|
|
217
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
218
|
+
guard let http = response as? HTTPURLResponse else {
|
|
219
|
+
throw UpdateCheckError.invalidResponse
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
switch http.statusCode {
|
|
223
|
+
case 200:
|
|
224
|
+
let decoder = JSONDecoder()
|
|
225
|
+
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
226
|
+
decoder.dateDecodingStrategy = .iso8601
|
|
227
|
+
return try decoder.decode(GitHubRelease.self, from: data)
|
|
228
|
+
case 404:
|
|
229
|
+
throw UpdateCheckError.noRelease
|
|
230
|
+
case 403:
|
|
231
|
+
throw UpdateCheckError.rateLimited
|
|
232
|
+
default:
|
|
233
|
+
throw UpdateCheckError.httpError(http.statusCode)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private func parseRelease(_ release: GitHubRelease) -> LatticesUpdateInfo? {
|
|
238
|
+
guard !release.draft, !release.prerelease else { return nil }
|
|
239
|
+
|
|
240
|
+
let asset = release.assets.first { asset in
|
|
241
|
+
asset.name == "Lattices.dmg" ||
|
|
242
|
+
(asset.name.hasPrefix("Lattices") && asset.name.hasSuffix(".dmg"))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
guard let asset,
|
|
246
|
+
let downloadURL = URL(string: asset.browserDownloadUrl),
|
|
247
|
+
let htmlURL = URL(string: release.htmlUrl) else {
|
|
248
|
+
return nil
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let version = release.tagName.hasPrefix("v")
|
|
252
|
+
? String(release.tagName.dropFirst())
|
|
253
|
+
: release.tagName
|
|
254
|
+
|
|
255
|
+
return LatticesUpdateInfo(
|
|
256
|
+
version: version,
|
|
257
|
+
downloadURL: downloadURL,
|
|
258
|
+
releaseNotes: release.body ?? "",
|
|
259
|
+
publishedAt: release.publishedAt,
|
|
260
|
+
htmlURL: htmlURL
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private func isNewerVersion(_ remoteVersion: String) -> Bool {
|
|
265
|
+
guard currentVersion != "unknown" else { return false }
|
|
266
|
+
return remoteVersion.compare(currentVersion, options: .numeric) == .orderedDescending
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private struct GitHubRelease: Decodable {
|
|
271
|
+
let tagName: String
|
|
272
|
+
let name: String
|
|
273
|
+
let body: String?
|
|
274
|
+
let htmlUrl: String
|
|
275
|
+
let publishedAt: Date
|
|
276
|
+
let assets: [GitHubAsset]
|
|
277
|
+
let prerelease: Bool
|
|
278
|
+
let draft: Bool
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private struct GitHubAsset: Decodable {
|
|
282
|
+
let name: String
|
|
283
|
+
let browserDownloadUrl: String
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private enum UpdateCheckError: LocalizedError {
|
|
287
|
+
case invalidResponse
|
|
288
|
+
case noRelease
|
|
289
|
+
case rateLimited
|
|
290
|
+
case httpError(Int)
|
|
291
|
+
|
|
292
|
+
var errorDescription: String? {
|
|
293
|
+
switch self {
|
|
294
|
+
case .invalidResponse:
|
|
295
|
+
return "Invalid response from GitHub."
|
|
296
|
+
case .noRelease:
|
|
297
|
+
return "No published release found."
|
|
298
|
+
case .rateLimited:
|
|
299
|
+
return "GitHub rate limited the update check."
|
|
300
|
+
case .httpError(let code):
|
|
301
|
+
return "GitHub returned HTTP \(code)."
|
|
302
|
+
}
|
|
303
|
+
}
|
|
92
304
|
}
|
|
@@ -24,7 +24,7 @@ enum CliActionLauncher {
|
|
|
24
24
|
) else { return }
|
|
25
25
|
|
|
26
26
|
Preferences.shared.terminal.launch(
|
|
27
|
-
command: "lattices init && lattices",
|
|
27
|
+
command: "lattices init && lattices start",
|
|
28
28
|
in: directory
|
|
29
29
|
)
|
|
30
30
|
}
|
|
@@ -36,7 +36,7 @@ enum CliActionLauncher {
|
|
|
36
36
|
) else { return }
|
|
37
37
|
|
|
38
38
|
Preferences.shared.terminal.launch(
|
|
39
|
-
command: "lattices",
|
|
39
|
+
command: "lattices start",
|
|
40
40
|
in: directory
|
|
41
41
|
)
|
|
42
42
|
}
|
|
@@ -490,7 +490,7 @@ struct MainView: View {
|
|
|
490
490
|
.buttonStyle(.plain)
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
Text("Initialize runs lattices init && lattices in the folder you choose.")
|
|
493
|
+
Text("Initialize runs lattices init && lattices start in the folder you choose.")
|
|
494
494
|
.font(Typo.mono(9))
|
|
495
495
|
.foregroundColor(Palette.textMuted)
|
|
496
496
|
.multilineTextAlignment(.center)
|
|
@@ -10,6 +10,7 @@ class Preferences: ObservableObject {
|
|
|
10
10
|
static let shared = Preferences()
|
|
11
11
|
|
|
12
12
|
private enum CompanionDefaultsKey {
|
|
13
|
+
static let bridgeEnabled = "companion.bridge.enabled"
|
|
13
14
|
static let trackpadEnabled = "companion.trackpad.enabled"
|
|
14
15
|
static let cockpitLayout = "companion.cockpit.layout"
|
|
15
16
|
}
|
|
@@ -30,6 +31,17 @@ class Preferences: ObservableObject {
|
|
|
30
31
|
didSet { UserDefaults.standard.set(dragSnapEnabled, forKey: "windowSnap.enabled") }
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
@Published var companionBridgeEnabled: Bool {
|
|
35
|
+
didSet {
|
|
36
|
+
UserDefaults.standard.set(companionBridgeEnabled, forKey: CompanionDefaultsKey.bridgeEnabled)
|
|
37
|
+
if companionBridgeEnabled {
|
|
38
|
+
LatticesCompanionBridgeServer.shared.start()
|
|
39
|
+
} else {
|
|
40
|
+
LatticesCompanionBridgeServer.shared.stop()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
@Published var companionTrackpadEnabled: Bool {
|
|
34
46
|
didSet { UserDefaults.standard.set(companionTrackpadEnabled, forKey: CompanionDefaultsKey.trackpadEnabled) }
|
|
35
47
|
}
|
|
@@ -41,6 +53,10 @@ class Preferences: ObservableObject {
|
|
|
41
53
|
didSet { UserDefaults.standard.set(mouseGesturesEnabled, forKey: "mouseGestures.enabled") }
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
@Published var keyboardRemapsEnabled: Bool {
|
|
57
|
+
didSet { UserDefaults.standard.set(keyboardRemapsEnabled, forKey: "keyboardRemaps.enabled") }
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
// MARK: - AI / Claude
|
|
45
61
|
|
|
46
62
|
@Published var claudePath: String {
|
|
@@ -155,10 +171,16 @@ class Preferences: ObservableObject {
|
|
|
155
171
|
self.dragSnapEnabled = true
|
|
156
172
|
}
|
|
157
173
|
|
|
174
|
+
if UserDefaults.standard.object(forKey: CompanionDefaultsKey.bridgeEnabled) != nil {
|
|
175
|
+
self.companionBridgeEnabled = UserDefaults.standard.bool(forKey: CompanionDefaultsKey.bridgeEnabled)
|
|
176
|
+
} else {
|
|
177
|
+
self.companionBridgeEnabled = false
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
if UserDefaults.standard.object(forKey: CompanionDefaultsKey.trackpadEnabled) != nil {
|
|
159
181
|
self.companionTrackpadEnabled = UserDefaults.standard.bool(forKey: CompanionDefaultsKey.trackpadEnabled)
|
|
160
182
|
} else {
|
|
161
|
-
self.companionTrackpadEnabled =
|
|
183
|
+
self.companionTrackpadEnabled = false
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
self.companionCockpitLayout = Self.loadCompanionCockpitLayout()
|
|
@@ -167,6 +189,12 @@ class Preferences: ObservableObject {
|
|
|
167
189
|
} else {
|
|
168
190
|
self.mouseGesturesEnabled = false
|
|
169
191
|
}
|
|
192
|
+
|
|
193
|
+
if UserDefaults.standard.object(forKey: "keyboardRemaps.enabled") != nil {
|
|
194
|
+
self.keyboardRemapsEnabled = UserDefaults.standard.bool(forKey: "keyboardRemaps.enabled")
|
|
195
|
+
} else {
|
|
196
|
+
self.keyboardRemapsEnabled = true
|
|
197
|
+
}
|
|
170
198
|
// AI / Claude
|
|
171
199
|
self.claudePath = UserDefaults.standard.string(forKey: "claude.path") ?? ""
|
|
172
200
|
self.advisorModel = UserDefaults.standard.string(forKey: "claude.advisorModel") ?? "haiku"
|