clipboard_x11.go raw
1 // +build linux freebsd openbsd
2
3 package clipboard
4
5 import (
6 "fmt"
7 "os"
8 "time"
9
10 "github.com/BurntSushi/xgb"
11 "github.com/BurntSushi/xgb/xproto"
12 )
13
14 // todo: only X is required from this package, the rest runs off the built-in Gio clipboard
15
16 const debugClipboardRequests = false
17
18 var X *xgb.Conn
19 var win xproto.Window
20 var clipboardText string
21 var selnotify chan bool
22
23 var clipboardAtom, primaryAtom, textAtom, targetsAtom, atomAtom xproto.Atom
24 var targetAtoms []xproto.Atom
25 var clipboardAtomCache = map[xproto.Atom]string{}
26
27 var RunningX bool
28
29 // Start up the clipboard watcher
30 func Start() {
31 // first, check if X is running as only X has the Primary buffer
32 env := os.Environ()
33 for i := range env {
34 if env[i]=="XDG_SESSION_TYPE=x11" {
35 I.Ln("running X11, primary selection buffer enabled")
36 RunningX=true
37 break
38 }
39 }
40 if !RunningX {
41 return
42 }
43 var e error
44 X, e = xgb.NewConnDisplay("")
45 if e != nil {
46 panic(e)
47 }
48
49 selnotify = make(chan bool, 1)
50
51 win, e = xproto.NewWindowId(X)
52 if e != nil {
53 panic(e)
54 }
55
56 setup := xproto.Setup(X)
57 s := setup.DefaultScreen(X)
58 e = xproto.CreateWindowChecked(
59 X,
60 s.RootDepth,
61 win,
62 s.Root,
63 100,
64 100,
65 1,
66 1,
67 0,
68 xproto.WindowClassInputOutput,
69 s.RootVisual,
70 0,
71 []uint32{},
72 ).Check()
73 if e != nil {
74 panic(e)
75 }
76
77 clipboardAtom = internAtom(X, "CLIPBOARD")
78 primaryAtom = internAtom(X, "PRIMARY")
79 textAtom = internAtom(X, "UTF8_STRING")
80 targetsAtom = internAtom(X, "TARGETS")
81 atomAtom = internAtom(X, "ATOM")
82
83 targetAtoms = []xproto.Atom{targetsAtom, textAtom}
84
85 go eventLoop()
86 }
87
88 func Set(text string) (e error){
89 if !RunningX {
90 return
91 }
92 clipboardText = text
93 ssoc := xproto.SetSelectionOwnerChecked(X, win, clipboardAtom, xproto.TimeCurrentTime)
94 if e = ssoc.Check(); E.Chk(e) {
95 fmt.Fprintf(os.Stderr, "Error setting clipboard: %v", e)
96 }
97 ssoc = xproto.SetSelectionOwnerChecked(X, win, primaryAtom, xproto.TimeCurrentTime)
98 if e = ssoc.Check(); E.Chk(e) {
99 fmt.Fprintf(os.Stderr, "Error setting primary selection: %v", e)
100 }
101 return
102 }
103
104 func SetPrimary(text string) (e error){
105 if !RunningX {
106 return
107 }
108 clipboardText = text
109 ssoc := xproto.SetSelectionOwnerChecked(X, win, primaryAtom, xproto.TimeCurrentTime)
110 if e = ssoc.Check(); E.Chk(e) {
111 fmt.Fprintf(os.Stderr, "Error setting primary selection: %v", e)
112 }
113 return
114 }
115
116 func Get() string {
117 if !RunningX {
118 return ""
119 }
120 return getSelection(clipboardAtom)
121 }
122
123 func GetPrimary() string {
124 if !RunningX {
125 return ""
126 }
127 return getSelection(primaryAtom)
128 }
129
130 func getSelection(selAtom xproto.Atom) string {
131 csc := xproto.ConvertSelectionChecked(X, win, selAtom, textAtom, selAtom, xproto.TimeCurrentTime)
132 e := csc.Check()
133 if e != nil {
134 fmt.Fprintln(os.Stderr, e)
135 return ""
136 }
137
138 select {
139 case r := <-selnotify:
140 if !r {
141 return ""
142 }
143 gpc := xproto.GetProperty(X, true, win, selAtom, textAtom, 0, 5*1024*1024)
144 gpr, e := gpc.Reply()
145 if e != nil {
146 fmt.Fprintln(os.Stderr, e)
147 return ""
148 }
149 if gpr.BytesAfter != 0 {
150 fmt.Fprintln(os.Stderr, "Clipboard too large")
151 return ""
152 }
153 return string(gpr.Value[:gpr.ValueLen])
154 case <-time.After(1 * time.Second):
155 fmt.Fprintln(os.Stderr, "Clipboard retrieval failed, timeout")
156 return ""
157 }
158 }
159
160 func eventLoop() {
161 for {
162 ev, e := X.WaitForEvent()
163 if e != nil {
164 continue
165 }
166 switch evt := ev.(type) {
167 case xproto.SelectionRequestEvent:
168 if debugClipboardRequests {
169 tgtname := lookupAtom(evt.Target)
170 fmt.Fprintln(
171 os.Stderr,
172 "SelectionRequest",
173 ev,
174 textAtom,
175 tgtname,
176 "isPrimary:",
177 evt.Selection == primaryAtom,
178 "isClipboard:",
179 evt.Selection == clipboardAtom,
180 )
181 }
182 t := clipboardText
183
184 switch evt.Target {
185 case textAtom:
186 if debugClipboardRequests {
187 fmt.Fprintln(os.Stderr, "Sending as text")
188 }
189 cpc := xproto.ChangePropertyChecked(
190 X,
191 xproto.PropModeReplace,
192 evt.Requestor,
193 evt.Property,
194 textAtom,
195 8,
196 uint32(len(t)),
197 []byte(t),
198 )
199 e := cpc.Check()
200 if e == nil {
201 sendSelectionNotify(evt)
202 } else {
203 fmt.Fprintln(os.Stderr, e)
204 }
205
206 case targetsAtom:
207 if debugClipboardRequests {
208 fmt.Fprintln(os.Stderr, "Sending targets")
209 }
210 buf := make([]byte, len(targetAtoms)*4)
211 for i, atom := range targetAtoms {
212 xgb.Put32(buf[i*4:], uint32(atom))
213 }
214
215 _ = xproto.ChangePropertyChecked(
216 X,
217 xproto.PropModeReplace,
218 evt.Requestor,
219 evt.Property,
220 atomAtom,
221 32,
222 uint32(len(targetAtoms)),
223 buf,
224 ).Check()
225 if e == nil {
226 sendSelectionNotify(evt)
227 } else {
228 fmt.Fprintln(os.Stderr, e)
229 }
230
231 default:
232 if debugClipboardRequests {
233 fmt.Fprintln(os.Stderr, "Skipping")
234 }
235 evt.Property = 0
236 sendSelectionNotify(evt)
237 }
238
239 case xproto.SelectionNotifyEvent:
240 selnotify <- (evt.Property == clipboardAtom) || (evt.Property == primaryAtom)
241 }
242 }
243 }
244
245 func lookupAtom(at xproto.Atom) string {
246 if s, ok := clipboardAtomCache[at]; ok {
247 return s
248 }
249
250 reply, e := xproto.GetAtomName(X, at).Reply()
251 if e != nil {
252 panic(e)
253 }
254
255 // If we're here, it means we didn't have the ATOM id cached. So cache it.
256 atomName := string(reply.Name)
257 clipboardAtomCache[at] = atomName
258 return atomName
259 }
260
261 func sendSelectionNotify(ev xproto.SelectionRequestEvent) {
262 sn := xproto.SelectionNotifyEvent{
263 Time: xproto.TimeCurrentTime,
264 Requestor: ev.Requestor,
265 Selection: ev.Selection,
266 Target: ev.Target,
267 Property: ev.Property,
268 }
269 var e error
270 sec := xproto.SendEventChecked(X, false, ev.Requestor, 0, string(sn.Bytes()))
271 if e = sec.Check(); E.Chk(e) {
272 fmt.Fprintln(os.Stderr, e)
273 }
274 }
275
276 func internAtom(conn *xgb.Conn, n string) xproto.Atom {
277 iac := xproto.InternAtom(conn, true, uint16(len(n)), n)
278 iar, e := iac.Reply()
279 if e != nil {
280 panic(e)
281 }
282 return iar.Atom
283 }
284