-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathclipboard_x11.go
More file actions
387 lines (326 loc) · 9.54 KB
/
Copy pathclipboard_x11.go
File metadata and controls
387 lines (326 loc) · 9.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
// Copyright 2025 Ayman Bagabas
// SPDX-License-Identifier: MIT
//go:build (linux || freebsd) && !android
package nativeclipboard
import (
"bytes"
"context"
"fmt"
"runtime"
"time"
"unsafe"
"github.com/ebitengine/purego"
)
// X11 types
type (
Display uintptr
Window uintptr
Atom uintptr
Time uintptr
Bool int
)
// X11 constants
const (
None = 0
CurrentTime = 0
AnyPropertyType = 0
PropModeReplace = 0
Success = 0
SelectionNotify = 31
SelectionClear = 29
SelectionRequest = 30
)
// XEvent is a union in C, we need the largest variant
type XEvent struct {
typ int32
pad [23]uintptr // Ensure it's large enough for all event types
}
type XSelectionEvent struct {
typ int32
_ [3]byte // padding
serial uintptr
send_event Bool
display Display
requestor Window
selection Atom
target Atom
property Atom
time Time
}
type XSelectionRequestEvent struct {
typ int32
_ [3]byte
serial uintptr
send_event Bool
display Display
owner Window
requestor Window
selection Atom
target Atom
property Atom
time Time
}
// X11 function pointers
var (
libX11 uintptr
xOpenDisplay func(display_name uintptr) Display
xCloseDisplay func(display Display)
xDefaultRootWindow func(display Display) Window
xCreateSimpleWindow func(display Display, parent Window, x, y int, width, height, border_width uint, border, background uintptr) Window
xInternAtom func(display Display, atom_name string, only_if_exists Bool) Atom
xSetSelectionOwner func(display Display, selection Atom, owner Window, time Time)
xGetSelectionOwner func(display Display, selection Atom) Window
xNextEvent func(display Display, event *XEvent)
xChangeProperty func(display Display, w Window, property Atom, typ Atom, Format int, mode int, data *byte, nelements int) int
xSendEvent func(display Display, w Window, propagate Bool, event_mask int64, event *XEvent)
xGetWindowProperty func(display Display, w Window, property Atom, long_offset, long_length int64, delete Bool, req_type Atom, actual_type_return *Atom, actual_format_return *int, nitems_return *uint64, bytes_after_return *uint64, prop_return **byte) int
xFree func(data unsafe.Pointer)
xDeleteProperty func(display Display, w Window, property Atom)
xConvertSelection func(display Display, selection Atom, target Atom, property Atom, requestor Window, time Time)
)
var helpmsg = `%w: Failed to initialize the X11 display, and the clipboard package
will not work properly. Install the following dependency may help:
# Debian/Ubuntu
apt install -y libx11-dev
# Fedora/RHEL
dnf install -y libX11-devel
# FreeBSD
pkg install xorg-libraries
If the clipboard package is in an environment without a frame buffer,
such as a cloud server, it may also be necessary to install xvfb and
initialize a virtual frame buffer:
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
export DISPLAY=:99.0
Then this package should be ready to use.
`
func initialize() error {
var err error
// Try common library paths for libX11
// Linux systems: libX11.so.6, libX11.so
// FreeBSD often has X11 in /usr/local/lib or /usr/X11R6/lib
libPaths := []string{
"libX11.so.6", // versioned library (Linux, some BSD)
"libX11.so", // generic library (Linux, BSD)
"/usr/local/lib/libX11.so.6", // FreeBSD, OpenBSD
"/usr/local/lib/libX11.so",
"/usr/X11R6/lib/libX11.so.6", // Older BSD systems
"/usr/X11R6/lib/libX11.so",
}
for _, path := range libPaths {
libX11, err = purego.Dlopen(path, purego.RTLD_LAZY|purego.RTLD_GLOBAL)
if err == nil {
break
}
}
if err != nil {
return fmt.Errorf(helpmsg, ErrUnavailable)
}
// Load all X11 functions
purego.RegisterLibFunc(&xOpenDisplay, libX11, "XOpenDisplay")
purego.RegisterLibFunc(&xCloseDisplay, libX11, "XCloseDisplay")
purego.RegisterLibFunc(&xDefaultRootWindow, libX11, "XDefaultRootWindow")
purego.RegisterLibFunc(&xCreateSimpleWindow, libX11, "XCreateSimpleWindow")
purego.RegisterLibFunc(&xInternAtom, libX11, "XInternAtom")
purego.RegisterLibFunc(&xSetSelectionOwner, libX11, "XSetSelectionOwner")
purego.RegisterLibFunc(&xGetSelectionOwner, libX11, "XGetSelectionOwner")
purego.RegisterLibFunc(&xNextEvent, libX11, "XNextEvent")
purego.RegisterLibFunc(&xChangeProperty, libX11, "XChangeProperty")
purego.RegisterLibFunc(&xSendEvent, libX11, "XSendEvent")
purego.RegisterLibFunc(&xGetWindowProperty, libX11, "XGetWindowProperty")
purego.RegisterLibFunc(&xFree, libX11, "XFree")
purego.RegisterLibFunc(&xDeleteProperty, libX11, "XDeleteProperty")
purego.RegisterLibFunc(&xConvertSelection, libX11, "XConvertSelection")
// Test if we can open display
var display Display
for i := 0; i < 42; i++ {
display = xOpenDisplay(0)
if display != 0 {
break
}
}
if display == 0 {
return fmt.Errorf(helpmsg, ErrUnavailable)
}
xCloseDisplay(display)
return nil
}
func read(t Format) ([]byte, error) {
var atomType string
switch t {
case Text:
atomType = "UTF8_STRING"
case Image:
atomType = "image/png"
default:
return nil, ErrUnsupported
}
return readX11(atomType)
}
func readX11(atomType string) ([]byte, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var display Display
for i := 0; i < 42; i++ {
display = xOpenDisplay(0)
if display != 0 {
break
}
}
if display == 0 {
return nil, ErrUnavailable
}
defer xCloseDisplay(display)
root := xDefaultRootWindow(display)
window := xCreateSimpleWindow(display, root, 0, 0, 1, 1, 0, 0, 0)
sel := xInternAtom(display, "CLIPBOARD", 0)
prop := xInternAtom(display, "GOLANG_DESIGN_DATA", 0)
target := xInternAtom(display, atomType, 1)
if target == None {
return nil, ErrUnsupported
}
xConvertSelection(display, sel, target, prop, window, CurrentTime)
var event XEvent
for {
xNextEvent(display, &event)
if event.typ != SelectionNotify {
continue
}
break
}
// Cast event to XSelectionEvent
sev := (*XSelectionEvent)(unsafe.Pointer(&event))
if sev.property == None || sev.selection != sel || sev.property != prop {
return nil, ErrUnavailable
}
var actual Atom
var Format int
var nitems, bytesAfter uint64
var data *byte
ret := xGetWindowProperty(sev.display, sev.requestor, sev.property,
0, ^int64(0), 0, AnyPropertyType,
&actual, &Format, &nitems, &bytesAfter, &data)
if ret != Success || data == nil {
return nil, ErrUnavailable
}
defer xFree(unsafe.Pointer(data))
if nitems == 0 {
return nil, nil
}
// Copy data to Go slice
result := make([]byte, nitems)
dataSlice := unsafe.Slice(data, nitems)
copy(result, dataSlice)
xDeleteProperty(sev.display, sev.requestor, sev.property)
return result, nil
}
func write(t Format, buf []byte) (<-chan struct{}, error) {
var atomType string
switch t {
case Text:
atomType = "UTF8_STRING"
case Image:
atomType = "image/png"
default:
return nil, ErrUnsupported
}
done := make(chan struct{}, 1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var display Display
for i := 0; i < 42; i++ {
display = xOpenDisplay(0)
if display != 0 {
break
}
}
if display == 0 {
close(done)
return
}
defer xCloseDisplay(display)
root := xDefaultRootWindow(display)
window := xCreateSimpleWindow(display, root, 0, 0, 1, 1, 0, 0, 0)
sel := xInternAtom(display, "CLIPBOARD", 0)
atomString := xInternAtom(display, "UTF8_STRING", 0)
atomImage := xInternAtom(display, "image/png", 0)
targetsAtom := xInternAtom(display, "TARGETS", 0)
xaAtom := xInternAtom(display, "ATOM", 0)
target := xInternAtom(display, atomType, 1)
if target == None {
close(done)
return
}
xSetSelectionOwner(display, sel, window, CurrentTime)
if xGetSelectionOwner(display, sel) != window {
close(done)
return
}
var event XEvent
for {
xNextEvent(display, &event)
switch event.typ {
case SelectionClear:
close(done)
return
case SelectionRequest:
req := (*XSelectionRequestEvent)(unsafe.Pointer(&event))
if req.selection != sel {
continue
}
var selEvent XSelectionEvent
selEvent.typ = SelectionNotify
selEvent.display = req.display
selEvent.requestor = req.requestor
selEvent.selection = req.selection
selEvent.target = req.target
selEvent.property = req.property
selEvent.time = req.time
if req.target == atomString && target == atomString {
if len(buf) > 0 {
xChangeProperty(selEvent.display, selEvent.requestor, selEvent.property,
atomString, 8, PropModeReplace, &buf[0], len(buf))
}
} else if req.target == atomImage && target == atomImage {
if len(buf) > 0 {
xChangeProperty(selEvent.display, selEvent.requestor, selEvent.property,
atomImage, 8, PropModeReplace, &buf[0], len(buf))
}
} else if req.target == targetsAtom {
targets := []Atom{atomString, atomImage}
xChangeProperty(selEvent.display, selEvent.requestor, selEvent.property,
xaAtom, 32, PropModeReplace, (*byte)(unsafe.Pointer(&targets[0])), len(targets))
} else {
selEvent.property = None
}
xSendEvent(selEvent.display, selEvent.requestor, 0, 0, (*XEvent)(unsafe.Pointer(&selEvent)))
}
}
}()
return done, nil
}
func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
ticker := time.NewTicker(time.Second)
last, _ := read(t)
go func() {
defer ticker.Stop()
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ticker.C:
b, _ := read(t)
if b == nil {
continue
}
if !bytes.Equal(last, b) {
recv <- b
last = b
}
}
}
}()
return recv
}