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