poke-gate 0.1.9 → 0.2.1
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/Gate.app +0 -0
- 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 +389 -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 +234 -91
- package/clients/Poke macOS Gate/Poke macOS Gate/SettingsView.swift +125 -81
- package/clients/Poke macOS Gate/Poke macOS Gate/SetupView.swift +157 -0
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +31 -11
- 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/macOS +0 -0
- package/package.json +3 -1
- package/src/agents.js +5 -8
- package/src/app.js +29 -5
- 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
|
@@ -24,7 +24,10 @@ struct Poke_macOS_GateApp: App {
|
|
|
24
24
|
.onAppear {
|
|
25
25
|
service.autoStartIfNeeded()
|
|
26
26
|
appDelegate.service = service
|
|
27
|
-
|
|
27
|
+
service.startPermissionPolling()
|
|
28
|
+
}
|
|
29
|
+
.onDisappear {
|
|
30
|
+
service.stopPermissionPolling()
|
|
28
31
|
}
|
|
29
32
|
} label: {
|
|
30
33
|
Image(systemName: menuBarIcon)
|
|
@@ -36,6 +39,11 @@ struct Poke_macOS_GateApp: App {
|
|
|
36
39
|
}
|
|
37
40
|
.defaultSize(width: 560, height: 400)
|
|
38
41
|
|
|
42
|
+
Window("Setup", id: "setup") {
|
|
43
|
+
SetupView(service: service)
|
|
44
|
+
}
|
|
45
|
+
.windowResizability(.contentSize)
|
|
46
|
+
|
|
39
47
|
Window("Settings", id: "settings") {
|
|
40
48
|
SettingsView(service: service)
|
|
41
49
|
}
|
|
@@ -96,128 +104,232 @@ struct Poke_macOS_GateApp: App {
|
|
|
96
104
|
struct PopoverContent: View {
|
|
97
105
|
@ObservedObject var service: GateService
|
|
98
106
|
@Environment(\.openWindow) private var openWindow
|
|
107
|
+
@State private var pendingFullMode = false
|
|
99
108
|
|
|
100
109
|
var body: some View {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
130
|
|
|
108
|
-
|
|
109
|
-
|
|
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)
|
|
110
137
|
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
Text(statusText)
|
|
139
|
+
.font(.system(.body, weight: .medium))
|
|
113
140
|
|
|
114
|
-
|
|
115
|
-
Text("This machine is accessible via Poke. Ask your Poke to run commands or read files.")
|
|
116
|
-
.font(.caption)
|
|
117
|
-
.foregroundStyle(.secondary)
|
|
118
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
119
|
-
.fixedSize(horizontal: false, vertical: true)
|
|
120
|
-
} else if service.status == .starting {
|
|
121
|
-
Text("Establishing connection…")
|
|
122
|
-
.font(.caption)
|
|
123
|
-
.foregroundStyle(.secondary)
|
|
124
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
125
|
-
} else if service.status == .error {
|
|
126
|
-
Text("Check Logs for details.")
|
|
127
|
-
.font(.caption)
|
|
128
|
-
.foregroundStyle(.red.opacity(0.8))
|
|
129
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
130
|
-
}
|
|
141
|
+
Spacer()
|
|
131
142
|
}
|
|
132
|
-
.padding(12)
|
|
133
143
|
|
|
134
|
-
|
|
144
|
+
statusMessage
|
|
145
|
+
}
|
|
146
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
147
|
+
.padding(12)
|
|
148
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
149
|
+
}
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
ForEach(Array(service.terminalPreviews.suffix(4).enumerated()), id: \.element.id) { _, entry in
|
|
181
|
+
HStack(spacing: 6) {
|
|
182
|
+
Circle()
|
|
183
|
+
.fill(entry.exitCode == 0 ? Color.green : (entry.exitCode == nil ? Color.gray : Color.red))
|
|
184
|
+
.frame(width: 5, height: 5)
|
|
185
|
+
Text("$ \(entry.command)")
|
|
149
186
|
.font(.system(size: 9, design: .monospaced))
|
|
150
187
|
.foregroundStyle(.tertiary)
|
|
151
188
|
.lineLimit(1)
|
|
152
189
|
.truncationMode(.tail)
|
|
153
190
|
}
|
|
154
191
|
}
|
|
192
|
+
} else if !service.logs.isEmpty {
|
|
193
|
+
ForEach(Array(service.logs.suffix(4).enumerated()), id: \.offset) { _, log in
|
|
194
|
+
Text(log)
|
|
195
|
+
.font(.system(size: 9, design: .monospaced))
|
|
196
|
+
.foregroundStyle(.tertiary)
|
|
197
|
+
.lineLimit(1)
|
|
198
|
+
.truncationMode(.tail)
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
Text("No activity yet")
|
|
202
|
+
.font(.caption)
|
|
203
|
+
.foregroundStyle(.secondary)
|
|
155
204
|
}
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
}
|
|
206
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
207
|
+
.padding(12)
|
|
208
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
209
|
+
}
|
|
158
210
|
|
|
159
|
-
|
|
211
|
+
private var accessModeSection: some View {
|
|
212
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
213
|
+
HStack {
|
|
214
|
+
sectionTitle("Access mode")
|
|
215
|
+
Spacer()
|
|
216
|
+
Text(service.permissionMode.title)
|
|
217
|
+
.font(.caption2)
|
|
218
|
+
.foregroundStyle(.secondary)
|
|
219
|
+
}
|
|
160
220
|
|
|
161
|
-
HStack(spacing:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
221
|
+
HStack(spacing: 6) {
|
|
222
|
+
ForEach(GateService.PermissionMode.allCases) { mode in
|
|
223
|
+
let isActive = service.permissionMode == mode || (mode == .full && pendingFullMode)
|
|
224
|
+
Button {
|
|
225
|
+
handleModeSelection(mode)
|
|
226
|
+
} label: {
|
|
227
|
+
Text(modeChipTitle(mode))
|
|
228
|
+
.font(.system(size: 10, weight: .semibold))
|
|
229
|
+
.foregroundStyle(isActive ? .white : .secondary)
|
|
230
|
+
.padding(.vertical, 5)
|
|
231
|
+
.padding(.horizontal, 8)
|
|
232
|
+
.background(isActive ? MacVisualStyle.chipActiveFill : MacVisualStyle.chipInactiveFill)
|
|
233
|
+
.clipShape(Capsule())
|
|
234
|
+
}
|
|
235
|
+
.buttonStyle(.plain)
|
|
165
236
|
}
|
|
237
|
+
}
|
|
166
238
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
239
|
+
Text(activeModeDescription)
|
|
240
|
+
.font(.caption2)
|
|
241
|
+
.foregroundStyle(.secondary)
|
|
242
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
171
243
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
244
|
+
if service.permissionMode == .full || pendingFullMode {
|
|
245
|
+
AccessibilityPermissionView(service: service)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
249
|
+
.padding(12)
|
|
250
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
251
|
+
}
|
|
176
252
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
253
|
+
private var activeModeDescription: String {
|
|
254
|
+
let mode = pendingFullMode ? GateService.PermissionMode.full : service.permissionMode
|
|
255
|
+
switch mode {
|
|
256
|
+
case .full:
|
|
257
|
+
return "All tools enabled. Risky actions require chat approval."
|
|
258
|
+
case .limited:
|
|
259
|
+
return "Read-only tools and safe commands only (ls, cat, grep, curl…). File writes and screenshots are disabled."
|
|
260
|
+
case .sandbox:
|
|
261
|
+
return "Broader commands (brew, node, python, ffmpeg…) but file writes restricted to ~/Downloads and /tmp via macOS sandbox."
|
|
262
|
+
}
|
|
263
|
+
}
|
|
186
264
|
|
|
187
|
-
|
|
265
|
+
private var actionsSection: some View {
|
|
266
|
+
HStack(spacing: 12) {
|
|
267
|
+
ActionButton(icon: "text.alignleft", label: "Logs") {
|
|
268
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
269
|
+
openWindow(id: "logs")
|
|
270
|
+
}
|
|
188
271
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
NSApp.terminate(nil)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
272
|
+
ActionButton(icon: "bolt.fill", label: "Agents") {
|
|
273
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
274
|
+
openWindow(id: "agents")
|
|
195
275
|
}
|
|
196
|
-
.padding(12)
|
|
197
276
|
|
|
198
|
-
|
|
277
|
+
ActionButton(icon: "gearshape", label: "Settings") {
|
|
278
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
279
|
+
openWindow(id: "settings")
|
|
280
|
+
}
|
|
199
281
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.foregroundStyle(.tertiary)
|
|
282
|
+
if service.status == .connected || service.status == .starting || service.status == .disconnected {
|
|
283
|
+
ActionButton(icon: "arrow.counterclockwise", label: "Restart") {
|
|
284
|
+
service.restart()
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
ActionButton(icon: "play.fill", label: "Start") {
|
|
288
|
+
service.start()
|
|
208
289
|
}
|
|
209
|
-
|
|
290
|
+
}
|
|
210
291
|
|
|
211
|
-
|
|
292
|
+
Spacer()
|
|
212
293
|
|
|
213
|
-
|
|
294
|
+
ActionButton(icon: "xmark.circle", label: "Quit", tint: .secondary) {
|
|
295
|
+
service.stop()
|
|
296
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
297
|
+
NSApp.terminate(nil)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
.padding(12)
|
|
302
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
303
|
+
.macPanelStyle(.neutral, cornerRadius: 12)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private var footerSection: some View {
|
|
307
|
+
HStack {
|
|
308
|
+
Button {
|
|
309
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
310
|
+
openWindow(id: "about")
|
|
311
|
+
} label: {
|
|
312
|
+
Text(appVersionText)
|
|
214
313
|
.font(.caption2)
|
|
215
|
-
.foregroundStyle(.
|
|
314
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
216
315
|
}
|
|
217
|
-
.
|
|
218
|
-
|
|
316
|
+
.buttonStyle(.plain)
|
|
317
|
+
|
|
318
|
+
Spacer()
|
|
319
|
+
|
|
320
|
+
Text("Not affiliated with Poke")
|
|
321
|
+
.font(.caption2)
|
|
322
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor.opacity(0.7))
|
|
219
323
|
}
|
|
220
|
-
.
|
|
324
|
+
.padding(.horizontal, 6)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private func sectionTitle(_ text: String) -> some View {
|
|
328
|
+
Text(text)
|
|
329
|
+
.font(.caption2)
|
|
330
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
331
|
+
.textCase(.uppercase)
|
|
332
|
+
.tracking(0.5)
|
|
221
333
|
}
|
|
222
334
|
|
|
223
335
|
private var statusText: String {
|
|
@@ -242,6 +354,37 @@ struct PopoverContent: View {
|
|
|
242
354
|
case .stopped: .gray.opacity(0.5)
|
|
243
355
|
}
|
|
244
356
|
}
|
|
357
|
+
|
|
358
|
+
private func modeChipTitle(_ mode: GateService.PermissionMode) -> String {
|
|
359
|
+
switch mode {
|
|
360
|
+
case .full: return "Full"
|
|
361
|
+
case .limited: return "Limited"
|
|
362
|
+
case .sandbox: return "Sandbox"
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private func handleModeSelection(_ mode: GateService.PermissionMode) {
|
|
367
|
+
guard mode != service.permissionMode else { return }
|
|
368
|
+
|
|
369
|
+
if mode == .full && !service.hasSystemPermissionsGranted {
|
|
370
|
+
pendingFullMode = true
|
|
371
|
+
service.openSystemPermission(.accessibility)
|
|
372
|
+
} else {
|
|
373
|
+
pendingFullMode = false
|
|
374
|
+
service.setPermissionMode(mode)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private var appVersionText: String {
|
|
379
|
+
let short = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
|
|
380
|
+
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
|
|
381
|
+
|
|
382
|
+
if let build, !build.isEmpty, build != short {
|
|
383
|
+
return "Poke Gate v\(short) (\(build))"
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return "Poke Gate v\(short)"
|
|
387
|
+
}
|
|
245
388
|
}
|
|
246
389
|
|
|
247
390
|
struct ActionButton: View {
|
|
@@ -8,90 +8,14 @@ struct SettingsView: View {
|
|
|
8
8
|
|
|
9
9
|
var body: some View {
|
|
10
10
|
VStack(alignment: .leading, spacing: 16) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.foregroundStyle(.tertiary)
|
|
15
|
-
.textCase(.uppercase)
|
|
16
|
-
.tracking(0.5)
|
|
17
|
-
|
|
18
|
-
HStack(spacing: 8) {
|
|
19
|
-
Image(systemName: service.hasPokeLoginCredentials
|
|
20
|
-
? "checkmark.shield.fill" : "shield.slash")
|
|
21
|
-
.foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
|
|
22
|
-
.font(.title3)
|
|
23
|
-
|
|
24
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
25
|
-
Text(service.hasPokeLoginCredentials
|
|
26
|
-
? "Signed in via Poke"
|
|
27
|
-
: "Not signed in")
|
|
28
|
-
.font(.subheadline)
|
|
29
|
-
.fontWeight(.medium)
|
|
30
|
-
|
|
31
|
-
if service.hasPokeLoginCredentials {
|
|
32
|
-
Text("Your Poke session is active.")
|
|
33
|
-
.font(.caption)
|
|
34
|
-
.foregroundStyle(.secondary)
|
|
35
|
-
} else {
|
|
36
|
-
Text("Run this command in Terminal to sign in:")
|
|
37
|
-
.font(.caption)
|
|
38
|
-
.foregroundStyle(.secondary)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
.padding(10)
|
|
43
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
44
|
-
.background(.quaternary.opacity(0.5))
|
|
45
|
-
.cornerRadius(8)
|
|
46
|
-
|
|
47
|
-
if !service.hasPokeLoginCredentials {
|
|
48
|
-
Button {
|
|
49
|
-
service.runPokeLogin()
|
|
50
|
-
} label: {
|
|
51
|
-
Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
|
|
52
|
-
}
|
|
53
|
-
.controlSize(.large)
|
|
54
|
-
|
|
55
|
-
Text("Opens a browser window to sign in.")
|
|
56
|
-
.font(.caption)
|
|
57
|
-
.foregroundStyle(.secondary)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
VStack(alignment: .leading, spacing: 8) {
|
|
62
|
-
Text("CONNECTION")
|
|
63
|
-
.font(.caption2)
|
|
64
|
-
.foregroundStyle(.tertiary)
|
|
65
|
-
.textCase(.uppercase)
|
|
66
|
-
.tracking(0.5)
|
|
67
|
-
|
|
68
|
-
HStack(spacing: 8) {
|
|
69
|
-
Circle()
|
|
70
|
-
.fill(connectionColor)
|
|
71
|
-
.frame(width: 8, height: 8)
|
|
72
|
-
|
|
73
|
-
Text(service.status.rawValue)
|
|
74
|
-
.font(.subheadline)
|
|
75
|
-
|
|
76
|
-
Spacer()
|
|
77
|
-
|
|
78
|
-
Button {
|
|
79
|
-
service.restart()
|
|
80
|
-
} label: {
|
|
81
|
-
Label("Reconnect", systemImage: "arrow.counterclockwise")
|
|
82
|
-
.font(.caption)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
.padding(10)
|
|
86
|
-
.frame(maxWidth: .infinity, alignment: .leading)
|
|
87
|
-
.background(.quaternary.opacity(0.5))
|
|
88
|
-
.cornerRadius(8)
|
|
89
|
-
}
|
|
11
|
+
authenticationSection
|
|
12
|
+
accessModeSection
|
|
13
|
+
connectionSection
|
|
90
14
|
|
|
91
15
|
VStack(alignment: .leading, spacing: 8) {
|
|
92
16
|
Text("GENERAL")
|
|
93
17
|
.font(.caption2)
|
|
94
|
-
.foregroundStyle(.
|
|
18
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
95
19
|
.textCase(.uppercase)
|
|
96
20
|
.tracking(0.5)
|
|
97
21
|
|
|
@@ -119,7 +43,127 @@ struct SettingsView: View {
|
|
|
119
43
|
}
|
|
120
44
|
}
|
|
121
45
|
.padding(20)
|
|
122
|
-
.frame(width:
|
|
46
|
+
.frame(width: 430)
|
|
47
|
+
.onAppear { service.startPermissionPolling() }
|
|
48
|
+
.onDisappear { service.stopPermissionPolling() }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private var authenticationSection: some View {
|
|
52
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
53
|
+
sectionTitle("AUTHENTICATION")
|
|
54
|
+
|
|
55
|
+
HStack(spacing: 8) {
|
|
56
|
+
Image(systemName: service.hasPokeLoginCredentials ? "checkmark.shield.fill" : "shield.slash")
|
|
57
|
+
.foregroundStyle(service.hasPokeLoginCredentials ? .green : .orange)
|
|
58
|
+
.font(.title3)
|
|
59
|
+
|
|
60
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
61
|
+
Text(service.hasPokeLoginCredentials ? "Signed in via Poke" : "Not signed in")
|
|
62
|
+
.font(.subheadline)
|
|
63
|
+
.fontWeight(.medium)
|
|
64
|
+
|
|
65
|
+
Text(service.hasPokeLoginCredentials ? "Your Poke session is active." : "Run this command in Terminal to sign in:")
|
|
66
|
+
.font(.caption)
|
|
67
|
+
.foregroundStyle(.secondary)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
.padding(10)
|
|
71
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
72
|
+
.macPanelStyle(.neutral, cornerRadius: 8)
|
|
73
|
+
|
|
74
|
+
if !service.hasPokeLoginCredentials {
|
|
75
|
+
Button {
|
|
76
|
+
service.runPokeLogin()
|
|
77
|
+
} label: {
|
|
78
|
+
Label("Sign in with Poke", systemImage: "person.crop.circle.badge.plus")
|
|
79
|
+
}
|
|
80
|
+
.controlSize(.large)
|
|
81
|
+
|
|
82
|
+
Text("Opens a browser window to sign in.")
|
|
83
|
+
.font(.caption)
|
|
84
|
+
.foregroundStyle(.secondary)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private var accessModeSection: some View {
|
|
90
|
+
VStack(alignment: .leading, spacing: 10) {
|
|
91
|
+
sectionTitle("ACCESS MODE")
|
|
92
|
+
|
|
93
|
+
VStack(spacing: 8) {
|
|
94
|
+
ForEach(GateService.PermissionMode.allCases) { mode in
|
|
95
|
+
permissionModeRow(mode)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if service.permissionMode == .full {
|
|
100
|
+
AccessibilityPermissionView(service: service)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func permissionModeRow(_ mode: GateService.PermissionMode) -> some View {
|
|
106
|
+
Button {
|
|
107
|
+
service.setPermissionMode(mode)
|
|
108
|
+
} label: {
|
|
109
|
+
HStack(alignment: .top, spacing: 10) {
|
|
110
|
+
Image(systemName: service.permissionMode == mode ? "checkmark.circle.fill" : "circle")
|
|
111
|
+
.font(.headline)
|
|
112
|
+
.foregroundStyle(service.permissionMode == mode ? .green : .secondary)
|
|
113
|
+
|
|
114
|
+
VStack(alignment: .leading, spacing: 2) {
|
|
115
|
+
Text(mode.title)
|
|
116
|
+
.font(.subheadline)
|
|
117
|
+
.fontWeight(.medium)
|
|
118
|
+
.foregroundStyle(.primary)
|
|
119
|
+
|
|
120
|
+
Text(mode.subtitle)
|
|
121
|
+
.font(.caption)
|
|
122
|
+
.foregroundStyle(.secondary)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Spacer()
|
|
126
|
+
}
|
|
127
|
+
.padding(10)
|
|
128
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
129
|
+
.macPanelStyle(service.permissionMode == mode ? .selected : .neutral)
|
|
130
|
+
}
|
|
131
|
+
.buttonStyle(.plain)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private var connectionSection: some View {
|
|
135
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
136
|
+
sectionTitle("CONNECTION")
|
|
137
|
+
|
|
138
|
+
HStack(spacing: 8) {
|
|
139
|
+
Circle()
|
|
140
|
+
.fill(connectionColor)
|
|
141
|
+
.frame(width: 8, height: 8)
|
|
142
|
+
|
|
143
|
+
Text(service.status.rawValue)
|
|
144
|
+
.font(.subheadline)
|
|
145
|
+
|
|
146
|
+
Spacer()
|
|
147
|
+
|
|
148
|
+
Button {
|
|
149
|
+
service.restart()
|
|
150
|
+
} label: {
|
|
151
|
+
Label("Reconnect", systemImage: "arrow.counterclockwise")
|
|
152
|
+
.font(.caption)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.padding(10)
|
|
156
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
157
|
+
.macPanelStyle(.neutral, cornerRadius: 8)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func sectionTitle(_ text: String) -> some View {
|
|
162
|
+
Text(text)
|
|
163
|
+
.font(.caption2)
|
|
164
|
+
.foregroundStyle(MacVisualStyle.sectionTitleColor)
|
|
165
|
+
.textCase(.uppercase)
|
|
166
|
+
.tracking(0.5)
|
|
123
167
|
}
|
|
124
168
|
|
|
125
169
|
private var connectionColor: Color {
|