2025-11-10 10:17:06 +08:00
|
|
|
|
//go:build windows
|
|
|
|
|
|
|
2024-03-27 11:20:59 +08:00
|
|
|
|
package clipboard
|
|
|
|
|
|
|
2024-03-30 15:07:20 +08:00
|
|
|
|
import (
|
|
|
|
|
|
"b612.me/win32api"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2025-11-10 10:17:06 +08:00
|
|
|
|
"runtime"
|
|
|
|
|
|
"sync"
|
2024-03-30 15:07:20 +08:00
|
|
|
|
"sync/atomic"
|
2025-11-10 10:17:06 +08:00
|
|
|
|
"syscall"
|
2024-03-30 15:07:20 +08:00
|
|
|
|
"time"
|
2025-11-10 10:17:06 +08:00
|
|
|
|
"unsafe"
|
2024-03-30 15:07:20 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 10:17:06 +08:00
|
|
|
|
|
2024-03-30 15:07:20 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 10:17:06 +08:00
|
|
|
|
|
2024-04-02 14:11:59 +08:00
|
|
|
|
func Listen(onlyMeta bool) (<-chan Clipboard, error) {
|
2024-03-30 15:07:20 +08:00
|
|
|
|
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 {
|
2024-04-02 14:11:59 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2024-03-30 15:07:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
return res, nil
|
|
|
|
|
|
}
|
2025-11-10 10:17:06 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|