From 3aad7c3d5a056c142c3536a107fdd132ba5b6a88 Mon Sep 17 00:00:00 2001 From: cmagnussen Date: Tue, 2 Jun 2026 21:43:16 +0200 Subject: [PATCH] Fix clipboard fallback for auto paste --- BlitztextMac/App/AppState.swift | 92 +++------------------------------ README.md | 2 + docs/privacy.md | 2 +- docs/setup.md | 6 ++- 4 files changed, 16 insertions(+), 86 deletions(-) diff --git a/BlitztextMac/App/AppState.swift b/BlitztextMac/App/AppState.swift index 58cbd97..0dce673 100644 --- a/BlitztextMac/App/AppState.swift +++ b/BlitztextMac/App/AppState.swift @@ -13,10 +13,7 @@ enum PopoverPage: Equatable { @MainActor final class AppState { private static let pasteRetryInitialAttempts = 22 - private static let clipboardRestoreDelayAfterPaste: TimeInterval = 1.5 - private static let clipboardFallbackRestoreDelay: TimeInterval = 60 private static let concealedPasteboardType = NSPasteboard.PasteboardType("org.nspasteboard.ConcealedType") - private static let pasteboardMarkerType = NSPasteboard.PasteboardType("app.blitztext.pasteboard-marker") var activeWorkflow: (any Workflow)? var page: PopoverPage = .main @@ -37,7 +34,6 @@ final class AppState { private var lastPopoverPasteTarget: PasteTarget? private var menuBarStatusResetTask: Task? private var workflowCleanupTask: Task? - private var pasteboardCleanupTask: Task? // Persisted settings var appSettings: AppSettings { @@ -294,20 +290,15 @@ final class AppState { } func copyToClipboard(_ text: String) { - _ = writeSensitiveTextToPasteboard(text) + writeSensitiveTextToPasteboard(text) } // MARK: - Auto-Paste /// Copies the text, restores focus when needed, then simulates Cmd+V. - /// The previous clipboard content is restored after paste when possible. + /// The text intentionally remains on the clipboard as a fallback if paste is blocked. private func pasteAtCursor(_ text: String, target: PasteTarget? = nil) { - let pasteboardState = writeSensitiveTextToPasteboard(text) - schedulePasteboardRestore( - marker: pasteboardState.marker, - previousContents: pasteboardState.previousContents, - after: Self.clipboardFallbackRestoreDelay - ) + writeSensitiveTextToPasteboard(text) if isPopoverShown { NotificationCenter.default.post(name: .dismissPopover, object: nil) @@ -322,45 +313,17 @@ final class AppState { attemptPasteTrusted( target: target, - attemptsRemaining: Self.pasteRetryInitialAttempts, - pasteboardState: pasteboardState + attemptsRemaining: Self.pasteRetryInitialAttempts ) } - private func writeSensitiveTextToPasteboard(_ text: String) -> (marker: String, previousContents: PasteboardSnapshot) { + private func writeSensitiveTextToPasteboard(_ text: String) { let pasteboard = NSPasteboard.general - let previousContents = PasteboardSnapshot.capture(from: pasteboard) - let marker = UUID().uuidString pasteboard.clearContents() - pasteboard.declareTypes([.string, Self.concealedPasteboardType, Self.pasteboardMarkerType], owner: nil) + pasteboard.declareTypes([.string, Self.concealedPasteboardType], owner: nil) pasteboard.setString(text, forType: .string) pasteboard.setString("", forType: Self.concealedPasteboardType) - pasteboard.setString(marker, forType: Self.pasteboardMarkerType) - - return (marker, previousContents) - } - - private func schedulePasteboardRestore( - marker: String, - previousContents: PasteboardSnapshot, - after delay: TimeInterval - ) { - pasteboardCleanupTask?.cancel() - pasteboardCleanupTask = Task { @MainActor [weak self] in - try? await Task.sleep(for: .seconds(delay)) - guard let self else { return } - self.restorePasteboardIfCurrent(marker: marker, previousContents: previousContents) - } - } - - private func restorePasteboardIfCurrent(marker: String, previousContents: PasteboardSnapshot) { - let pasteboard = NSPasteboard.general - guard pasteboard.string(forType: Self.pasteboardMarkerType) == marker else { - return - } - - previousContents.restore(to: pasteboard) } func prepareForPopoverPresentation() { @@ -573,19 +536,13 @@ final class AppState { private func attemptPasteTrusted( target: PasteTarget?, - attemptsRemaining: Int, - pasteboardState: (marker: String, previousContents: PasteboardSnapshot) + attemptsRemaining: Int ) { let frontmostPid = NSWorkspace.shared.frontmostApplication?.processIdentifier if let target { if frontmostPid == target.processIdentifier { performPaste() - schedulePasteboardRestore( - marker: pasteboardState.marker, - previousContents: pasteboardState.previousContents, - after: Self.clipboardRestoreDelayAfterPaste - ) return } @@ -611,8 +568,7 @@ final class AppState { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in self?.attemptPasteTrusted( target: target, - attemptsRemaining: attemptsRemaining - 1, - pasteboardState: pasteboardState + attemptsRemaining: attemptsRemaining - 1 ) } } @@ -660,35 +616,3 @@ private struct PasteTarget { let processIdentifier: pid_t let application: NSRunningApplication } - -private struct PasteboardSnapshot { - private let items: [[NSPasteboard.PasteboardType: Data]] - - static func capture(from pasteboard: NSPasteboard) -> PasteboardSnapshot { - let capturedItems = pasteboard.pasteboardItems?.map { item in - item.types.reduce(into: [NSPasteboard.PasteboardType: Data]()) { values, type in - values[type] = item.data(forType: type) - } - } ?? [] - - return PasteboardSnapshot(items: capturedItems) - } - - func restore(to pasteboard: NSPasteboard) { - pasteboard.clearContents() - - guard !items.isEmpty else { - return - } - - let restoredItems = items.map { values in - let item = NSPasteboardItem() - for (type, data) in values { - item.setData(data, forType: type) - } - return item - } - - pasteboard.writeObjects(restoredItems) - } -} diff --git a/README.md b/README.md index 6221ae4..b19491c 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ Blitztext asks for: If you do not grant Accessibility permission, you can still copy results manually. +Full Disk Access is not required. If auto-paste does not work even though transcription succeeds, open **System Settings -> Privacy & Security -> Accessibility**, enable Blitztext there, restart Blitztext, and try again with the cursor focused in a text field. If macOS shows multiple Blitztext entries, remove or disable the old ones and grant the permission to the app you just built or installed. + ## Data Flow The preview has no custom backend. diff --git a/docs/privacy.md b/docs/privacy.md index 1047f1d..a950c8f 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -21,7 +21,7 @@ The app stores: - optional WhisperKit/CoreML model folders in local app support storage - temporary audio files while a transcription is being processed; the app attempts to delete each recording when the workflow ends or is cancelled -Workflow output may also be placed on your clipboard so it can be pasted into another app. Auto-paste marks the clipboard entry as concealed for compatible clipboard managers and attempts to restore the previous clipboard content after paste. Clipboard managers, macOS, or other apps may still observe clipboard contents while they are present. +Workflow output may also be placed on your clipboard so it can be pasted into another app. Auto-paste marks the clipboard entry as concealed for compatible clipboard managers, but the generated text intentionally remains on the clipboard as a fallback if automatic paste is blocked. Clipboard managers, macOS, or other apps may still observe clipboard contents while they are present. The app uses the system TLS trust store for OpenAI and Hugging Face requests. It does not currently pin certificates. A user-installed or managed root certificate can therefore affect HTTPS trust decisions on that Mac. diff --git a/docs/setup.md b/docs/setup.md index ac969f4..bc45292 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -65,12 +65,16 @@ The app needs Microphone permission to record audio. For automatic paste into the previous app, grant Accessibility permission in macOS System Settings. Without it, you can still copy and paste manually. +Blitztext does not need Full Disk Access. Auto-paste uses the Accessibility permission because the app simulates Cmd+V after putting the result on the clipboard. + ## Troubleshooting - If `xcodebuild` reports that the active developer directory is only Command Line Tools, run `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`. - If the build cannot find XcodeGen, install it explicitly with `brew install xcodegen`. - If online transcription fails immediately, check whether the API key is present and valid. - If secure local mode is disabled, check whether a WhisperKit model is installed in the expected folder. -- If paste does not work, check Accessibility permission. +- If transcription works but paste does not, this is not an OpenAI billing issue. Check **Privacy & Security -> Accessibility**, restart Blitztext after changing the permission, and make sure the cursor is focused in a text field before starting the workflow. +- If macOS shows multiple Blitztext entries under Accessibility, remove or disable stale entries, run the app from the final location (`/Applications` if you used `./build.sh --install`), then grant the permission again. +- If the target app blocks synthetic paste or the target app was not detected, the result still stays on the clipboard so you can press Cmd+V manually. - If audio is missing, check Microphone permission and macOS input settings. - If you see OpenAI errors, verify model access and account billing.