aboutsummaryrefslogtreecommitdiff
path: root/v_windows/v/vlib/clipboard/x11/clipboard.c.v
blob: d5a4656a7ffa8263840a55067d5c19036473420b (plain)
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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
// Currently there is only X11 Selections support and no way to handle Wayland
// but since Wayland isn't extremely adopted, we are covering almost all Linux distros.
module x11

import time
import sync
import math

$if freebsd {
	#flag -I/usr/local/include
	#flag -L/usr/local/lib
} $else $if openbsd {
	#flag -I/usr/X11R6/include
	#flag -L/usr/X11R6/lib
}
#flag -lX11

#include <X11/Xlib.h> # Please install a package with the X11 development headers, for example: `apt-get install libx11-dev`
// X11
[typedef]
struct C.Display {
}

type Window = u64
type Atom = u64

fn C.XInitThreads() int

fn C.XCloseDisplay(d &C.Display)

fn C.XFlush(d &C.Display)

fn C.XDestroyWindow(d &C.Display, w Window)

fn C.XNextEvent(d &C.Display, e &C.XEvent)

fn C.XSetSelectionOwner(d &C.Display, a Atom, w Window, time int)

fn C.XGetSelectionOwner(d &C.Display, a Atom) Window

fn C.XChangeProperty(d &C.Display, requestor Window, property Atom, typ Atom, format int, mode int, data voidptr, nelements int) int

fn C.XSendEvent(d &C.Display, requestor Window, propogate int, mask i64, event &C.XEvent)

fn C.XInternAtom(d &C.Display, typ &byte, only_if_exists int) Atom

fn C.XCreateSimpleWindow(d &C.Display, root Window, x int, y int, width u32, height u32, border_width u32, border u64, background u64) Window

fn C.XOpenDisplay(name &byte) &C.Display

fn C.XConvertSelection(d &C.Display, selection Atom, target Atom, property Atom, requestor Window, time int) int

fn C.XSync(d &C.Display, discard int) int

fn C.XGetWindowProperty(d &C.Display, w Window, property Atom, offset i64, length i64, delete int, req_type Atom, actual_type_return &Atom, actual_format_return &int, nitems &u64, bytes_after_return &u64, prop_return &&byte) int

fn C.XDeleteProperty(d &C.Display, w Window, property Atom) int

fn C.DefaultScreen(display &C.Display) int

fn C.RootWindow(display &C.Display, screen_number int) Window

fn C.BlackPixel(display &C.Display, screen_number int) u32

fn C.WhitePixel(display &C.Display, screen_number int) u32

fn C.XFree(data voidptr)

fn todo_del() {}

[typedef]
struct C.XSelectionRequestEvent {
mut:
	display   &C.Display // Display the event was read from
	owner     Window
	requestor Window
	selection Atom
	target    Atom
	property  Atom
	time      int
}

[typedef]
struct C.XSelectionEvent {
mut:
	@type     int
	display   &C.Display // Display the event was read from
	requestor Window
	selection Atom
	target    Atom
	property  Atom
	time      int
}

[typedef]
struct C.XSelectionClearEvent {
mut:
	window    Window
	selection Atom
}

[typedef]
struct C.XDestroyWindowEvent {
mut:
	window Window
}

[typedef]
union C.XEvent {
mut:
	@type             int
	xdestroywindow    C.XDestroyWindowEvent
	xselectionclear   C.XSelectionClearEvent
	xselectionrequest C.XSelectionRequestEvent
	xselection        C.XSelectionEvent
}

const (
	atom_names = ['TARGETS', 'CLIPBOARD', 'PRIMARY', 'SECONDARY', 'TEXT', 'UTF8_STRING', 'text/plain',
		'text/html',
	]
)

// UNSUPPORTED TYPES: MULTIPLE, INCR, TIMESTAMP, image/bmp, image/jpeg, image/tiff, image/png
// all the atom types we need
// currently we only support text
// in the future, maybe we can extend this
// to support other mime types
enum AtomType {
	xa_atom = 0 // value 4
	xa_string = 1 // value 31
	targets = 2
	clipboard = 3
	primary = 4
	secondary = 5
	text = 6
	utf8_string = 7
	text_plain = 8
	text_html = 9
}

pub struct Clipboard {
	display &C.Display
mut:
	selection Atom // the selection atom
	window    Window
	atoms     []Atom
	mutex     &sync.Mutex
	text      string // text data sent or received
	got_text  bool   // used to confirm that we have got the text
	is_owner  bool   // to save selection owner state
}

struct Property {
	actual_type   Atom
	actual_format int
	nitems        u64
	data          &byte
}

// new_clipboard returns a new `Clipboard` instance allocated on the heap.
// The `Clipboard` resources can be released with `free()`
pub fn new_clipboard() &Clipboard {
	return new_x11_clipboard(.clipboard)
}

