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