erm/vendor/github.com/d-tsuji/clipboard/clipboard_unix.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
}