// new_x11_clipboard initializes a new clipboard of the given selection type.
// Multiple clipboard instance types can be initialized and used separately.
fn new_x11_clipboard(selection AtomType) &Clipboard {
	if selection !in [.clipboard, .primary, .secondary] {
		panic('Wrong AtomType. Must be one of .primary, .secondary or .clipboard.')
	}
	// init x11 thread support
	status := C.XInitThreads()
	if status == 0 {
		println('WARN: this system does not support threads; clipboard will cause the program to lock.')
	}

	display := new_display()

	if display == C.NULL {
		println('ERROR: No X Server running. Clipboard cannot be used.')
		return &Clipboard{
			display: 0
			mutex: sync.new_mutex()
		}
	}

	mut cb := &Clipboard{
		display: display
		window: create_xwindow(display)
		mutex: sync.new_mutex()
	}
	cb.intern_atoms()
	cb.selection = cb.get_atom(selection)
	// start the listener on another thread or
	// we will be locked and will have to hard exit
	go cb.start_listener()
	return cb
}

pub fn (cb &Clipboard) check_availability() bool {
	return cb.display != C.NULL
}

pub fn (mut cb Clipboard) free() {
	C.XDestroyWindow(cb.display, cb.window)
	cb.window = Window(0)
	// FIX ME: program hangs when closing display
	// XCloseDisplay(cb.display)
}

pub fn (mut cb Clipboard) clear() {
	cb.mutex.@lock()
	C.XSetSelectionOwner(cb.display, cb.selection, Window(0), C.CurrentTime)
	C.XFlush(cb.display)
	cb.is_owner = false
	cb.text = ''
	cb.mutex.unlock()
}

pub fn (cb &Clipboard) has_ownership() bool {
	return cb.is_owner
}

fn (cb &Clipboard) take_ownership() {
	C.XSetSelectionOwner(cb.display, cb.selection, cb.window, C.CurrentTime)
	C.XFlush(cb.display)
}

// set_text stores `text` in the system clipboard.
pub fn (mut cb Clipboard) set_text(text string) bool {
	if cb.window == Window(0) {
		return false
	}
	cb.mutex.@lock()
	cb.text = text
	cb.is_owner = true
	cb.take_ownership()
	C.XFlush(cb.display)
	cb.mutex.unlock()
	// sleep a little bit
	time.sleep(1 * time.millisecond)
	return cb.is_owner
}

pub fn (mut cb Clipboard) get_text() string {
	if cb.window == Window(0) {
		return ''
	}
	if cb.is_owner {
		return cb.text
	}
	cb.got_text = false

	// Request a list of possible conversions, if we're pasting.
	C.XConvertSelection(cb.display, cb.selection, cb.get_atom(.targets), cb.selection,
		cb.window, C.CurrentTime)

	// wait for the text to arrive
	mut retries := 5
	for {
		if cb.got_text || retries == 0 {
			break
		}
		time.sleep(50 * time.millisecond)
		retries--
	}
	return cb.text
}

// transmit_selection is crucial to handling all the different data types.
// If we ever support other mimetypes they should be handled here.
fn (mut cb Clipboard) transmit_selection(xse &C.XSelectionEvent) bool {
	if xse.target == cb.get_atom(.targets) {
		targets := cb.get_supported_targets()
		C.XChangeProperty(xse.display, xse.requestor, xse.property, cb.get_atom(.xa_atom),
			32, C.PropModeReplace, targets.data, targets.len)
	} else if cb.is_supported_target(xse.target) && cb.is_owner && cb.text != '' {
		cb.mutex.@lock()
		C.XChangeProperty(xse.display, xse.requestor, xse.property, xse.target, 8, C.PropModeReplace,
			cb.text.str, cb.text.len)
		cb.mutex.unlock()
	} else {
		return false
	}
	return true
}

fn (mut cb Clipboard) start_listener() {
	event := C.XEvent{}
	mut sent_request := false
	mut to_be_requested := Atom(0)
	for {
		C.XNextEvent(cb.display, &event)
		if unsafe { event.@type == 0 } {
			println('error')
			continue
		}
		match unsafe { event.@type } {
			C.DestroyNotify {
				if unsafe { event.xdestroywindow.window == cb.window } {
					// we are done
					return
				}
			}
			C.SelectionClear {
				if unsafe { event.xselectionclear.window == cb.window } && unsafe {
					event.xselectionclear.selection == cb.selection
				} {
					cb.mutex.@lock()
					cb.is_owner = false
					cb.text = ''
					cb.mutex.unlock()
				}
			}
			C.SelectionRequest {
				if unsafe { event.xselectionrequest.selection == cb.selection } {
					mut xsre := &C.XSelectionRequestEvent{
						display: 0
					}
					xsre = unsafe { &event.xselectionrequest }

					mut xse := C.XSelectionEvent{
						@type: C.SelectionNotify // 31
						display: xsre.display
						requestor: xsre.requestor
						selection: xsre.selection
						time: xsre.time
						target: xsre.target
						property: xsre.property
					}
					if !cb.transmit_selection(&xse) {
						xse.property = new_atom(0)
					}
					C.XSendEvent(cb.display, xse.requestor, 0, C.PropertyChangeMask, voidptr(&xse))
					C.XFlush(cb.display)
				}
			}
			C.SelectionNotify {
				if unsafe {
					event.xselection.selection == cb.selection
						&& event.xselection.property != Atom(0)
				} {
					if unsafe { event.xselection.target == cb.get_atom(.targets) && !sent_request } {
						sent_request = true
						prop := read_property(cb.display, cb.window, cb.selection)
						to_be_requested = cb.pick_target(prop)
						if to_be_requested != Atom(0) {
							C.XConvertSelection(cb.display, cb.selection, to_be_requested,
								cb.selection, cb.window, C.CurrentTime)
						}
					} else if unsafe { event.xselection.target == to_be_requested } {
						sent_request = false
						to_be_requested = Atom(0)
						cb.mutex.@lock()
						prop := unsafe {
							read_property(event.xselection.display, event.xselection.requestor,
								event.xselection.property)
						}
						unsafe {
							C.XDeleteProperty(event.xselection.display, event.xselection.requestor,
								event.xselection.property)
						}
						if cb.is_supported_target(prop.actual_type) {
							cb.got_text = true
							unsafe {
								cb.text = prop.data.vstring() // TODO: return byteptr to support other mimetypes
							}
						}
						cb.mutex.unlock()
					}
				}
			}
			C.PropertyNotify {}
			else {}
		}
	}
}

