Skip to content

Commit

Permalink
add: linux implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
d-tsuji committed May 10, 2020
1 parent 50440a3 commit 79e58a2
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 7 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/test.yaml
Expand Up @@ -13,12 +13,11 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Setup X server on ubuntu-latest
- name: Setup X on Linux
if: runner.os == 'Linux'
run: |
sudo apt update -y
sudo apt install -y xorg openbox
sudo startx
sudo apt install xvfb
- name: Setup Go
uses: actions/setup-go@v1
with:
Expand Down
3 changes: 0 additions & 3 deletions README.md
Expand Up @@ -15,9 +15,6 @@ This is a multi-platform clipboard library in Go.

- Windows
- macOS

### ⚠WIP⚠

- Linux, Unix (X11)

## Installation
Expand Down
2 changes: 1 addition & 1 deletion clipboard_test.go
Expand Up @@ -21,7 +21,7 @@ func TestCopyAndPaste(t *testing.T) {
t.Error(err)
}
if got != tt.want {
t.Errorf("CopyAndPaste mismatch: got %v, want %v", got, tt.want)
t.Errorf("copy and paste mismatch: got %v, want %v", got, tt.want)
}
})
}
Expand Down
230 changes: 230 additions & 0 deletions clipboard_unix.go
@@ -0,0 +1,230 @@
// 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
}
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
git.wow.st/gmp/clip v0.0.0-20191001134149-1458ba6a7cf5
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 // indirect
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
@@ -1,5 +1,7 @@
git.wow.st/gmp/clip v0.0.0-20191001134149-1458ba6a7cf5 h1:OKeTjZST+/TKvtdA258NXJH+/gIx/xwyZxKrAezNFvk=
git.wow.st/gmp/clip v0.0.0-20191001134149-1458ba6a7cf5/go.mod h1:NLdpaBoMQNFqncwP8OVRNWUDw1Kt9XWm3snfT7cXu24=
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843 h1:3iF31c7rp7nGZVDv7YQ+VxOgpipVfPKotLXykjZmwM8=
github.com/BurntSushi/xgb v0.0.0-20200324125942-20f126ea2843/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1 h1:/QwQcwWVOQXcoNuV9tHx30gQ3q7jCE/rKcGjwzsa5tg=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4 h1:5BmtGkQbch91lglMHQ9JIDGiYCL3kBRBA0ItZTvOcEI=
Expand Down

0 comments on commit 79e58a2

Please sign in to comment.