125 lines
2.9 KiB
Go
125 lines
2.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package xorg
|
|
|
|
/*
|
|
#cgo pkg-config: x11 xfixes
|
|
#include <X11/Xlib.h>
|
|
#include <X11/extensions/Xfixes.h>
|
|
#include <unistd.h>
|
|
|
|
static int watch_clipboard(Display *display, int *event_base) {
|
|
int error_base;
|
|
if (!XFixesQueryExtension(display, event_base, &error_base)) return 0;
|
|
Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
|
XFixesSelectSelectionInput(display, DefaultRootWindow(display), clipboard,
|
|
XFixesSetSelectionOwnerNotifyMask);
|
|
XSync(display, False);
|
|
return 1;
|
|
}
|
|
|
|
static int next_clipboard_change(Display *display, int event_base) {
|
|
while (XPending(display)) {
|
|
XEvent event;
|
|
XNextEvent(display, &event);
|
|
if (event.type == event_base + XFixesSelectionNotify) {
|
|
XFixesSelectionNotifyEvent *change = (XFixesSelectionNotifyEvent *)&event;
|
|
if (change->owner != None) return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
*/
|
|
import "C"
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ClipboardMonitor reports successful X11 clipboard ownership changes. It
|
|
// never reads clipboard contents or blocks the keyboard listener.
|
|
type ClipboardMonitor struct {
|
|
changes chan struct{}
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
func NewClipboardMonitor() *ClipboardMonitor {
|
|
return &ClipboardMonitor{changes: make(chan struct{}, 16)}
|
|
}
|
|
|
|
func (monitor *ClipboardMonitor) Changes() <-chan struct{} { return monitor.changes }
|
|
|
|
func (monitor *ClipboardMonitor) Start(ctx context.Context) error {
|
|
monitor.mutex.Lock()
|
|
defer monitor.mutex.Unlock()
|
|
if monitor.done != nil {
|
|
return errors.New("clipboard monitor already started")
|
|
}
|
|
ready := make(chan error, 1)
|
|
watchCtx, cancel := context.WithCancel(ctx)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
display := C.XOpenDisplay(nil)
|
|
if display == nil {
|
|
ready <- errors.New("clipboard monitor: no X11 display")
|
|
return
|
|
}
|
|
defer C.XCloseDisplay(display)
|
|
var eventBase C.int
|
|
if C.watch_clipboard(display, &eventBase) == 0 {
|
|
ready <- errors.New("clipboard monitor: XFixes unavailable")
|
|
return
|
|
}
|
|
ready <- nil
|
|
ticker := time.NewTicker(20 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-watchCtx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if C.next_clipboard_change(display, eventBase) != 0 {
|
|
select {
|
|
case monitor.changes <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
if err := <-ready; err != nil {
|
|
cancel()
|
|
<-done
|
|
return err
|
|
}
|
|
monitor.cancel, monitor.done = cancel, done
|
|
return nil
|
|
}
|
|
|
|
func (monitor *ClipboardMonitor) Stop(ctx context.Context) error {
|
|
monitor.mutex.Lock()
|
|
cancel, done := monitor.cancel, monitor.done
|
|
monitor.mutex.Unlock()
|
|
if done == nil {
|
|
return nil
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|