/*
* Helpers
*/
// intern_atoms initializes all the atoms we need.
fn (mut cb Clipboard) intern_atoms() {
	cb.atoms << Atom(4) // XA_ATOM
	cb.atoms << Atom(31) // XA_STRING
	for i, name in x11.atom_names {
		only_if_exists := if i == int(AtomType.utf8_string) { 1 } else { 0 }
		cb.atoms << C.XInternAtom(cb.display, &char(name.str), only_if_exists)
		if i == int(AtomType.utf8_string) && cb.atoms[i] == Atom(0) {
			cb.atoms[i] = cb.get_atom(.xa_string)
		}
	}
}

fn read_property(d &C.Display, w Window, p Atom) Property {
	actual_type := Atom(0)
	actual_format := 0
	nitems := u64(0)
	bytes_after := u64(0)
	ret := &byte(0)
	mut read_bytes := 1024
	for {
		if ret != 0 {
			C.XFree(ret)
		}
		C.XGetWindowProperty(d, w, p, 0, read_bytes, 0, 0, &actual_type, &actual_format,
			&nitems, &bytes_after, &ret)
		read_bytes *= 2
		if bytes_after == 0 {
			break
		}
	}
	return Property{actual_type, actual_format, nitems, ret}
}

// pick_target finds the best target given a local copy of a property.
fn (cb &Clipboard) pick_target(prop Property) Atom {
	// The list of targets is a list of atoms, so it should have type XA_ATOM
	// but it may have the type TARGETS instead.
	if (prop.actual_type != cb.get_atom(.xa_atom) && prop.actual_type != cb.get_atom(.targets))
		|| prop.actual_format != 32 {
		// This would be really broken. Targets have to be an atom list
		// and applications should support this. Nevertheless, some
		// seem broken (MATLAB 7, for instance), so ask for STRING
		// next instead as the lowest common denominator
		return cb.get_atom(.xa_string)
	} else {
		atom_list := &Atom(voidptr(prop.data))

		mut to_be_requested := Atom(0)

		// This is higher than the maximum priority.
		mut priority := math.max_i32

		for i in 0 .. prop.nitems {
			// See if this data type is allowed and of higher priority (closer to zero)
			// than the present one.

			target := unsafe { atom_list[i] }
			if cb.is_supported_target(target) {
				index := cb.get_target_index(target)
				if priority > index && index >= 0 {
					priority = index
					to_be_requested = target
				}
			}
		}
		return to_be_requested
	}
}

fn (cb &Clipboard) get_atoms(types ...AtomType) []Atom {
	mut atoms := []Atom{}
	for typ in types {
		atoms << cb.atoms[typ]
	}
	return atoms
}

fn (cb &Clipboard) get_atom(typ AtomType) Atom {
	return cb.atoms[typ]
}

fn (cb &Clipboard) is_supported_target(target Atom) bool {
	return cb.get_target_index(target) >= 0
}

fn (cb &Clipboard) get_target_index(target Atom) int {
	for i, atom in cb.get_supported_targets() {
		if atom == target {
			return i
		}
	}
	return -1
}

fn (cb &Clipboard) get_supported_targets() []Atom {
	return cb.get_atoms(AtomType.utf8_string, .xa_string, .text, .text_plain, .text_html)
}

fn new_atom(value int) &Atom {
	return unsafe { &Atom(&u64(u64(value))) }
}

fn create_xwindow(display &C.Display) Window {
	n := C.DefaultScreen(display)
	return C.XCreateSimpleWindow(display, C.RootWindow(display, n), 0, 0, 1, 1, 0, C.BlackPixel(display,
		n), C.WhitePixel(display, n))
}

fn new_display() &C.Display {
	return C.XOpenDisplay(C.NULL)
}

// new_primary returns a new X11 `PRIMARY` type `Clipboard` instance allocated on the heap.
// Please note: new_primary only works on X11 based systems.
pub fn new_primary() &Clipboard {
	return new_x11_clipboard(.primary)
}