497 lines
13 KiB
V
497 lines
13 KiB
V
// 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 = 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 := &u8(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 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)
|
|
}
|