// 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 clipboard import ( time sync math ) #flag -lX11 #include // X11 struct C.Display struct C.Atom struct C.Window fn C.XInitThreads() int fn C.XCloseDisplay(d &Display) fn C.XFlush(d &Display) fn C.XDestroyWindow(d &Display, w Window) fn C.XNextEvent(d Display, e &XEvent) fn C.XSetSelectionOwner(d &Display, a Atom, w Window, time int) fn C.XGetSelectionOwner(d &Display, a Atom) Window fn C.XChangeProperty(d &Display, requestor Window, property Atom, typ Atom, format int, mode int, data voidptr, nelements int) int fn C.XSendEvent(d &Display, requestor Window, propogate int, mask i64, event &XEvent) fn C.XInternAtom(d &Display, typ byteptr, only_if_exists int) Atom fn C.XCreateSimpleWindow(d &Display, root Window, x int, y int, width u32, height u32, border_width u32, border u64, background u64) Window fn C.XOpenDisplay(name byteptr) &Display fn C.XConvertSelection(d &Display, selection Atom, target Atom, property Atom, requestor Window, time int) int fn C.XSync(d &Display, discard int) int fn C.XGetWindowProperty(d &Display, w Window, property Atom, offset i64, length i64, delete int, req_type Atom, actual_type_return &Atom, actual_format_return &int, nitems &i64, bytes_after_return &i64, prop_return &byteptr) int fn C.XDeleteProperty(d &Display, w Window, property Atom) int fn C.DefaultScreen() int fn C.RootWindow() voidptr fn C.BlackPixel() voidptr fn C.WhitePixel() voidptr fn C.XFree() struct C.XSelectionRequestEvent{ mut: selection Atom display &Display /* Display the event was read from */ owner Window requestor Window target Atom property Atom time int } struct C.XSelectionEvent{ mut: @type int selection Atom display &Display /* Display the event was read from */ requestor Window target Atom property Atom time int } struct C.XSelectionClearEvent{ mut: window Window selection Atom } struct C.XDestroyWindowEvent { mut: window Window } struct C.XEvent{ mut: @type int xselectionrequest XSelectionRequestEvent xselection XSelectionEvent xselectionclear XSelectionClearEvent xdestroywindow XDestroyWindowEvent } 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 atom_type { 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 &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 int data byteptr } fn new_clipboard() &Clipboard { return new_x11_clipboard(.clipboard) } // Initialize a new clipboard of the given selection type. // We can initialize multiple clipboard instances and use them separately fn new_x11_clipboard(selection atom_type) &Clipboard { if !(selection in [.clipboard, .primary, .secondary]) { panic("Wrong atom_type. Must be one of .primary, .secondary or .clipboard.") } //init x11 thread support status := 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 } fn (cb &Clipboard) check_availability() bool { return cb.display != C.NULL } fn (cb mut Clipboard) free() { XDestroyWindow(cb.display, cb.window) cb.window = Window(C.None) //FIX ME: program hangs when closing display //XCloseDisplay(cb.display) } fn (cb mut Clipboard) clear(){ cb.mutex.lock() XSetSelectionOwner(cb.display, cb.selection, Window(C.None), C.CurrentTime) XFlush(cb.display) cb.is_owner = false cb.text = "" cb.mutex.unlock() } fn (cb &Clipboard) has_ownership() bool { return cb.is_owner } fn (cb &Clipboard) take_ownership(){ XSetSelectionOwner(cb.display, cb.selection, cb.window, C.CurrentTime) XFlush(cb.display) } fn (cb mut Clipboard) set_text(text string) bool { if cb.window == Window(C.None) {return false} cb.mutex.lock() cb.text = text cb.is_owner = true cb.take_ownership() XFlush(cb.display) cb.mutex.unlock() // sleep a little bit time.sleep(1) return cb.is_owner } fn (cb mut Clipboard) get_text() string { if cb.window == Window(C.None) {return ""} if cb.is_owner { return cb.text } cb.got_text = false //Request a list of possible conversions, if we're pasting. 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.usleep(50000) retries-- } return cb.text } // this function is crucial to handling all the different data types // if we ever support other mimetypes they should be handled here fn (cb mut Clipboard) transmit_selection(xse &XSelectionEvent) bool { if xse.target == cb.get_atom(.targets) { targets := cb.get_supported_targets() 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() 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 (cb mut Clipboard) start_listener(){ event := XEvent{} mut sent_request := false mut to_be_requested := Atom(0) for { XNextEvent(cb.display, &event) if (event.@type == 0) { println("error") continue } match event.@type { C.DestroyNotify { if event.xdestroywindow.window == cb.window { return // we are done } } C.SelectionClear { if event.xselectionclear.window == cb.window && event.xselectionclear.selection == cb.selection { cb.mutex.lock() cb.is_owner = false cb.text = "" cb.mutex.unlock() } } C.SelectionRequest { if event.xselectionrequest.selection == cb.selection { mut xsre := &XSelectionRequestEvent{ display: 0 } xsre = &event.xselectionrequest mut xse := 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(C.None) } XSendEvent(cb.display, xse.requestor, 0, C.PropertyChangeMask, &xse) XFlush(cb.display) } } C.SelectionNotify { if event.xselection.selection == cb.selection && event.xselection.property != Atom(C.None) { if 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) { XConvertSelection(cb.display, cb.selection, to_be_requested, cb.selection, cb.window, C.CurrentTime) } } else if event.xselection.target == to_be_requested { sent_request = false to_be_requested = Atom(0) cb.mutex.lock() prop := read_property(event.xselection.display, event.xselection.requestor, event.xselection.property) XDeleteProperty(event.xselection.display, event.xselection.requestor, event.xselection.property) if cb.is_supported_target(prop.actual_type) { cb.got_text = true cb.text = string(prop.data) //TODO: return byteptr to support other mimetypes } cb.mutex.unlock() } } } C.PropertyNotify {} else {} } } } // Helpers // Initialize all the atoms we need fn (cb mut Clipboard) intern_atoms(){ cb.atoms << Atom(4) //XA_ATOM cb.atoms << Atom(31) //XA_STRING for i, name in atom_names{ only_if_exists := if i == int(atom_type.utf8_string) {1} else {0} cb.atoms << XInternAtom(cb.display, name.str, only_if_exists) if i == int(atom_type.utf8_string) && cb.atoms[i] == Atom(C.None) { cb.atoms[i] = cb.get_atom(.xa_string) } } } fn read_property(d &Display, w Window, p Atom) Property { actual_type := Atom(0) actual_format := 0 nitems := 0 bytes_after := 0 ret := byteptr(0) mut read_bytes := 1024 for { if ret != 0 { C.XFree(ret) } XGetWindowProperty(d, w, p, 0, read_bytes, 0, C.AnyPropertyType, &actual_type, &actual_format, &nitems, &bytes_after, &ret) read_bytes *= 2 if bytes_after == 0 {break} } return Property{actual_type, actual_format, nitems, ret} } // 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(prop.data) mut to_be_requested := Atom(0) //This is higher than the maximum priority. mut priority := math.max_i32 for i := 0; i < prop.nitems; i++ { //See if this data type is allowed and of higher priority (closer to zero) //than the present one. if cb.is_supported_target(atom_list[i]) { index := cb.get_target_index(atom_list[i]) if(priority > index && index >= 0) { priority = index to_be_requested = atom_list[i] } } } return to_be_requested } } fn (cb &Clipboard) get_atoms(types ...atom_type) []Atom { mut atoms := []Atom for typ in types { atoms << cb.atoms[typ] } return atoms } fn (cb &Clipboard) get_atom(typ atom_type) 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(atom_type.utf8_string, .xa_string, .text, .text_plain, .text_html) } fn new_atom(value int) &Atom { mut atom := &Atom{} atom = value return atom } fn create_xwindow(display &Display) Window { N := C.DefaultScreen(display) return XCreateSimpleWindow(display, C.RootWindow(display, N), 0, 0, 1, 1, 0, C.BlackPixel(display, N), C.WhitePixel(display, N)) } fn new_display() &Display { return XOpenDisplay(C.NULL) } // create a new PRIMARY clipboard (only supported on Linux) pub fn new_primary() &Clipboard { return new_x11_clipboard(.primary) }