vxx2 / vlib / clipboard / x11 / clipboard.c.v
529 lines · 458 sloc · 13.97 KB · 106e6ec1141d9a4382a13e40641aadee6313c717
Raw
1// Currently there is only X11 Selections support and no way to handle Wayland
2// but since Wayland isn't extremely adopted, we are covering almost all Linux distros.
3module x11
4
5import time
6import sync
7
8$if freebsd {
9 #flag -I/usr/local/include
10 #flag -L/usr/local/lib
11} $else $if openbsd {
12 #flag -I/usr/X11R6/include
13 #flag -L/usr/X11R6/lib
14}
15#flag -lX11
16
17// Include X11 headers BEFORE any type definitions to avoid incomplete type errors
18#preinclude <X11/Xlib.h>
19#preinclude <X11/Xatom.h>
20#preinclude <X11/Xutil.h>
21
22// X11 types are defined by X11 headers
23// For V check mode, we need to define C.Display
24@[typedef]
25pub struct C.Display {
26pub mut:
27 _ int // opaque - actual definition from X11 headers
28}
29
30// Local type aliases for convenience
31type Window = u64
32type Atom = u64
33type Time = u64
34
35// Forward declarations for V type checking (actual types from X11 headers)
36@[typedef]
37pub struct C.XSelectionEvent {
38pub mut:
39 type int
40 display voidptr
41 requestor Window
42 selection Atom
43 target Atom
44 property Atom
45 time int
46}
47
48@[typedef]
49pub struct C.XSelectionClearEvent {
50pub mut:
51 window Window
52 selection Atom
53}
54
55@[typedef]
56pub struct C.XSelectionRequestEvent {
57pub mut:
58 display voidptr
59 owner Window
60 requestor Window
61 selection Atom
62 target Atom
63 property Atom
64 time int
65}
66
67@[typedef]
68pub struct C.XDestroyWindowEvent {
69pub mut:
70 window Window
71}
72
73fn C.XInitThreads() i32
74
75fn C.XCloseDisplay(d &C.Display)
76
77fn C.XFlush(d &C.Display)
78
79fn C.XDestroyWindow(d &C.Display, w Window)
80
81fn C.XNextEvent(d &C.Display, e &C.XEvent)
82
83fn C.XSetSelectionOwner(d &C.Display, a Atom, w Window, time Time)
84
85fn C.XGetSelectionOwner(d &C.Display, a Atom) Window
86
87fn C.XChangeProperty(d &C.Display, requestor Window, property Atom, typ Atom, format i32, mode i32, data voidptr,
88 nelements i32) i32
89
90fn C.XSendEvent(d &C.Display, requestor Window, propagate i32, mask i64, event &C.XEvent)
91
92fn C.XInternAtom(d &C.Display, typ &u8, only_if_exists i32) Atom
93
94fn C.XCreateSimpleWindow(d &C.Display, root Window, x i32, y i32, width u32, height u32, border_width u32,
95 border u64, background u64) Window
96
97fn C.XOpenDisplay(name &u8) &C.Display
98
99fn C.XConvertSelection(d &C.Display, selection Atom, target Atom, property Atom, requestor Window, time Time) i32
100
101fn C.XSync(d &C.Display, discard i32) i32
102
103fn C.XGetWindowProperty(d &C.Display, w Window, property Atom, offset i64, length i64, delete i32, req_type Atom,
104 actual_type_return &Atom, actual_format_return &int, nitems &u64, bytes_after_return &u64, prop_return &&u8) i32
105
106fn C.XDeleteProperty(d &C.Display, w Window, property Atom) i32
107
108fn C.DefaultScreen(display &C.Display) i32
109
110fn C.RootWindow(display &C.Display, screen_number i32) Window
111
112fn C.BlackPixel(display &C.Display, screen_number i32) u32
113
114fn C.WhitePixel(display &C.Display, screen_number i32) u32
115
116fn C.XFree(data voidptr)
117
118// X11 event type constants
119pub const C.DestroyNotify int
120pub const C.SelectionClear int
121pub const C.SelectionRequest int
122pub const C.SelectionNotify int
123pub const C.PropertyNotify int // X11 property/selection constants
124
125pub const C.CurrentTime int
126pub const C.PropModeReplace int
127pub const C.PropertyChangeMask i64
128
129fn todo_del() {}
130
131// X11 event types are defined in vlib/x/x11/x11.v
132// XEvent union is defined here since it's used by clipboard
133
134@[typedef]
135union C.XEvent {
136pub mut:
137 type int
138 xdestroywindow C.XDestroyWindowEvent
139 xselectionclear C.XSelectionClearEvent
140 xselectionrequest C.XSelectionRequestEvent
141 xselection C.XSelectionEvent
142}
143
144const atom_names = ['TARGETS', 'CLIPBOARD', 'PRIMARY', 'SECONDARY', 'TEXT', 'UTF8_STRING',
145 'text/plain', 'text/html']
146const atom_types = [AtomType.targets, .clipboard, .primary, .secondary, .text, .utf8_string,
147 .text_plain, .text_html]
148
149// UNSUPPORTED TYPES: MULTIPLE, INCR, TIMESTAMP, image/bmp, image/jpeg, image/tiff, image/png
150// all the atom types we need
151// currently we only support text
152// in the future, maybe we can extend this
153// to support other mime types
154enum AtomType {
155 xa_atom = 0 // value 4
156 xa_string = 1 // value 31
157 targets = 2
158 clipboard = 3
159 primary = 4
160 secondary = 5
161 text = 6
162 utf8_string = 7
163 text_plain = 8
164 text_html = 9
165}
166
167@[heap]
168pub struct Clipboard {
169 display &C.Display = unsafe { nil }
170mut:
171 selection Atom // the selection atom
172 window Window
173 atoms []Atom
174 mutex &sync.Mutex = sync.new_mutex()
175 text string // text data sent or received
176 got_text bool // used to confirm that we have got the text
177 is_owner bool // to save selection owner state
178}
179
180struct Property {
181 actual_type Atom
182 actual_format int
183 nitems u64
184 data &u8 = unsafe { nil }
185}
186
187// new_clipboard returns a new `Clipboard` instance allocated on the heap.
188// The `Clipboard` resources can be released with `free()`
189pub fn new_clipboard() &Clipboard {
190 return new_x11_clipboard(.clipboard)
191}
192
193// new_x11_clipboard initializes a new clipboard of the given selection type.
194// Multiple clipboard instance types can be initialized and used separately.
195fn new_x11_clipboard(selection AtomType) &Clipboard {
196 if selection !in [.clipboard, .primary, .secondary] {
197 panic('Wrong AtomType. Must be one of .primary, .secondary or .clipboard.')
198 }
199 // init x11 thread support
200 status := C.XInitThreads()
201 if status == 0 {
202 println('WARN: this system does not support threads; clipboard will cause the program to lock.')
203 }
204
205 display := new_display()
206
207 if display == C.NULL {
208 println('ERROR: No X Server running. Clipboard cannot be used.')
209 return &Clipboard{
210 display: unsafe { nil }
211 mutex: sync.new_mutex()
212 }
213 }
214
215 mut cb := &Clipboard{
216 display: display
217 window: create_xwindow(display)
218 mutex: sync.new_mutex()
219 }
220 cb.intern_atoms()
221 cb.selection = cb.get_atom(selection)
222 // start the listener on another thread or
223 // we will be locked and will have to hard exit
224 spawn cb.start_listener()
225 return cb
226}
227
228// check_availability returns `true` if the clipboard is available for use.
229pub fn (cb &Clipboard) check_availability() bool {
230 return cb.display != C.NULL
231}
232
233// free releases the clipboard resources.
234pub fn (mut cb Clipboard) free() {
235 C.XDestroyWindow(cb.display, cb.window)
236 cb.window = Window(0)
237 // FIXME: program hangs when closing display
238 // XCloseDisplay(cb.display)
239}
240
241// clear clears the clipboard (sets it to an empty string).
242pub fn (mut cb Clipboard) clear() {
243 cb.mutex.lock()
244 C.XSetSelectionOwner(cb.display, cb.selection, Window(0), Time(C.CurrentTime))
245 C.XFlush(cb.display)
246 cb.is_owner = false
247 cb.text = ''
248 cb.mutex.unlock()
249}
250
251// has_ownership returns `true` if the `Clipboard` has the content ownership.
252pub fn (cb &Clipboard) has_ownership() bool {
253 return cb.is_owner
254}
255
256fn (cb &Clipboard) take_ownership() {
257 C.XSetSelectionOwner(cb.display, cb.selection, cb.window, Time(C.CurrentTime))
258 C.XFlush(cb.display)
259}
260
261// set_text stores `text` in the system clipboard.
262pub fn (mut cb Clipboard) set_text(text string) bool {
263 if cb.window == Window(0) {
264 return false
265 }
266 cb.mutex.lock()
267 cb.text = text
268 cb.is_owner = true
269 cb.take_ownership()
270 C.XFlush(cb.display)
271 cb.mutex.unlock()
272 // sleep a little bit
273 time.sleep(1 * time.millisecond)
274 return cb.is_owner
275}
276
277// get_text returns the current entry as a `string` from the clipboard.
278pub fn (mut cb Clipboard) get_text() string {
279 if cb.window == Window(0) {
280 return ''
281 }
282 if cb.is_owner {
283 return cb.text
284 }
285 cb.got_text = false
286
287 // Request a list of possible conversions, if we're pasting.
288 C.XConvertSelection(cb.display, cb.selection, cb.get_atom(.targets), cb.selection, cb.window,
289 Time(C.CurrentTime))
290
291 // wait for the text to arrive
292 mut retries := 5
293 for {
294 if cb.got_text || retries == 0 {
295 break
296 }
297 time.sleep(50 * time.millisecond)
298 retries--
299 }
300 return cb.text
301}
302
303// transmit_selection is crucial to handling all the different data types.
304// If we ever support other mimetypes they should be handled here.
305fn (mut cb Clipboard) transmit_selection(xse &C.XSelectionEvent) bool {
306 unsafe {
307 if xse.target == cb.get_atom(.targets) {
308 targets := cb.get_supported_targets()
309 C.XChangeProperty(xse.display, xse.requestor, xse.property, cb.get_atom(.xa_atom), 32,
310 C.PropModeReplace, targets.data, targets.len)
311 } else if cb.is_supported_target(xse.target) && cb.is_owner && cb.text != '' {
312 cb.mutex.lock()
313 C.XChangeProperty(xse.display, xse.requestor, xse.property, xse.target, 8,
314 C.PropModeReplace, cb.text.str, cb.text.len)
315 cb.mutex.unlock()
316 } else {
317 return false
318 }
319 }
320 return true
321}
322
323fn (mut cb Clipboard) start_listener() {
324 event := C.XEvent{}
325 mut sent_request := false
326 mut to_be_requested := Atom(0)
327 for {
328 time.sleep(1 * time.millisecond)
329 C.XNextEvent(cb.display, &event)
330 if unsafe { event.type == 0 } {
331 println('error')
332 continue
333 }
334 match unsafe { event.type } {
335 C.DestroyNotify {
336 if unsafe { event.xdestroywindow.window == cb.window } {
337 // we are done
338 return
339 }
340 }
341 C.SelectionClear {
342 unsafe {
343 if event.xselectionclear.window == cb.window
344 && event.xselectionclear.selection == cb.selection {
345 cb.mutex.lock()
346 cb.is_owner = false
347 cb.text = ''
348 cb.mutex.unlock()
349 }
350 }
351 }
352 C.SelectionRequest {
353 unsafe {
354 if event.xselectionrequest.selection == cb.selection {
355 xsre := &event.xselectionrequest
356
357 xse := C.XSelectionEvent{
358 type: C.SelectionNotify // 31
359 display: xsre.display
360 requestor: xsre.requestor
361 selection: xsre.selection
362 time: xsre.time
363 target: xsre.target
364 property: xsre.property
365 }
366 if !cb.transmit_selection(&xse) {
367 xse.property = Atom(0)
368 }
369 C.XSendEvent(cb.display, xse.requestor, 0, C.PropertyChangeMask,
370 voidptr(&xse))
371 C.XFlush(cb.display)
372 }
373 }
374 }
375 C.SelectionNotify {
376 unsafe {
377 if event.xselection.selection == cb.selection
378 && event.xselection.property != Atom(0) {
379 if event.xselection.target == cb.get_atom(.targets) && !sent_request {
380 sent_request = true
381 prop := read_property(cb.display, cb.window, cb.selection)
382 to_be_requested = cb.pick_target(prop)
383 if to_be_requested != Atom(0) {
384 C.XConvertSelection(cb.display, cb.selection, to_be_requested,
385 cb.selection, cb.window, Time(C.CurrentTime))
386 }
387 } else if event.xselection.target == to_be_requested {
388 sent_request = false
389 to_be_requested = Atom(0)
390 cb.mutex.lock()
391 prop := read_property(event.xselection.display,
392 event.xselection.requestor, event.xselection.property)
393 C.XDeleteProperty(event.xselection.display, event.xselection.requestor,
394 event.xselection.property)
395 if cb.is_supported_target(prop.actual_type) {
396 cb.got_text = true
397 cb.text =
398 prop.data.vstring() // TODO: return byteptr to support other mimetypes
399 }
400 cb.mutex.unlock()
401 }
402 }
403 }
404 }
405 C.PropertyNotify {}
406 else {}
407 }
408 }
409}
410
411/*
412* Helpers
413*/
414// intern_atoms initializes all the atoms we need.
415fn (mut cb Clipboard) intern_atoms() {
416 cb.atoms << Atom(4) // XA_ATOM
417 cb.atoms << Atom(31) // XA_STRING
418 for i, name in atom_names {
419 atom_type := atom_types[i]
420 only_if_exists := if atom_type == .utf8_string { 1 } else { 0 }
421 mut atom := C.XInternAtom(cb.display, &char(name.str), only_if_exists)
422 if atom_type == .utf8_string && atom == Atom(0) {
423 atom = cb.get_atom(.xa_string)
424 }
425 cb.atoms << atom
426 }
427}
428
429fn read_property(d &C.Display, w Window, p Atom) Property {
430 actual_type := Atom(0)
431 actual_format := 0
432 nitems := u64(0)
433 bytes_after := u64(0)
434 ret := &u8(unsafe { nil })
435 mut read_bytes := 1024
436 for {
437 if ret != 0 {
438 C.XFree(ret)
439 }
440 C.XGetWindowProperty(d, w, p, 0, read_bytes, 0, 0, &actual_type, &actual_format, &nitems,
441 &bytes_after, &ret)
442 read_bytes *= 2
443 if bytes_after == 0 {
444 break
445 }
446 }
447 return Property{actual_type, actual_format, nitems, ret}
448}
449
450// pick_target finds the best target given a local copy of a property.
451fn (cb &Clipboard) pick_target(prop Property) Atom {
452 // The list of targets is a list of atoms, so it should have type XA_ATOM
453 // but it may have the type TARGETS instead.
454 if (prop.actual_type != cb.get_atom(.xa_atom) && prop.actual_type != cb.get_atom(.targets))
455 || prop.actual_format != 32 {
456 // This would be really broken. Targets have to be an atom list
457 // and applications should support this. Nevertheless, some
458 // seem broken (MATLAB 7, for instance), so ask for STRING
459 // next instead as the lowest common denominator
460 return cb.get_atom(.xa_string)
461 } else {
462 atom_list := &Atom(voidptr(prop.data))
463
464 mut to_be_requested := Atom(0)
465
466 // This is higher than the maximum priority.
467 mut priority := int(max_i32)
468
469 for i in 0 .. prop.nitems {
470 // See if this data type is allowed and of higher priority (closer to zero)
471 // than the present one.
472
473 target := unsafe { atom_list[i] }
474 if cb.is_supported_target(target) {
475 index := cb.get_target_index(target)
476 if priority > index && index >= 0 {
477 priority = index
478 to_be_requested = target
479 }
480 }
481 }
482 return to_be_requested
483 }
484}
485
486fn (cb &Clipboard) get_atoms(types ...AtomType) []Atom {
487 mut atoms := []Atom{}
488 for typ in types {
489 atoms << cb.atoms[typ]
490 }
491 return atoms
492}
493
494fn (cb &Clipboard) get_atom(typ AtomType) Atom {
495 return cb.atoms[typ]
496}
497
498fn (cb &Clipboard) is_supported_target(target Atom) bool {
499 return cb.get_target_index(target) >= 0
500}
501
502fn (cb &Clipboard) get_target_index(target Atom) int {
503 for i, atom in cb.get_supported_targets() {
504 if atom == target {
505 return i
506 }
507 }
508 return -1
509}
510
511fn (cb &Clipboard) get_supported_targets() []Atom {
512 return cb.get_atoms(AtomType.utf8_string, .xa_string, .text, .text_plain, .text_html)
513}
514
515fn create_xwindow(display &C.Display) Window {
516 n := C.DefaultScreen(display)
517 return C.XCreateSimpleWindow(display, C.RootWindow(display, n), 0, 0, 1, 1, 0, C.BlackPixel(display,
518 n), C.WhitePixel(display, n))
519}
520
521fn new_display() &C.Display {
522 return C.XOpenDisplay(C.NULL)
523}
524
525// new_primary returns a new X11 `PRIMARY` type `Clipboard` instance allocated on the heap.
526// Please note: new_primary only works on X11 based systems.
527pub fn new_primary() &Clipboard {
528 return new_x11_clipboard(.primary)
529}
530