// 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 #include #include 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() } }