//go:build windows package clipboard import ( "b612.me/win32api" "errors" "fmt" "runtime" "sync" "sync/atomic" "syscall" "time" "unsafe" ) var stopSign chan struct{} var isListening uint32 /* func listenMethod2() (<-chan Clipboard, error) { if atomic.LoadUint32(&isListening) == 1 { return nil, errors.New("Already listening") } atomic.StoreUint32(&isListening, 1) res := make(chan Clipboard, 3) stopSign = make(chan struct{}) hWnd, err := win32api.CreateWindowEx(0, "Message", "B612 Clipboard Listener", 0, 0, 0, 500, 500, 0, 0, 0, nil) if hWnd == 0 || err != nil { return nil, fmt.Errorf("Failed to create window: %v,hWnd:%v", err, hWnd) } set, err := win32api.AddClipboardFormatListener(hWnd) if !set || err != nil { return nil, fmt.Errorf("Failed to set clipboard listener: %v", err) } fetcher := make(chan struct{}, 8) go fetchListener(hWnd, fetcher) go func() { for { select { case <-stopSign: fmt.Println("stopped") atomic.StoreUint32(&isListening, 0) close(res) close(stopSign) win32api.RemoveClipboardFormatListener(win32api.HWND(hWnd)) win32api.DestoryWindow(hWnd) return case <-fetcher: cb, err := Get() if err != nil { fmt.Println(err) } if atomic.LoadUint32(&isListening) == 1 { res <- cb continue } } } }() return res, nil } func fetchListener(hWnd win32api.HWND, res chan struct{}) { for { var msg win32api.MSG _, err := win32api.GetMessage(&msg, hWnd, 0, 0) if msg.Message == 0x0012 { return } if err == nil && win32api.DWORD(msg.Message) == win32api.WM_CLIPBOARDUPDATE { res <- struct{}{} } } } func PauseListen() error { if atomic.LoadUint32(&isListening) == 0 { return errors.New("Not listening") } atomic.StoreUint32(&isListening, 2) return nil } func RecoverListen() error { if atomic.LoadUint32(&isListening) == 0 { return errors.New("Not Listening") } atomic.StoreUint32(&isListening, 1) return nil } func StopListen() error { defer func() { if r := recover(); r != nil { fmt.Println("StopListen panic:", r) } }() if atomic.LoadUint32(&isListening) == 0 { return nil } stopSign <- struct{}{} return nil } func Listen(onlyMeta bool) (<-chan Clipboard, error) { if atomic.LoadUint32(&isListening) != 0 { return nil, errors.New("Already listening") } atomic.StoreUint32(&isListening, 1) res := make(chan Clipboard, 3) stopSign = make(chan struct{}) go func() { var storeSeq win32api.DWORD for { select { case <-stopSign: atomic.StoreUint32(&isListening, 0) close(res) close(stopSign) return case <-time.After(time.Millisecond * 900): seq, err := win32api.GetClipboardSequenceNumber() if err != nil { continue } if seq != storeSeq { storeSeq = seq //fmt.Println("Clipboard updated", seq, storeSeq) if atomic.LoadUint32(&isListening) == 1 { if !onlyMeta { cb, err := Get() if err != nil { continue } if atomic.LoadUint32(&isListening) == 1 { res <- cb continue } } else { cb, err := GetMeta() if err != nil { continue } if atomic.LoadUint32(&isListening) == 1 { res <- cb continue } } } } } } }() return res, nil } */ const WM_USER_STOP = win32api.WM_USER + 1 var clipboardChannel chan Clipboard var onlyMetaGlobal bool var hwndGlobal win32api.HWND var lastSequence win32api.DWORD var sequenceMux sync.Mutex func clipboardWndProc(hWnd win32api.HWND, uMsg win32api.UINT, wParam win32api.WPARAM, lParam win32api.LPARAM) win32api.LRESULT { switch uMsg { case 0x031D: // WM_CLIPBOARDUPDATE // 标记需要检查剪贴板 go checkClipboard(false) return 0 case WM_USER_STOP: win32api.PostQuitMessage(0) return 0 case 0x0002: // WM_DESTROY win32api.PostQuitMessage(0) return 0 } return win32api.DefWindowProc(hWnd, uMsg, wParam, lParam) } // 检查剪贴板是否真的有变化 func checkClipboard(ignoreMessage bool) { if atomic.LoadUint32(&isListening) != 1 { return } sequenceMux.Lock() defer sequenceMux.Unlock() // 获取当前剪贴板序列号 var currentSeq win32api.DWORD runInWorker(func() { currentSeq, _ = win32api.GetClipboardSequenceNumber() }) // 如果序列号没有变化,跳过 if !ignoreMessage && currentSeq == lastSequence { return } lastSequence = currentSeq // 尝试读取剪贴板,带重试机制 var cb Clipboard var err error maxRetries := 3 retryDelay := 10 * time.Millisecond for i := 0; i < maxRetries; i++ { runInWorker(func() { if !onlyMetaGlobal { cb, err = getClipboardWithRetry() } else { cb, err = getMetaWithRetry() } }) if err == nil { break } // 如果剪贴板被占用,稍等再试 if i < maxRetries-1 { time.Sleep(retryDelay) retryDelay *= 2 // 指数退避 } } if err == nil && clipboardChannel != nil { select { case clipboardChannel <- cb: default: // channel满了,移除最旧的 select { case <-clipboardChannel: clipboardChannel <- cb default: } } } } func handleClipboardUpdate() { if atomic.LoadUint32(&isListening) != 1 { return } var cb Clipboard var err error if !onlyMetaGlobal { cb, err = Get() } else { cb, err = GetMeta() } if err == nil && clipboardChannel != nil { select { case clipboardChannel <- cb: default: // channel满了,丢弃旧数据 } } } func Listen(onlyMeta bool) (<-chan Clipboard, error) { if atomic.LoadUint32(&isListening) != 0 { return nil, errors.New("Already listening") } atomic.StoreUint32(&isListening, 1) res := make(chan Clipboard, 10) // 增大缓冲区 clipboardChannel = res onlyMetaGlobal = onlyMeta stopSign = make(chan struct{}) // 初始化序列号 runInWorker(func() { lastSequence, _ = win32api.GetClipboardSequenceNumber() }) go func() { runtime.LockOSThread() defer runtime.UnlockOSThread() defer func() { if hwndGlobal != 0 { win32api.RemoveClipboardFormatListener(hwndGlobal) win32api.DestroyWindow(hwndGlobal) hwndGlobal = 0 } atomic.StoreUint32(&isListening, 0) close(res) clipboardChannel = nil }() hInstance, err := win32api.GetModuleHandle(nil) if err != nil { return } className := fmt.Sprintf("ClipListener_%d", time.Now().UnixNano()) var wc win32api.WNDCLASSEX wc.CbSize = win32api.UINT(unsafe.Sizeof(wc)) wc.Style = 0 wc.LpfnWndProc = syscall.NewCallback(clipboardWndProc) wc.CbClsExtra = 0 wc.CbWndExtra = 0 wc.HInstance = hInstance wc.HIcon = 0 wc.HCursor = 0 wc.HbrBackground = 0 wc.LpszMenuName = nil wc.LpszClassName = syscall.StringToUTF16Ptr(className) wc.HIconSm = 0 _, err = win32api.RegisterClassEx(&wc) if err != nil { return } hwnd, err := win32api.CreateWindowEx( 0, className, "", 0, 0, 0, 0, 0, 0, 0, hInstance, unsafe.Pointer(nil)) if err != nil { return } hwndGlobal = hwnd ok, err := win32api.AddClipboardFormatListener(hwnd) if err != nil || !ok { win32api.DestroyWindow(hwnd) return } go func() { ticker := time.NewTicker(5000 * time.Millisecond) defer ticker.Stop() for { select { case <-stopSign: return case <-ticker.C: // 定期检查,防止遗漏 go checkClipboard(false) } } }() // 监听停止信号 go func() { <-stopSign if hwndGlobal != 0 { win32api.PostMessage(hwndGlobal, WM_USER_STOP, 0, 0) } }() // 消息循环 for { var msg win32api.MSG ret, err := win32api.GetMessage(&msg, 0, 0, 0) if ret == 0 { // 收到WM_QUIT消息,正常退出 break } if ret == 0xFFFFFFFF { // GetMessage出错 if err != nil { fmt.Printf("GetMessage error: %v\n", err) } continue } win32api.TranslateMessage(&msg) win32api.DispatchMessage(&msg) } }() return res, nil } func StopListen() { if atomic.LoadUint32(&isListening) == 1 && stopSign != nil { close(stopSign) } } // 在工作线程中执行剪贴板操作 func runInWorker(fn func()) { done := make(chan struct{}) clipboardWorker <- func() { fn() close(done) } <-done }