From 859c14d783dc31fe68550b114fc8adeea09dab2a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 21:48:32 +0800
Subject: [PATCH] Sync macOS paste clipboard safety to X-ASR branch
---
privatevoice.src/internal/input/paste_darwin.go | 153 +++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 136 insertions(+), 17 deletions(-)
diff --git a/privatevoice.src/internal/input/paste_darwin.go b/privatevoice.src/internal/input/paste_darwin.go
index cc54e46..bcd8aa2 100755
--- a/privatevoice.src/internal/input/paste_darwin.go
+++ b/privatevoice.src/internal/input/paste_darwin.go
@@ -24,6 +24,11 @@
[pb setString:str forType:NSPasteboardTypeString];
}
+static void clearClipboardText(void) {
+ NSPasteboard* pb = [NSPasteboard generalPasteboard];
+ [pb clearContents];
+}
+
static char* getClipboardText(void) {
NSPasteboard* pb = [NSPasteboard generalPasteboard];
NSString* str = [pb stringForType:NSPasteboardTypeString];
@@ -91,11 +96,33 @@
import (
"fmt"
+ "sync"
"time"
"unsafe"
)
+const (
+ darwinClipboardVerifyTimeout = 500 * time.Millisecond
+ darwinClipboardPollInterval = 15 * time.Millisecond
+ darwinClipboardRestoreDelay = 900 * time.Millisecond
+)
+
type darwinPaster struct{}
+
+type darwinClipboardSnapshot struct {
+ text string
+ hasText bool
+}
+
+type darwinPasteState struct {
+ mu sync.Mutex
+ generation uint64
+ pendingRestore bool
+ pendingText string
+ restoreTo darwinClipboardSnapshot
+}
+
+var globalDarwinPasteState darwinPasteState
func newPlatformPaster() Paster {
return &darwinPaster{}
@@ -106,31 +133,44 @@
return fmt.Errorf("accessibility permission is required to paste")
}
- var oldClip *string
+ state := &globalDarwinPasteState
+ state.mu.Lock()
+ state.generation++
+ generation := state.generation
+
+ originalClipboard := currentDarwinClipboardSnapshot()
if !keepClipboard {
- cOld := C.getClipboardText()
- if cOld != nil {
- old := C.GoString(cOld)
- oldClip = &old
- C.free(unsafe.Pointer(cOld))
- }
+ originalClipboard = resolveDarwinRestoreTarget(
+ originalClipboard,
+ state.pendingRestore,
+ state.pendingText,
+ state.restoreTo,
+ )
}
cstr := C.CString(text)
defer C.free(unsafe.Pointer(cstr))
C.setClipboardText(cstr)
- time.Sleep(60 * time.Millisecond)
- C.simulateCmdV()
-
- if !keepClipboard && oldClip != nil {
- go func(old string) {
- time.Sleep(600 * time.Millisecond)
- cOld := C.CString(old)
- defer C.free(unsafe.Pointer(cOld))
- C.setClipboardText(cOld)
- }(*oldClip)
+ if !waitForDarwinClipboardText(text, darwinClipboardVerifyTimeout) {
+ state.pendingRestore = false
+ state.mu.Unlock()
+ return fmt.Errorf("clipboard verification failed before paste")
}
+ C.simulateCmdV()
+
+ if keepClipboard {
+ state.pendingRestore = false
+ state.mu.Unlock()
+ return nil
+ }
+
+ state.pendingRestore = true
+ state.pendingText = text
+ state.restoreTo = originalClipboard
+ state.mu.Unlock()
+
+ go restoreDarwinClipboardAfterPaste(generation, text, originalClipboard)
return nil
}
@@ -143,3 +183,82 @@
}
return nil
}
+
+func currentDarwinClipboardSnapshot() darwinClipboardSnapshot {
+ cText := C.getClipboardText()
+ if cText == nil {
+ return darwinClipboardSnapshot{}
+ }
+ defer C.free(unsafe.Pointer(cText))
+ return darwinClipboardSnapshot{
+ text: C.GoString(cText),
+ hasText: true,
+ }
+}
+
+func resolveDarwinRestoreTarget(current darwinClipboardSnapshot, pending bool, pendingText string, restoreTo darwinClipboardSnapshot) darwinClipboardSnapshot {
+ if pending && current.hasText && current.text == pendingText {
+ return restoreTo
+ }
+ return current
+}
+
+func waitForDarwinClipboardText(expected string, timeout time.Duration) bool {
+ deadline := time.Now().Add(timeout)
+ for {
+ current := currentDarwinClipboardSnapshot()
+ if current.hasText && current.text == expected {
+ return true
+ }
+ if time.Now().After(deadline) {
+ return false
+ }
+ time.Sleep(darwinClipboardPollInterval)
+ }
+}
+
+func restoreDarwinClipboardAfterPaste(generation uint64, pastedText string, restoreTo darwinClipboardSnapshot) {
+ time.Sleep(darwinClipboardRestoreDelay)
+
+ state := &globalDarwinPasteState
+ state.mu.Lock()
+ defer state.mu.Unlock()
+
+ if state.generation != generation || !state.pendingRestore || state.pendingText != pastedText {
+ return
+ }
+
+ current := currentDarwinClipboardSnapshot()
+ if !shouldRestoreDarwinClipboard(
+ state.generation,
+ generation,
+ state.pendingRestore,
+ state.pendingText,
+ pastedText,
+ current,
+ ) {
+ state.pendingRestore = false
+ return
+ }
+
+ restoreDarwinClipboardSnapshot(restoreTo)
+ state.pendingRestore = false
+}
+
+func shouldRestoreDarwinClipboard(currentGeneration, restoreGeneration uint64, pending bool, pendingText, pastedText string, current darwinClipboardSnapshot) bool {
+ if currentGeneration != restoreGeneration || !pending || pendingText != pastedText {
+ return false
+ }
+ return current.hasText && current.text == pastedText
+}
+
+func restoreDarwinClipboardSnapshot(snapshot darwinClipboardSnapshot) {
+ if !snapshot.hasText {
+ C.clearClipboardText()
+ return
+ }
+
+ cText := C.CString(snapshot.text)
+ defer C.free(unsafe.Pointer(cText))
+ C.setClipboardText(cText)
+}
--
Gitblit v1.9.3