@hasna/recordings 0.3.13 → 0.5.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/README.md +279 -43
- package/bun.lock +37 -38
- package/contracts/v1/fixtures.json +1206 -0
- package/dist/cli/index.js +1909 -619
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/contracts/hosted-v1.d.ts +105 -0
- package/dist/contracts/hosted-v1.d.ts.map +1 -0
- package/dist/contracts/hosted-v1.js +41 -0
- package/dist/contracts/stream-v1.d.ts +117 -0
- package/dist/contracts/stream-v1.d.ts.map +1 -0
- package/dist/contracts/stream-v1.js +35 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/hosted/index.d.ts +58 -0
- package/dist/hosted/index.d.ts.map +1 -0
- package/dist/hosted/index.js +266 -0
- package/dist/hosted/transport.d.ts +36 -0
- package/dist/hosted/transport.d.ts.map +1 -0
- package/dist/hosted-v1-aavn7ktb.js +4114 -0
- package/dist/hosted-v1-gdr9extc.js +84 -0
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2879 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1713 -459
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +590 -287
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +1358 -874
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/hosted-sdk.md +71 -0
- package/docs/wire-contracts.md +24 -0
- package/package.json +33 -11
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/ci-linux-suite.ts +23 -13
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +60 -49
- package/scripts/migrate.ts +4 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +36 -4
- package/scripts/native-core-receipt.py +171 -0
- package/scripts/native_fs_guard.ts +2 -0
- package/scripts/release-suite-gate.ts +227 -150
- package/scripts/resolve_tailscale_cli.sh +24 -3
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
- package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +8 -7
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
- package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
- package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
- package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -4,29 +4,31 @@ import SwiftUI
|
|
|
4
4
|
public struct SettingsView: View {
|
|
5
5
|
@ObservedObject public var engine: RecordingEngine
|
|
6
6
|
@ObservedObject public var shortcuts: VoiceShortcuts
|
|
7
|
-
@ObservedObject public var
|
|
8
|
-
@
|
|
7
|
+
@ObservedObject public var preferences: ProjectStore
|
|
8
|
+
@State private var openAIAPIKey = ""
|
|
9
|
+
@State private var providerStatus = ""
|
|
9
10
|
|
|
10
|
-
public init(engine: RecordingEngine, shortcuts: VoiceShortcuts,
|
|
11
|
+
public init(engine: RecordingEngine, shortcuts: VoiceShortcuts, preferences: ProjectStore) {
|
|
11
12
|
self.engine = engine
|
|
12
13
|
self.shortcuts = shortcuts
|
|
13
|
-
self.
|
|
14
|
+
self.preferences = preferences
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
public var body: some View {
|
|
17
18
|
TabView {
|
|
18
19
|
generalTab.tabItem { Label("General", systemImage: "gear") }
|
|
19
|
-
projectsTab.tabItem { Label("Projects", systemImage: "folder") }
|
|
20
20
|
shortcutsTab.tabItem { Label("Voice Shortcuts", systemImage: "text.badge.star") }
|
|
21
21
|
}
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
.font(.system(size: 13))
|
|
23
|
+
.controlSize(.small)
|
|
24
|
+
.frame(width: 520, height: 460)
|
|
25
|
+
.alert("Settings Error", isPresented: Binding(
|
|
26
|
+
get: { preferences.persistenceError != nil },
|
|
27
|
+
set: { if !$0 { preferences.clearPersistenceError() } }
|
|
26
28
|
)) {
|
|
27
29
|
Button("OK", role: .cancel) {}
|
|
28
30
|
} message: {
|
|
29
|
-
Text(
|
|
31
|
+
Text(preferences.persistenceError ?? "The settings could not be saved.")
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -34,18 +36,38 @@ public struct SettingsView: View {
|
|
|
34
36
|
|
|
35
37
|
private var generalTab: some View {
|
|
36
38
|
Form {
|
|
39
|
+
ServiceConnectionView(home: engine.home)
|
|
37
40
|
Section("OpenAI") {
|
|
38
41
|
SecureField("API key", text: $openAIAPIKey)
|
|
39
42
|
.textFieldStyle(.roundedBorder)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try
|
|
43
|
+
Button("Save OpenAI Key") {
|
|
44
|
+
do {
|
|
45
|
+
try OpenAIAPIKeyStore.save(key: openAIAPIKey, homePath: engine.home)
|
|
46
|
+
openAIAPIKey = ""
|
|
47
|
+
providerStatus = "OpenAI key saved in Keychain."
|
|
48
|
+
} catch {
|
|
49
|
+
providerStatus = error.localizedDescription
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.disabled(openAIAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
53
|
+
Button("Connect Saved Key") {
|
|
54
|
+
do {
|
|
55
|
+
providerStatus = (try OpenAIAPIKeyStore.loadKeychain(allowInteraction: true))?.isEmpty == false
|
|
56
|
+
? "OpenAI key available from Keychain."
|
|
57
|
+
: "No saved OpenAI key found in Keychain."
|
|
58
|
+
} catch { providerStatus = error.localizedDescription }
|
|
59
|
+
}
|
|
60
|
+
Text(providerStatus).foregroundStyle(.secondary)
|
|
61
|
+
.onAppear {
|
|
62
|
+
providerStatus = OpenAIAPIKeyStore.load(homePath: engine.home).isEmpty
|
|
63
|
+
? "Add an OpenAI key for transcription."
|
|
64
|
+
: "OpenAI key available for transcription."
|
|
43
65
|
}
|
|
44
66
|
Picker("Language", selection: $engine.transcriptionLanguage) {
|
|
45
67
|
Text("English").tag("en")
|
|
46
68
|
Text("Auto Detect").tag("auto")
|
|
47
69
|
}
|
|
48
|
-
Text("Used for live transcription and the final paste.
|
|
70
|
+
Text("Used for live transcription and the final paste. New keys are stored in macOS Keychain.")
|
|
49
71
|
.foregroundStyle(.secondary)
|
|
50
72
|
}
|
|
51
73
|
|
|
@@ -143,82 +165,26 @@ public struct SettingsView: View {
|
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
Section("Transcription Cleanup") {
|
|
146
|
-
Picker("Mode", selection: $
|
|
168
|
+
Picker("Mode", selection: $preferences.settings.postProcessingMode) {
|
|
147
169
|
ForEach(PostProcessingMode.allCases) { mode in
|
|
148
170
|
Text(mode.label).tag(mode.rawValue)
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
173
|
.pickerStyle(.segmented)
|
|
152
|
-
.onChange(of:
|
|
153
|
-
try?
|
|
174
|
+
.onChange(of: preferences.settings.postProcessingMode) {
|
|
175
|
+
try? preferences.save()
|
|
154
176
|
}
|
|
155
|
-
TextEditor(text: $
|
|
156
|
-
.frame(height: 80)
|
|
157
|
-
.onChange(of:
|
|
158
|
-
try?
|
|
177
|
+
TextEditor(text: $preferences.settings.globalSystemPrompt)
|
|
178
|
+
.frame(height: 80).scrollContentBackground(.hidden)
|
|
179
|
+
.onChange(of: preferences.settings.globalSystemPrompt) {
|
|
180
|
+
try? preferences.save()
|
|
159
181
|
}
|
|
160
182
|
Text("Instructions for post-transcription cleanup and formatting.")
|
|
161
183
|
.foregroundStyle(.secondary)
|
|
162
184
|
}
|
|
163
|
-
.disabled(!
|
|
164
|
-
}
|
|
165
|
-
.formStyle(.grouped).padding()
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// MARK: - Projects
|
|
169
|
-
|
|
170
|
-
@State private var newProjectName = ""
|
|
171
|
-
@State private var editingProject: RecProject?
|
|
172
|
-
|
|
173
|
-
private var projectsTab: some View {
|
|
174
|
-
VStack(spacing: 0) {
|
|
175
|
-
HStack(spacing: 8) {
|
|
176
|
-
TextField("New project name", text: $newProjectName)
|
|
177
|
-
.textFieldStyle(.roundedBorder)
|
|
178
|
-
Button("Add") {
|
|
179
|
-
guard !newProjectName.isEmpty else { return }
|
|
180
|
-
let name = newProjectName
|
|
181
|
-
Task {
|
|
182
|
-
do {
|
|
183
|
-
try await projectStore.addProject(name: name)
|
|
184
|
-
newProjectName = ""
|
|
185
|
-
} catch {}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
.disabled(newProjectName.isEmpty)
|
|
189
|
-
}
|
|
190
|
-
.padding()
|
|
191
|
-
|
|
192
|
-
Divider()
|
|
193
|
-
|
|
194
|
-
if projectStore.settings.projects.isEmpty {
|
|
195
|
-
Spacer()
|
|
196
|
-
VStack(spacing: 8) {
|
|
197
|
-
Image(systemName: "folder").font(.largeTitle).foregroundStyle(.quaternary)
|
|
198
|
-
Text("No projects yet").foregroundStyle(.secondary)
|
|
199
|
-
}
|
|
200
|
-
Spacer()
|
|
201
|
-
} else {
|
|
202
|
-
List {
|
|
203
|
-
ForEach(projectStore.settings.projects) { project in
|
|
204
|
-
ProjectRow(project: project) {
|
|
205
|
-
editingProject = project
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
.onDelete { indexSet in
|
|
209
|
-
for i in indexSet {
|
|
210
|
-
try? projectStore.removeProject(id: projectStore.settings.projects[i].id)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
.disabled(!projectStore.canMutateProjects)
|
|
217
|
-
.sheet(item: $editingProject) { project in
|
|
218
|
-
ProjectEditView(project: project, store: projectStore) {
|
|
219
|
-
editingProject = nil
|
|
220
|
-
}
|
|
185
|
+
.disabled(!preferences.canMutateProjects)
|
|
221
186
|
}
|
|
187
|
+
.formStyle(.grouped).scrollContentBackground(.hidden).padding(8)
|
|
222
188
|
}
|
|
223
189
|
|
|
224
190
|
// MARK: - Voice Shortcuts
|
|
@@ -263,73 +229,3 @@ public struct SettingsView: View {
|
|
|
263
229
|
}
|
|
264
230
|
}
|
|
265
231
|
}
|
|
266
|
-
|
|
267
|
-
// MARK: - Project Row
|
|
268
|
-
|
|
269
|
-
struct ProjectRow: View {
|
|
270
|
-
let project: RecProject
|
|
271
|
-
let onEdit: () -> Void
|
|
272
|
-
|
|
273
|
-
var body: some View {
|
|
274
|
-
HStack {
|
|
275
|
-
VStack(alignment: .leading, spacing: 2) {
|
|
276
|
-
Text(project.name)
|
|
277
|
-
if let path = project.path, !path.isEmpty {
|
|
278
|
-
Text(path).foregroundStyle(.secondary).lineLimit(1)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
Spacer()
|
|
282
|
-
Button("Edit") { onEdit() }
|
|
283
|
-
.controlSize(.small)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// MARK: - Project Edit
|
|
289
|
-
|
|
290
|
-
struct ProjectEditView: View {
|
|
291
|
-
@State var project: RecProject
|
|
292
|
-
let store: ProjectStore
|
|
293
|
-
let onDismiss: () -> Void
|
|
294
|
-
|
|
295
|
-
var body: some View {
|
|
296
|
-
VStack(spacing: 0) {
|
|
297
|
-
Form {
|
|
298
|
-
Section("Project") {
|
|
299
|
-
TextField("Name", text: $project.name)
|
|
300
|
-
TextField("Path", text: Binding(
|
|
301
|
-
get: { project.path ?? "" },
|
|
302
|
-
set: { project.path = $0.isEmpty ? nil : $0 }
|
|
303
|
-
))
|
|
304
|
-
.textFieldStyle(.roundedBorder)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
Section("Transcriber Instructions") {
|
|
308
|
-
TextEditor(text: Binding(
|
|
309
|
-
get: { project.systemPrompt ?? "" },
|
|
310
|
-
set: { project.systemPrompt = $0.isEmpty ? nil : $0 }
|
|
311
|
-
))
|
|
312
|
-
.frame(height: 100)
|
|
313
|
-
Text("Project-specific cleanup and formatting instructions.")
|
|
314
|
-
.foregroundStyle(.secondary)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
.formStyle(.grouped)
|
|
318
|
-
.disabled(!store.canMutateProjects)
|
|
319
|
-
|
|
320
|
-
HStack {
|
|
321
|
-
Spacer()
|
|
322
|
-
Button("Cancel") { onDismiss() }
|
|
323
|
-
Button("Save") {
|
|
324
|
-
do {
|
|
325
|
-
try store.updateProject(project)
|
|
326
|
-
onDismiss()
|
|
327
|
-
} catch {}
|
|
328
|
-
}
|
|
329
|
-
.disabled(!store.canMutateProjects)
|
|
330
|
-
}
|
|
331
|
-
.padding()
|
|
332
|
-
}
|
|
333
|
-
.frame(width: 420, height: 340)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
/// Settings must also open from an accessory app with no SwiftUI workspace scene.
|
|
5
|
+
/// Retain the window so repeated menu actions and closing/reopening preserve its state.
|
|
6
|
+
@MainActor
|
|
7
|
+
public final class SettingsWindowController {
|
|
8
|
+
private var window: NSWindow?
|
|
9
|
+
private let makeContent: @MainActor () -> NSView
|
|
10
|
+
|
|
11
|
+
public init(content: @escaping @MainActor () -> NSView) {
|
|
12
|
+
makeContent = content
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@discardableResult
|
|
16
|
+
public func show() -> NSWindow {
|
|
17
|
+
let settings: NSWindow
|
|
18
|
+
if let window {
|
|
19
|
+
settings = window
|
|
20
|
+
} else {
|
|
21
|
+
settings = NSWindow(
|
|
22
|
+
contentRect: NSRect(x: 0, y: 0, width: 520, height: 500),
|
|
23
|
+
styleMask: [.titled, .closable],
|
|
24
|
+
backing: .buffered,
|
|
25
|
+
defer: false
|
|
26
|
+
)
|
|
27
|
+
settings.title = "Hasna Recordings Settings"
|
|
28
|
+
settings.isReleasedWhenClosed = false
|
|
29
|
+
settings.isOpaque = false
|
|
30
|
+
settings.backgroundColor = .clear
|
|
31
|
+
settings.titlebarAppearsTransparent = true
|
|
32
|
+
let content = makeContent()
|
|
33
|
+
let backdrop = NSHostingView(rootView: RecordingGlassBackground())
|
|
34
|
+
backdrop.frame = content.bounds
|
|
35
|
+
backdrop.autoresizingMask = [.width, .height]
|
|
36
|
+
content.addSubview(backdrop, positioned: .below, relativeTo: nil)
|
|
37
|
+
settings.contentView = content
|
|
38
|
+
settings.center()
|
|
39
|
+
window = settings
|
|
40
|
+
}
|
|
41
|
+
// Activation is needed when invoked from a menu-bar popover. Preserve the
|
|
42
|
+
// activation policy so a bar-only app stays out of the Dock and app switcher.
|
|
43
|
+
NSApplication.shared.activate()
|
|
44
|
+
NSRunningApplication.current.activate(options: [.activateAllWindows])
|
|
45
|
+
settings.makeKeyAndOrderFront(nil)
|
|
46
|
+
return settings
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Dispatch
|
|
2
|
+
import Foundation
|
|
3
|
+
import Testing
|
|
4
|
+
@testable import RecordingsLib
|
|
5
|
+
|
|
6
|
+
@Suite("Blocking operation scheduling")
|
|
7
|
+
struct BlockingOperationTests {
|
|
8
|
+
private final class Observation: @unchecked Sendable {
|
|
9
|
+
let entered = DispatchSemaphore(value: 0)
|
|
10
|
+
let release = DispatchSemaphore(value: 0)
|
|
11
|
+
let peer = DispatchSemaphore(value: 0)
|
|
12
|
+
private let lock = NSLock()
|
|
13
|
+
private var result = (entered: false, peerBeforeRelease: false)
|
|
14
|
+
|
|
15
|
+
func record(entered: Bool, peerBeforeRelease: Bool) {
|
|
16
|
+
lock.lock()
|
|
17
|
+
result = (entered, peerBeforeRelease)
|
|
18
|
+
lock.unlock()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func snapshot() -> (entered: Bool, peerBeforeRelease: Bool) {
|
|
22
|
+
lock.lock()
|
|
23
|
+
defer { lock.unlock() }
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Test("blocking work leaves executor progress available and drains after cancellation", arguments: [false, true], [false, true])
|
|
29
|
+
func peerProgressWhileBlocked(cancel: Bool, delayedSubmission: Bool) async {
|
|
30
|
+
let observation = Observation()
|
|
31
|
+
// A single buffered handoff gives the operation its already-created Task handle
|
|
32
|
+
// without blocking a cooperative worker or capturing an uninitialized variable.
|
|
33
|
+
let handoff = AsyncStream<Task<(Int, Bool), Never>>.makeStream(bufferingPolicy: .bufferingNewest(1))
|
|
34
|
+
let work = Task.detached {
|
|
35
|
+
var handles = handoff.stream.makeAsyncIterator()
|
|
36
|
+
guard let ownTask = await handles.next() else {
|
|
37
|
+
preconditionFailure("Fixture task handle was not supplied")
|
|
38
|
+
}
|
|
39
|
+
// Admission is deliberately slower than the former two-second entry timer.
|
|
40
|
+
// Only progress while the synchronous operation is held is under observation.
|
|
41
|
+
if delayedSubmission { try? await Task.sleep(for: .seconds(3)) }
|
|
42
|
+
let value = await BlockingOperation.run {
|
|
43
|
+
observation.entered.signal()
|
|
44
|
+
// Start the observation inside the operation, after entry. This independent
|
|
45
|
+
// thread always releases the gate, even when the peer misses its deadline.
|
|
46
|
+
Thread.detachNewThread {
|
|
47
|
+
defer { observation.release.signal() }
|
|
48
|
+
let entered = observation.entered.wait(timeout: .now()) == .success
|
|
49
|
+
if cancel { ownTask.cancel() }
|
|
50
|
+
if entered {
|
|
51
|
+
Task.detached { observation.peer.signal() }
|
|
52
|
+
}
|
|
53
|
+
let progressed = entered && observation.peer.wait(timeout: .now() + .seconds(1)) == .success
|
|
54
|
+
observation.record(entered: entered, peerBeforeRelease: progressed)
|
|
55
|
+
}
|
|
56
|
+
observation.release.wait()
|
|
57
|
+
return 7
|
|
58
|
+
}
|
|
59
|
+
return (value, Task.isCancelled)
|
|
60
|
+
}
|
|
61
|
+
handoff.continuation.yield(work)
|
|
62
|
+
handoff.continuation.finish()
|
|
63
|
+
let (value, wasCancelled) = await work.value
|
|
64
|
+
let observed = observation.snapshot()
|
|
65
|
+
#expect(observed.entered)
|
|
66
|
+
#expect(observed.peerBeforeRelease)
|
|
67
|
+
#expect(value == 7)
|
|
68
|
+
#expect(wasCancelled == cancel)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private enum FixtureFailure: Error { case expected }
|
|
72
|
+
|
|
73
|
+
@Test("throwing blocking work preserves its exact error")
|
|
74
|
+
func preservesError() async {
|
|
75
|
+
do {
|
|
76
|
+
let _: Int = try await BlockingOperation.runThrowing(qos: .utility) {
|
|
77
|
+
throw FixtureFailure.expected
|
|
78
|
+
}
|
|
79
|
+
Issue.record("Blocking operation discarded its error")
|
|
80
|
+
} catch FixtureFailure.expected {
|
|
81
|
+
} catch {
|
|
82
|
+
Issue.record("Blocking operation replaced its error: \(error)")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|