231 lines
5.5 KiB
Go
231 lines
5.5 KiB
Go
// The MIT License (MIT)
|
|
// Copyright (c) 2016 Alessandro Arzilli
|
|
// https://github.com/aarzilli/nucular/blob/master/LICENSE
|
|
|
|
// +build freebsd linux netbsd openbsd solaris dragonfly
|
|
|
|
package clipboard
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/xgb"
|
|
"github.com/BurntSushi/xgb/xproto"
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
const debugClipboardRequests = false
|
|
|
|
var (
|
|
x *xgb.Conn
|
|
win xproto.Window
|
|
clipboardText string
|
|
selnotify chan bool
|
|
|
|
clipboardAtom, primaryAtom, textAtom, targetsAtom, atomAtom xproto.Atom
|
|
targetAtoms []xproto.Atom
|
|
clipboardAtomCache = map[xproto.Atom]string{}
|
|
|
|
doneCh = make(chan interface{}, 1)
|
|
)
|
|
|
|
func start() error {
|
|
var err error
|
|
xServer := os.Getenv("DISPLAY")
|
|
if xServer == "" {
|
|
return xerrors.New("could not identify xserver")
|
|
}
|
|
x, err = xgb.NewConnDisplay(xServer)
|
|
if err != nil {
|
|
return xerrors.Errorf("%w", err)
|
|
}
|
|
|
|
selnotify = make(chan bool, 1)
|
|
|
|
win, err = xproto.NewWindowId(x)
|
|
if err != nil {
|
|
return xerrors.Errorf("%w", err)
|
|
}
|
|
|
|
setup := xproto.Setup(x)
|
|
s := setup.DefaultScreen(x)
|
|
err = xproto.CreateWindowChecked(x, s.RootDepth, win, s.Root, 100, 100, 1, 1, 0, xproto.WindowClassInputOutput, s.RootVisual, 0, []uint32{}).Check()
|
|
if err != nil {
|
|
return xerrors.Errorf("%w", err)
|
|
}
|
|
|
|
clipboardAtom = internAtom(x, "CLIPBOARD")
|
|
primaryAtom = internAtom(x, "PRIMARY")
|
|
textAtom = internAtom(x, "UTF8_STRING")
|
|
targetsAtom = internAtom(x, "TARGETS")
|
|
atomAtom = internAtom(x, "ATOM")
|
|
|
|
targetAtoms = []xproto.Atom{targetsAtom, textAtom}
|
|
|
|
go eventLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
func set(text string) error {
|
|
if err := start(); err != nil {
|
|
return xerrors.Errorf("init clipboard: %w", err)
|
|
}
|
|
clipboardText = text
|
|
ssoc := xproto.SetSelectionOwnerChecked(x, win, clipboardAtom, xproto.TimeCurrentTime)
|
|
if err := ssoc.Check(); err != nil {
|
|
return xerrors.Errorf("setting clipboard: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func get() (string, error) {
|
|
if err := start(); err != nil {
|
|
return "", xerrors.Errorf("init clipboard: %w", err)
|
|
}
|
|
return getSelection(clipboardAtom)
|
|
}
|
|
|
|
func getSelection(selAtom xproto.Atom) (string, error) {
|
|
csc := xproto.ConvertSelectionChecked(x, win, selAtom, textAtom, selAtom, xproto.TimeCurrentTime)
|
|
err := csc.Check()
|
|
if err != nil {
|
|
return "", xerrors.Errorf("convert selection check: %w", err)
|
|
}
|
|
|
|
select {
|
|
case r := <-selnotify:
|
|
if !r {
|
|
return "", nil
|
|
}
|
|
gpc := xproto.GetProperty(x, true, win, selAtom, textAtom, 0, 5*1024*1024)
|
|
gpr, err := gpc.Reply()
|
|
if err != nil {
|
|
return "", xerrors.Errorf("grp reply: %w", err)
|
|
}
|
|
if gpr.BytesAfter != 0 {
|
|
return "", xerrors.New("clipboard too large")
|
|
}
|
|
return string(gpr.Value[:gpr.ValueLen]), nil
|
|
case <-time.After(1 * time.Second):
|
|
return "", xerrors.New("clipboard retrieval failed, timeout")
|
|
}
|
|
}
|
|
|
|
func pollForEvent(X *xgb.Conn, events chan<- xgb.Event) {
|
|
for {
|
|
select {
|
|
case <-doneCh:
|
|
return
|
|
default:
|
|
ev, err := X.PollForEvent()
|
|
if err != nil {
|
|
fmt.Println("wait for event:", err)
|
|
}
|
|
events <- ev
|
|
}
|
|
}
|
|
}
|
|
|
|
func eventLoop() {
|
|
eventCh := make(chan xgb.Event, 1)
|
|
go pollForEvent(x, eventCh)
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
switch e := event.(type) {
|
|
case xproto.SelectionRequestEvent:
|
|
if debugClipboardRequests {
|
|
tgtname := lookupAtom(e.Target)
|
|
propname := lookupAtom(e.Property)
|
|
fmt.Println("SelectionRequest", e, textAtom, tgtname, propname, "isPrimary:", e.Selection == primaryAtom, "isClipboard:", e.Selection == clipboardAtom)
|
|
}
|
|
t := clipboardText
|
|
|
|
switch e.Target {
|
|
case textAtom:
|
|
if debugClipboardRequests {
|
|
fmt.Println("Sending as text")
|
|
}
|
|
cpc := xproto.ChangePropertyChecked(x, xproto.PropModeReplace, e.Requestor, e.Property, textAtom, 8, uint32(len(t)), []byte(t))
|
|
err := cpc.Check()
|
|
if err == nil {
|
|
sendSelectionNotify(e)
|
|
} else {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
case targetsAtom:
|
|
if debugClipboardRequests {
|
|
fmt.Println("Sending targets")
|
|
}
|
|
buf := make([]byte, len(targetAtoms)*4)
|
|
for i, atom := range targetAtoms {
|
|
xgb.Put32(buf[i*4:], uint32(atom))
|
|
}
|
|
|
|
err := xproto.ChangePropertyChecked(x, xproto.PropModeReplace, e.Requestor, e.Property, atomAtom, 32, uint32(len(targetAtoms)), buf).Check()
|
|
if err == nil {
|
|
sendSelectionNotify(e)
|
|
} else {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
default:
|
|
if debugClipboardRequests {
|
|
fmt.Println("Skipping")
|
|
}
|
|
e.Property = 0
|
|
sendSelectionNotify(e)
|
|
}
|
|
|
|
case xproto.SelectionNotifyEvent:
|
|
selnotify <- (e.Property == clipboardAtom) || (e.Property == primaryAtom)
|
|
}
|
|
case <-doneCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func lookupAtom(at xproto.Atom) string {
|
|
if s, ok := clipboardAtomCache[at]; ok {
|
|
return s
|
|
}
|
|
|
|
reply, err := xproto.GetAtomName(x, at).Reply()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// If we're here, it means we didn't have ths ATOM id cached. So cache it.
|
|
atomName := string(reply.Name)
|
|
clipboardAtomCache[at] = atomName
|
|
return atomName
|
|
}
|
|
|
|
func sendSelectionNotify(e xproto.SelectionRequestEvent) {
|
|
sn := xproto.SelectionNotifyEvent{
|
|
Time: e.Time,
|
|
Requestor: e.Requestor,
|
|
Selection: e.Selection,
|
|
Target: e.Target,
|
|
Property: e.Property}
|
|
sec := xproto.SendEventChecked(x, false, e.Requestor, 0, string(sn.Bytes()))
|
|
err := sec.Check()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
}
|
|
|
|
func internAtom(conn *xgb.Conn, n string) xproto.Atom {
|
|
iac := xproto.InternAtom(conn, true, uint16(len(n)), n)
|
|
iar, err := iac.Reply()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return iar.Atom
|
|
}
|