Files

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()
}
}