poke-gate 0.1.8 → 0.2.0
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/.github/workflows/release.yml +53 -3
- package/README.md +48 -14
- package/bin/poke-gate.js +17 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/AboutView.swift +7 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/AccessibilityPermissionView.swift +58 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +354 -23
- package/clients/Poke macOS Gate/Poke macOS Gate/Info.plist +2 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/LogsView.swift +1 -1
- package/clients/Poke macOS Gate/Poke macOS Gate/MacVisualStyle.swift +89 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/PermissionRowView.swift +55 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/Poke_macOS_GateApp.swift +241 -91
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +131 -65
- package/clients/Poke macOS Gate/Poke macOS Gate/SetupView.swift +157 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +28 -8
- package/docs/cli.md +19 -0
- package/docs/getting-started.md +9 -6
- package/docs/index.md +23 -18
- package/docs/macos-app.md +39 -4
- package/docs/security.md +62 -18
- package/examples/agents/battery.30m.js +1 -1
- package/examples/agents/screentime.24h.js +5 -6
- package/package.json +3 -1
- package/src/agents.js +5 -8
- package/src/app.js +3 -3
- package/src/mcp-server.js +502 -27
- package/src/permission-service.js +128 -0
- package/test/mcp-server-access-policy.test.js +40 -0
- package/test/mcp-server-loop-guard.test.js +57 -0
- package/test/mcp-server-sandbox-command.test.js +18 -0
- package/test/permission-service.test.js +97 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
+
import ServiceManagement
|
|
2
3
|
|
|
3
4
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
4
5
|
var service: GateService?
|
|
@@ -17,13 +18,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
17
18
|
struct Poke_macOS_GateApp: App {
|
|
18
19
|
@StateObject private var service = GateService()
|
|
19
20
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
20
|
-
|
|
21
21
|
var body: some Scene {
|
|
22
22
|
MenuBarExtra {
|
|
23
23
|
PopoverContent(service: service)
|
|
24
24
|
.onAppear {
|
|
25
25
|
service.autoStartIfNeeded()
|
|
26
26
|
appDelegate.service = service
|
|
27
|
+
service.startPermissionPolling()
|
|
28
|
+
}
|
|
29
|
+
.onDisappear {
|
|
30
|
+
service.stopPermissionPolling()
|
|
27
31
|
}
|
|
28
32
|
} label: {
|
|
29
33
|
Image(systemName: menuBarIcon)
|
|
@@ -35,6 +39,11 @@ struct Poke_macOS_GateApp: App {
|
|
|
35
39
|
}
|
|
36
40
|
.defaultSize(width: 560, height: 400)
|
|
37
41
|
|
|
42
|
+
Window("Setup", id: "setup") {
|
|
43
|
+
SetupView(service: service)
|
|
44
|
+
}
|
|
45
|
+
.windowResizability(.contentSize)
|
|
46
|
+
|
|
38
47
|
Window("Settings", id: "settings") {
|
|
39
48
|
SettingsView(service: service)
|
|
40
49
|
}
|
|
@@ -51,6 +60,37 @@ struct Poke_macOS_GateApp: App {
|
|
|
51
60
|
.windowResizability(.contentSize)
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
private func checkLoginItemPrompt() {
|
|
64
|
+
let dismissed = UserDefaults.standard.bool(forKey: "loginItemPromptDismissed")
|
|
65
|
+
let alreadyEnabled = SMAppService.mainApp.status == .enabled
|
|
66
|
+
if dismissed || alreadyEnabled { return }
|
|
67
|
+
|
|
68
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
69
|
+
let alert = NSAlert()
|
|
70
|
+
alert.messageText = "Start on login?"
|
|
71
|
+
alert.informativeText = "Would you like Poke Gate to start automatically when you log in?"
|
|
72
|
+
alert.addButton(withTitle: "Enable")
|
|
73
|
+
alert.addButton(withTitle: "Not now")
|
|
74
|
+
alert.addButton(withTitle: "Don't ask again")
|
|
75
|
+
alert.alertStyle = .informational
|
|
76
|
+
|
|
77
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
78
|
+
let response = alert.runModal()
|
|
79
|
+
|
|
80
|
+
switch response {
|
|
81
|
+
case .alertFirstButtonReturn:
|
|
82
|
+
try? SMAppService.mainApp.register()
|
|
83
|
+
UserDefaults.standard.set(true, forKey: "loginItemPromptDismissed")
|
|
84
|
+
case .alertSecondButtonReturn:
|
|
85
|
+
break
|
|
86
|
+
case .alertThirdButtonReturn:
|
|
87
|
+
UserDefaults.standard.set(true, forKey: "loginItemPromptDismissed")
|
|
88
|
+
default:
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
private var menuBarIcon: String {
|
|
55
95
|
switch service.status {
|
|
56
96
|
case .connected: "door.left.hand.open"
|
|
@@ -64,56 +104,89 @@ struct Poke_macOS_GateApp: App {
|
|
|
64
104
|
struct PopoverContent: View {
|
|
65
105
|
@ObservedObject var service: GateService
|
|
66
106
|
@Environment(\.openWindow) private var openWindow
|
|
107
|
+
@State private var pendingFullMode = false
|
|
67
108
|
|
|
68
109
|
var body: some View {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
110
|
+
if !service.hasCompletedSetup {
|
|
111
|
+
SetupView(service: service)
|
|
112
|
+
} else {
|
|
113
|
+
VStack(spacing: 10) {
|
|
114
|
+
statusSection
|
|
115
|
+
recentActivitySection
|
|
116
|
+
accessModeSection
|
|
117
|
+
actionsSection
|
|
118
|
+
footerSection
|
|
119
|
+
}
|
|
120
|
+
.frame(width: 320)
|
|
121
|
+
.padding(10)
|
|
122
|
+
.onChange(of: service.hasSystemPermissionsGranted) { _, granted in
|
|
123
|
+
if granted && pendingFullMode {
|
|
124
|
+
pendingFullMode = false
|
|
125
|
+
service.setPermissionMode(.full)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
75
130
|
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
private var statusSection: some View {
|
|
132
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
133
|
+
HStack(spacing: 8) {
|
|
134
|
+
Circle()
|
|
135
|
+
.fill(statusColor)
|
|
136
|
+
.frame(width: 10, height: 10)
|
|
78
137
|
|
|
79
|
-
|
|
80
|
-
|
|
138
|
+
Text(statusText)
|
|
139
|
+
.font(.system(.body, weight: .medium))
|
|
81
140
|
|
|
82
|
-
|
|
83
|
-
Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
|
|
84
|
-
.font(.caption)
|
|
85
|
-
.foregroundStyle(.secondary)
|
|
86
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
87
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
88
|
-
} else if service.status == .starting {
|
|
89
|
-
Text("Establishing connection…")
|
|
90
|
-
.font(.caption)
|
|
91
|
-
.foregroundStyle(.secondary)
|
|
92
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
93
|
-
} else if service.status == .error {
|
|
94
|
-
Text("Check Logs for details.")
|
|
95
|
-
.font(.caption)
|
|
96
|
-
.foregroundStyle(.red.opacity(0.8))
|
|
97
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
98
|
-
}
|
|
141
|
+
Spacer()
|
|
99
142
|
}
|
|
100
|
-
.padding(12)
|
|
101
143
|
|
|
102
|
-
|
|
144
|
+
statusMessage
|
|
145
|
+
}
|
|
146
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
147
|
+
.padding(12)
|
|
148
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
149
|
+
}
|
|
103
150
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
151
|
+
@ViewBuilder
|
|
152
|
+
private var statusMessage: some View {
|
|
153
|
+
switch service.status {
|
|
154
|
+
case .connected:
|
|
155
|
+
Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
|
|
156
|
+
.font(.caption)
|
|
157
|
+
.foregroundStyle(.secondary)
|
|
158
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
159
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
160
|
+
case .starting:
|
|
161
|
+
Text("Establishing connection…")
|
|
162
|
+
.font(.caption)
|
|
163
|
+
.foregroundStyle(.secondary)
|
|
164
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
165
|
+
case .error:
|
|
166
|
+
Text("Check Logs for details.")
|
|
167
|
+
.font(.caption)
|
|
168
|
+
.foregroundStyle(.red.opacity(0.8))
|
|
169
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
170
|
+
case .disconnected, .stopped:
|
|
171
|
+
EmptyView()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private var recentActivitySection: some View {
|
|
176
|
+
VStack(alignment: .leading, spacing: 6) {
|
|
177
|
+
sectionTitle("Recent activity")
|
|
178
|
+
|
|
179
|
+
if service.terminalPreviews.isEmpty {
|
|
180
|
+
Text("No activity yet")
|
|
181
|
+
.font(.caption)
|
|
182
|
+
.foregroundStyle(.secondary)
|
|
183
|
+
} else {
|
|
184
|
+
ForEach(Array(service.terminalPreviews.suffix(4).enumerated()), id: \.element.id) { _, entry in
|
|
185
|
+
HStack(spacing: 6) {
|
|
186
|
+
Circle()
|
|
187
|
+
.fill(entry.exitCode == 0 ? Color.green : (entry.exitCode == nil ? Color.gray : Color.red))
|
|
188
|
+
.frame(width: 5, height: 5)
|
|
189
|
+
Text("$ \(entry.command)")
|
|
117
190
|
.font(.system(size: 9, design: .monospaced))
|
|
118
191
|
.foregroundStyle(.tertiary)
|
|
119
192
|
.lineLimit(1)
|
|
@@ -121,71 +194,117 @@ struct PopoverContent: View {
|
|
|
121
194
|
}
|
|
122
195
|
}
|
|
123
196
|
}
|
|
124
|
-
|
|
125
|
-
|
|
197
|
+
}
|
|
198
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
199
|
+
.padding(12)
|
|
200
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
201
|
+
}
|
|
126
202
|
|
|
127
|
-
|
|
203
|
+
private var accessModeSection: some View {
|
|
204
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
205
|
+
HStack {
|
|
206
|
+
sectionTitle("Access mode")
|
|
207
|
+
Spacer()
|
|
208
|
+
Text(service.permissionMode.title)
|
|
209
|
+
.font(.caption2)
|
|
210
|
+
.foregroundStyle(.secondary)
|
|
211
|
+
}
|
|
128
212
|
|
|
129
|
-
HStack(spacing:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
213
|
+
HStack(spacing: 6) {
|
|
214
|
+
ForEach(GateService.PermissionMode.allCases) { mode in
|
|
215
|
+
let isActive = service.permissionMode == mode || (mode == .full && pendingFullMode)
|
|
216
|
+
Button {
|
|
217
|
+
handleModeSelection(mode)
|
|
218
|
+
} label: {
|
|
219
|
+
Text(modeChipTitle(mode))
|
|
220
|
+
.font(.system(size: 10, weight: .semibold))
|
|
221
|
+
.foregroundStyle(isActive ? .white : .secondary)
|
|
222
|
+
.padding(.vertical, 5)
|
|
223
|
+
.padding(.horizontal, 8)
|
|
224
|
+
.background(isActive ? MacVisualStyle.chipActiveFill : MacVisualStyle.chipInactiveFill)
|
|
225
|
+
.clipShape(Capsule())
|
|
226
|
+
}
|
|
227
|
+
.buttonStyle(.plain)
|
|
133
228
|
}
|
|
229
|
+
}
|
|
134
230
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
231
|
+
if service.permissionMode == .full || pendingFullMode {
|
|
232
|
+
AccessibilityPermissionView(service: service)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
236
|
+
.padding(12)
|
|
237
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
238
|
+
}
|
|
139
239
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
240
|
+
private var actionsSection: some View {
|
|
241
|
+
HStack(spacing: 12) {
|
|
242
|
+
ActionButton(icon: "text.alignleft", label: "Logs") {
|
|
243
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
244
|
+
openWindow(id: "logs")
|
|
245
|
+
}
|
|
144
246
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else {
|
|
150
|
-
ActionButton(icon: "play.fill", label: "Start") {
|
|
151
|
-
service.start()
|
|
152
|
-
}
|
|
153
|
-
}
|
|
247
|
+
ActionButton(icon: "bolt.fill", label: "Agents") {
|
|
248
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
249
|
+
openWindow(id: "agents")
|
|
250
|
+
}
|
|
154
251
|
|
|
155
|
-
|
|
252
|
+
ActionButton(icon: "gearshape", label: "Settings") {
|
|
253
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
254
|
+
openWindow(id: "settings")
|
|
255
|
+
}
|
|
156
256
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
257
|
+
if service.status == .connected || service.status == .starting || service.status == .disconnected {
|
|
258
|
+
ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
|
|
259
|
+
service.restart()
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
ActionButton(icon: "play.fill", label: "Start") {
|
|
263
|
+
service.start()
|
|
162
264
|
}
|
|
163
265
|
}
|
|
164
|
-
.padding(12)
|
|
165
266
|
|
|
166
|
-
|
|
267
|
+
Spacer()
|
|
167
268
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
} label: {
|
|
173
|
-
Text("Poke Gate v0.0.8")
|
|
174
|
-
.font(.caption2)
|
|
175
|
-
.foregroundStyle(.tertiary)
|
|
269
|
+
ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
|
|
270
|
+
service.stop()
|
|
271
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
272
|
+
NSApp.terminate(nil)
|
|
176
273
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
.padding(12)
|
|
277
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
278
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
279
|
+
}
|
|
180
280
|
|
|
181
|
-
|
|
281
|
+
private var footerSection: some View {
|
|
282
|
+
HStack {
|
|
283
|
+
Button {
|
|
284
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
285
|
+
openWindow(id: "about")
|
|
286
|
+
} label: {
|
|
287
|
+
Text(appVersionText)
|
|
182
288
|
.font(.caption2)
|
|
183
|
-
.foregroundStyle(.
|
|
289
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
184
290
|
}
|
|
185
|
-
.
|
|
186
|
-
|
|
291
|
+
.buttonStyle(.plain)
|
|
292
|
+
|
|
293
|
+
Spacer()
|
|
294
|
+
|
|
295
|
+
Text("Not affiliated with Poke")
|
|
296
|
+
.font(.caption2)
|
|
297
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor.opacity(0.7))
|
|
187
298
|
}
|
|
188
|
-
.
|
|
299
|
+
.padding(.horizontal, 6)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func sectionTitle(_ text: String) -> some View {
|
|
303
|
+
Text(text)
|
|
304
|
+
.font(.caption2)
|
|
305
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
306
|
+
.textCase(.uppercase)
|
|
307
|
+
.tracking(0.5)
|
|
189
308
|
}
|
|
190
309
|
|
|
191
310
|
private var statusText: String {
|
|
@@ -210,6 +329,37 @@ struct PopoverContent: View {
|
|
|
210
329
|
case .stopped: .gray.opacity(0.5)
|
|
211
330
|
}
|
|
212
331
|
}
|
|
332
|
+
|
|
333
|
+
private func modeChipTitle(_ mode: GateService.PermissionMode) -> String {
|
|
334
|
+
switch mode {
|
|
335
|
+
case .full: return "Full"
|
|
336
|
+
case .limited: return "Limited"
|
|
337
|
+
case .sandbox: return "Sandbox"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func handleModeSelection(_ mode: GateService.PermissionMode) {
|
|
342
|
+
guard mode != service.permissionMode else { return }
|
|
343
|
+
|
|
344
|
+
if mode == .full && !service.hasSystemPermissionsGranted {
|
|
345
|
+
pendingFullMode = true
|
|
346
|
+
service.openSystemPermission(.accessibility)
|
|
347
|
+
} else {
|
|
348
|
+
pendingFullMode = false
|
|
349
|
+
service.setPermissionMode(mode)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private var appVersionText: String {
|
|
354
|
+
let short = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
|
|
355
|
+
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
|
|
356
|
+
|
|
357
|
+
if let build, !build.isEmpty, build != short {
|
|
358
|
+
return "Poke Gate v\(short) (\(build))"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return "Poke Gate v\(short)"
|
|
362
|
+
}
|
|
213
363
|
}
|
|
214
364
|
|
|
215
365
|
struct ActionButton: View {
|
|
@@ -1,101 +1,167 @@
|
|
|
1
1
|
import SwiftUI
|
|
2
|
+
import ServiceManagement
|
|
2
3
|
|
|
3
4
|
struct SettingsView: View {
|
|
4
5
|
@ObservedObject var service: GateService
|
|
5
6
|
@Environment(\.dismiss) private var dismiss
|
|
7
|
+
@State private var launchAtLogin = SMAppService.mainApp.status == .enabled
|
|
6
8
|
|
|
7
9
|
var body: some View {
|
|
8
10
|
VStack(alignment: .leading, spacing: 16) {
|
|
11
|
+
authenticationSection
|
|
12
|
+
accessModeSection
|
|
13
|
+
connectionSection
|
|
14
|
+
|
|
9
15
|
VStack(alignment: .leading, spacing: 8) {
|
|
10
|
-
Text("
|
|
16
|
+
Text("GENERAL")
|
|
11
17
|
.font(.caption2)
|
|
12
|
-
.foregroundStyle(.
|
|
18
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
13
19
|
.textCase(.uppercase)
|
|
14
20
|
.tracking(0.5)
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.fontWeight(.medium)
|
|
28
|
-
|
|
29
|
-
if service.hasPokeLoginCredentials {
|
|
30
|
-
Text("Your Poke session is active.")
|
|
31
|
-
.font(.caption)
|
|
32
|
-
.foregroundStyle(.secondary)
|
|
33
|
-
} else {
|
|
34
|
-
Text("Run this command in Terminal to sign in:")
|
|
35
|
-
.font(.caption)
|
|
36
|
-
.foregroundStyle(.secondary)
|
|
22
|
+
Toggle("Start Poke Gate on login", isOn: $launchAtLogin)
|
|
23
|
+
.font(.subheadline)
|
|
24
|
+
.onChange(of: launchAtLogin) { _, newValue in
|
|
25
|
+
do {
|
|
26
|
+
if newValue {
|
|
27
|
+
try SMAppService.mainApp.register()
|
|
28
|
+
} else {
|
|
29
|
+
try SMAppService.mainApp.unregister()
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
launchAtLogin = !newValue
|
|
37
33
|
}
|
|
38
34
|
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
HStack {
|
|
38
|
+
Spacer()
|
|
39
|
+
Button("Close") {
|
|
40
|
+
dismiss()
|
|
39
41
|
}
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
.controlSize(.large)
|
|
42
|
+
.keyboardShortcut(.cancelAction)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
.padding(20)
|
|
46
|
+
.frame(width: 430)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private var authenticationSection: some View {
|
|
50
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
51
|
+
sectionTitle("AUTHENTICATION")
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
HStack(spacing: 8) {
|
|
54
|
+
Image(systemName: service.hasPokeLoginCredentials ? "checkmark.shield.fill" : "shield.slash")
|
|
55
|
+
.foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
|
|
56
|
+
.font(.title3)
|
|
57
|
+
|
|
58
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
59
|
+
Text(service.hasPokeLoginCredentials ? "Signed in via Poke" : "Not signed in")
|
|
60
|
+
.font(.subheadline)
|
|
61
|
+
.fontWeight(.medium)
|
|
62
|
+
|
|
63
|
+
Text(service.hasPokeLoginCredentials ? "Your Poke session is active." : "Run this command in Terminal to sign in:")
|
|
54
64
|
.font(.caption)
|
|
55
65
|
.foregroundStyle(.secondary)
|
|
56
66
|
}
|
|
57
67
|
}
|
|
68
|
+
.padding(10)
|
|
69
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
70
|
+
.macPanelStyle(.neutral, cornerRadius: 8)
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
if !service.hasPokeLoginCredentials {
|
|
73
|
+
Button {
|
|
74
|
+
service.runPokeLogin()
|
|
75
|
+
} label: {
|
|
76
|
+
Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
|
|
77
|
+
}
|
|
78
|
+
.controlSize(.large)
|
|
79
|
+
|
|
80
|
+
Text("Opens a browser window to sign in.")
|
|
81
|
+
.font(.caption)
|
|
82
|
+
.foregroundStyle(.secondary)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.frame(width: 8, height: 8)
|
|
87
|
+
private var accessModeSection: some View {
|
|
88
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
89
|
+
sectionTitle("ACCESS MODE")
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
VStack(spacing: 8) {
|
|
92
|
+
ForEach(GateService.PermissionMode.allCases) { mode in
|
|
93
|
+
permissionModeRow(mode)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
if service.permissionMode == .full {
|
|
98
|
+
AccessibilityPermissionView(service: service)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
75
102
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
private func permissionModeRow(_ mode: GateService.PermissionMode) -> some View {
|
|
104
|
+
Button {
|
|
105
|
+
service.setPermissionMode(mode)
|
|
106
|
+
} label: {
|
|
107
|
+
HStack(alignment: .top, spacing: 10) {
|
|
108
|
+
Image(systemName: service.permissionMode == mode ? "checkmark.circle.fill" : "circle")
|
|
109
|
+
.font(.headline)
|
|
110
|
+
.foregroundStyle(service.permissionMode == mode ? .green : .secondary)
|
|
111
|
+
|
|
112
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
113
|
+
Text(mode.title)
|
|
114
|
+
.font(.subheadline)
|
|
115
|
+
.fontWeight(.medium)
|
|
116
|
+
.foregroundStyle(.primary)
|
|
117
|
+
|
|
118
|
+
Text(mode.subtitle)
|
|
119
|
+
.font(.caption)
|
|
120
|
+
.foregroundStyle(.secondary)
|
|
82
121
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.background(.quaternary.opacity(0.5))
|
|
86
|
-
.cornerRadius(8)
|
|
122
|
+
|
|
123
|
+
Spacer()
|
|
87
124
|
}
|
|
125
|
+
.padding(10)
|
|
126
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
127
|
+
.macPanelStyle(service.permissionMode == mode ? .selected : .neutral)
|
|
128
|
+
}
|
|
129
|
+
.buttonStyle(.plain)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private var connectionSection: some View {
|
|
133
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
134
|
+
sectionTitle("CONNECTION")
|
|
135
|
+
|
|
136
|
+
HStack(spacing: 8) {
|
|
137
|
+
Circle()
|
|
138
|
+
.fill(connectionColor)
|
|
139
|
+
.frame(width: 8, height: 8)
|
|
140
|
+
|
|
141
|
+
Text(service.status.rawValue)
|
|
142
|
+
.font(.subheadline)
|
|
88
143
|
|
|
89
|
-
HStack {
|
|
90
144
|
Spacer()
|
|
91
|
-
|
|
92
|
-
|
|
145
|
+
|
|
146
|
+
Button {
|
|
147
|
+
service.restart()
|
|
148
|
+
} label: {
|
|
149
|
+
Label("Reconnect", systemImage: "arrow.counterclockwise")
|
|
150
|
+
.font(.caption)
|
|
93
151
|
}
|
|
94
|
-
.keyboardShortcut(.cancelAction)
|
|
95
152
|
}
|
|
153
|
+
.padding(10)
|
|
154
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
155
|
+
.macPanelStyle(.neutral, cornerRadius: 8)
|
|
96
156
|
}
|
|
97
|
-
|
|
98
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private func sectionTitle(_ text: String) -> some View {
|
|
160
|
+
Text(text)
|
|
161
|
+
.font(.caption2)
|
|
162
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
163
|
+
.textCase(.uppercase)
|
|
164
|
+
.tracking(0.5)
|
|
99
165
|
}
|
|
100
166
|
|
|
101
167
|
private var connectionColor: Color {
|