// 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 }