// F16 X11 demo — clipboard + file drag-and-drop, both implemented by hand on
// the one mechanism X11 has for either: ICCCM selections
// (../../../features/f16-clipboard-dnd.md; findings in ../../f16-clipboard-dnd.md).
//
// Modes (--mode=..., default `dnd`):
// copy own CLIPBOARD (and PRIMARY — the one-line contrast), serve
// TARGETS/TIMESTAMP/UTF8_STRING ('é漢🎈') to any requestor
// (xclip -o); log every SelectionRequest; exit on SelectionClear
// copy-incr same with a 400 KiB payload -> INCR *sending* (the
// property-delete-driven chunk choreography, logged per chunk)
// paste log the owner's TARGETS, then convert UTF8_STRING; a payload
// above the owner's chunk limit arrives as INCR *receive*
// dnd XDND v5, BOTH SIDES in one process: this window is the target
// (XdndAware); a second Display connection plays the source —
// owns XdndSelection, Enter/Position, aborts once with Leave,
// re-enters, Drops, serves text/uri-list through a normal
// selection transfer, gets XdndFinished. (The CI default.)
//
// Headless-safe: no X server -> prints `SKIP:` and exits 0. WSI_AUTO_EXIT=1
// tightens the deadline so CI runs stay bounded. run.sh drives xclip passes.
module (module) appapp;
import (module) cc; // ImportC: Xlib + Xutil + Xatom + poll
import (module) instrumentinstrument;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.configD compatible types that correspond to various basic types in associated
C and C++ compilers.
Source
core/stdc/config.d
config : c_long, c_ulong;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdioD header file for C99 <stdio.h>
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html, stdio.h
Source
core/stdc/stdio.d
stdio : (alias) app.printf = int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf, (alias) app.snprintf = int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv, (alias) app.malloc = void* core.stdc.stdlib.malloc(ulong size) nothrow @nogcmalloc;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stringD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/string.h.html, string.h
Source
core/stdc/string.d
string : (alias) app.memcpy = void* core.stdc.string.memcpy(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemcpy, (alias) app.strcmp = int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp, (alias) app.strlen = ulong core.stdc.string.strlen(scope const(char*) s) pure nothrow @nogcstrlen;
// -- macro constants ImportC cannot export (cast-expression #defines) --------
enum : c_long
{
(enum value) app.PropertyChangeMask = 4194304LPropertyChangeMask = 1L << 22,
(enum value) app.StructureNotifyMask = 131072LStructureNotifyMask = 1L << 17,
}
enum // XEvent.type discriminators
{
(enum value) app.PropertyNotify = 28PropertyNotify = 28, (enum value) app.SelectionClear = 29SelectionClear = 29, (enum value) app.SelectionRequest = 30SelectionRequest = 30,
(enum value) app.SelectionNotify = 31SelectionNotify = 31, (enum value) app.ClientMessage = 33ClientMessage = 33,
}
enum { (enum value) app.PropertyNewValue = 0PropertyNewValue = 0, (enum value) app.PropertyDelete = 1PropertyDelete = 1 }
enum { (enum value) app.PropModeReplace = 0PropModeReplace = 0, (enum value) app.PropModeAppend = 2PropModeAppend = 2 }
enum { (enum value) app.None = 0None = 0, (enum value) app.False = 0False = 0, (enum value) app.True = 1True = 1, (enum value) app.POLLIN = 1POLLIN = 0x001 }
enum Atom (constant) _error_ app.XA_PRIMARY = __errorXA_PRIMARY = 1, (constant) _error_ app.XA_ATOM = __errorXA_ATOM = 4, (constant) _error_ app.XA_INTEGER = __errorXA_INTEGER = 19, (constant) _error_ app.XA_STRING = __errorXA_STRING = 31;
enum (alias) object.size_t = ulongsize_t (constant) ulong app.INCR_THRESHOLD = 262144LUINCR_THRESHOLD = 256 * 1024; // own a bigger payload -> send INCR
enum (alias) object.size_t = ulongsize_t (constant) ulong app.INCR_CHUNK = 65536LUINCR_CHUNK = 64 * 1024; // bytes per INCR append
enum int (constant) int app.XDND_VERSION = 5XDND_VERSION = 5;
// -- small helpers ------------------------------------------------------------
/// Atom -> name for log lines (rotating static buffers so one emitf can hold 4).
const(char)* app.anAtom -> name for log lines (rotating static buffers so one emitf can hold 4).
an(Display* (parameter) Display* dpydpy, Atom (parameter) Atom aa) @nogc nothrow
{
static char[64][4] char[64][4] bufsbufs;
static int int nextnext;
if (a == None)
return "None";
auto _error_ bufbuf = bufs[next & 3].bufs[next & 3].ptrptr;
next++;
char* _error_ ss = XGetAtomName(dpy, a);
snprintf(buf, 64, "%s", s);
XFree(s);
return buf;
}
/// ICCCM-correct ownership timestamp: "A zero-length append to a property is
/// a way to obtain a timestamp for this purpose; the timestamp is in the
/// corresponding PropertyNotify event." (ICCCM § 2.1) — never CurrentTime.
Time app.serverTimestampICCCM-correct ownership timestamp: "A zero-length append to a property is
a way to obtain a timestamp for this purpose; the timestamp is in the
corresponding PropertyNotify event." (ICCCM § 2.1) — never CurrentTime.
serverTimestamp(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin) @nogc nothrow
{
const _error_ propprop = XInternAtom(dpy, "WSI_TIMESTAMP", False);
XChangeProperty(dpy, win, prop, XA_STRING, 8, PropModeAppend, null, 0);
for (;;)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
if (ev.ev.typetype == PropertyNotify && ev.xproperty.atom == prop)
return ev.xproperty.time;
}
}
/// Read a whole property (deleting it when `del`); returns malloc'd bytes.
ubyte* app.readPropertyRead a whole property (deleting it when del); returns malloc'd bytes.
readProperty(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin, Atom (parameter) Atom propprop, int del,
out Atom (parameter) Atom typetype, out int (parameter) int formatformat, out c_ulong (parameter) c_ulong nitemsnitems) @nogc nothrow
{
Atom _error_ actualTypeactualType;
int _error_ actualFormatactualFormat;
c_ulong _error_ nn, _error_ afterafter;
ubyte* _error_ datadata;
XGetWindowProperty(dpy, win, prop, 0, 0x1FFFFFFF, del, AnyPropertyType,
&actualType, &actualFormat, &n, &after, &data);
type = actualType;
format = actualFormat;
nitems = n;
return data; // caller XFree()s
}
enum Atom (constant) _error_ app.AnyPropertyType = __errorAnyPropertyType = 0;
// -- selection OWNER side -----------------------------------------------------
// One struct serves both the clipboard owner and the XDND source: on X11 the
// clipboard and DnD literally share the transfer machinery (the F16 finding).
struct (struct) app.SelOwnerSelOwner
{
Display* (field) _error_ app.SelOwner.dpydpy;
Window (field) _error_ app.SelOwner.winwin;
Atom (field) _error_ app.SelOwner.dataTargetdataTarget; // UTF8_STRING (clipboard) or text/uri-list (XDND)
const(ubyte)* (field) const(ubyte)* app.SelOwner.datadata;
(alias) object.size_t = ulongsize_t (field) ulong app.SelOwner.lenlen;
Time (field) _error_ app.SelOwner.tsts;
Atom (field) _error_ app.SelOwner.targetsAtomtargetsAtom, (field) _error_ app.SelOwner.timestampAtomtimestampAtom, (field) _error_ app.SelOwner.incrAtomincrAtom;
// active INCR send (at most one in this demo)
Window (field) _error_ app.SelOwner.incrRequestorincrRequestor;
Atom (field) _error_ app.SelOwner.incrPropertyincrProperty;
(alias) object.size_t = ulongsize_t (field) ulong app.SelOwner.incrOffsetincrOffset;
int (field) int app.SelOwner.incrChunksincrChunks;
bool (field) bool app.SelOwner.incrActiveincrActive;
}
/// Answer one SelectionRequest per ICCCM § 2.2: convert into the named
/// property on the requestor's window, then confirm with SelectionNotify
/// (property = None refuses). Payloads above INCR_THRESHOLD start an INCR
/// transfer (ICCCM § 2.7.2) instead of one giant ChangeProperty.
void app.handleSelectionRequestAnswer one SelectionRequest per ICCCM § 2.2: convert into the named
property on the requestor's window, then confirm with SelectionNotify
(property = None refuses). Payloads above INCR_THRESHOLD start an INCR
transfer (ICCCM § 2.7.2) instead of one giant ChangeProperty.
handleSelectionRequest(ref (struct) app.SelOwnerSelOwner o, ref XSelectionRequestEvent req) @nogc nothrow
{
emitf("clip_request", "requestor=0x%lx selection=%s target=%s property=%s",
req.requestor, an(o.o.dpydpy, req.req.selectionselection), an(o.o.dpydpy, req.target),
an(o.o.dpydpy, req.req.propertyproperty));
// "If the specified property is None, the requestor is an obsolete
// client. Owners are encouraged to support these clients by using the
// specified target atom as the property name" (ICCCM § 2.2).
Atom _error_ propertyproperty = req.req.propertyproperty == None ? req.target : req.req.propertyproperty;
if (req.target == o.o.targetsAtomtargetsAtom)
{
Atom[3] _error_ targetstargets = [o.o.targetsAtomtargetsAtom, o.o.timestampAtomtimestampAtom, o.o.dataTargetdataTarget];
XChangeProperty(o.o.dpydpy, req.requestor, property, XA_ATOM, 32,
PropModeReplace, cast(ubyte*) targets.targets.ptrptr, 3);
}
else if (req.target == o.o.timestampAtomtimestampAtom)
{
c_long _error_ tt = cast(c_long) o.o.tsts;
XChangeProperty(o.o.dpydpy, req.requestor, property, XA_INTEGER, 32,
PropModeReplace, cast(ubyte*)&t, 1);
}
else if (req.target == o.o.dataTargetdataTarget && o.o.lenlen > INCR_THRESHOLD)
{
// INCR start: property of type INCR holding "a lower bound on the
// number of bytes of data in the selection" (ICCCM § 2.7.2); we then
// watch the requestor's window for the property *deletions* that
// drive the chunk loop. Input masks are per-client, so selecting
// PropertyChangeMask on a foreign window is legal.
c_long _error_ boundbound = cast(c_long) o.o.lenlen;
XSelectInput(o.o.dpydpy, req.requestor, PropertyChangeMask);
XChangeProperty(o.o.dpydpy, req.requestor, property, o.o.incrAtomincrAtom, 32,
PropModeReplace, cast(ubyte*)&bound, 1);
o.o.incrRequestorincrRequestor = req.requestor;
o.o.incrPropertyincrProperty = property;
o.o.incrOffsetincrOffset = 0;
o.o.incrChunksincrChunks = 0;
o.o.incrActiveincrActive = true;
emitf("clip_send", "bytes=%zu incr=1 phase=start lower_bound=%ld", o.o.lenlen, bound);
}
else if (req.target == o.o.dataTargetdataTarget)
{
XChangeProperty(o.o.dpydpy, req.requestor, property, o.o.dataTargetdataTarget, 8,
PropModeReplace, o.o.datadata, cast(int) o.o.lenlen);
emitf("clip_send", "bytes=%zu incr=0 target=%s", o.o.lenlen, an(o.o.dpydpy, o.o.dataTargetdataTarget));
}
else
{
property = None; // refuse: SelectionNotify with property None
emitf("clip_send", "refused target=%s", an(o.o.dpydpy, req.target));
}
XEvent _error_ replyreply;
reply.xselection.reply.xselection.typetype = SelectionNotify;
reply.xselection.display = req.display;
reply.xselection.requestor = req.requestor;
reply.xselection.reply.xselection.selectionselection = req.req.selectionselection;
reply.xselection.target = req.target;
reply.xselection.reply.xselection.propertyproperty = property;
reply.xselection.time = req.time;
XSendEvent(o.o.dpydpy, req.requestor, False, 0, &reply);
XFlush(o.o.dpydpy);
}
/// INCR chunk pump: each PropertyDelete from the requestor means "read,
/// send the next chunk"; a zero-length append terminates. Returns true when
/// the transfer just completed.
bool app.handleIncrDeleteINCR chunk pump: each PropertyDelete from the requestor means "read,
send the next chunk"; a zero-length append terminates. Returns true when
the transfer just completed.
handleIncrDelete(ref (struct) app.SelOwnerSelOwner o, ref XPropertyEvent pev) @nogc nothrow
{
if (!o.o.incrActiveincrActive || pev.window != o.o.incrRequestorincrRequestor
|| pev.atom != o.o.incrPropertyincrProperty || pev.state != PropertyDelete)
return false;
const _error_ remainingremaining = o.o.lenlen - o.o.incrOffsetincrOffset;
const _error_ nn = remaining < INCR_CHUNK ? remaining : INCR_CHUNK;
XChangeProperty(o.o.dpydpy, o.o.incrRequestorincrRequestor, o.o.incrPropertyincrProperty, o.o.dataTargetdataTarget, 8,
PropModeReplace, o.o.datadata + o.o.incrOffsetincrOffset, cast(int) n);
XFlush(o.o.dpydpy);
if (n == 0) // final zero-length chunk -> done
{
o.o.incrActiveincrActive = false;
emitf("clip_send", "bytes=%zu incr=1 phase=done chunks=%d", o.o.lenlen, o.o.incrChunksincrChunks);
return true;
}
o.o.incrOffsetincrOffset += n;
o.o.incrChunksincrChunks++;
emitf("incr_chunk", "n=%d bytes=%zu offset=%zu", o.o.incrChunksincrChunks, n, o.o.incrOffsetincrOffset);
return false;
}
// -- selection REQUESTOR side ---------------------------------------------------
/// Fetch one conversion result (the property a SelectionNotify named),
/// following the INCR receive loop when the type is INCR: delete the INCR
/// property to start, then GetProperty(delete=True) on each PropertyNewValue
/// until a zero-length chunk (ICCCM § 2.7.2). Returns total bytes.
(alias) object.size_t = ulongsize_t app.fetchConvertedFetch one conversion result (the property a SelectionNotify named),
following the INCR receive loop when the type is INCR: delete the INCR
property to start, then GetProperty(delete=True) on each PropertyNewValue
until a zero-length chunk (ICCCM § 2.7.2). Returns total bytes.
fetchConverted(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin, Atom (parameter) Atom propprop, Atom (parameter) Atom incrAtomincrAtom,
ubyte* outBuf, (alias) object.size_t = ulongsize_t outCap, out bool (parameter) bool wasIncrwasIncr, out int (parameter) int chunkschunks) @nogc nothrow
{
Atom _error_ typetype;
int _error_ formatformat;
c_ulong _error_ nitemsnitems;
ubyte* _error_ datadata = readProperty(dpy, win, prop, True, type, format, nitems);
if (type != incrAtom)
{
wasIncr = false;
chunks = 0;
const _error_ nn = cast((unresolved type) size_tsize_t) nitems;
if (data !is null && outBuf !is null && n <= outCap)
memcpy(outBuf, data, n);
if (data !is null)
XFree(data);
return n;
}
// INCR: the GetProperty(delete=True) above performed the ICCCM "starts
// the transfer process by deleting the (type==INCR) property".
const _error_ boundbound = data !is null && nitems >= 1 ? *cast(c_long*) data : 0;
if (data !is null)
XFree(data);
emitf("paste_incr", "phase=start lower_bound=%ld", bound);
wasIncr = true;
chunks = 0;
(unresolved type) size_tsize_t _error_ totaltotal = 0;
for (;;)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
if (ev.ev.typetype != PropertyNotify || ev.xproperty.atom != prop
|| ev.xproperty.state != PropertyNewValue)
continue;
ubyte* _error_ chunkchunk = readProperty(dpy, win, prop, True, type, format, nitems);
const _error_ nn = cast((unresolved type) size_tsize_t) nitems;
if (chunk !is null)
{
if (outBuf !is null && total + n <= outCap)
memcpy(outBuf + total, chunk, n);
XFree(chunk);
}
if (n == 0)
break; // zero-length chunk terminates
chunks++;
total += n;
emitf("incr_chunk", "n=%d bytes=%zu total=%zu", chunks, n, total);
}
return total;
}
// -- modes ----------------------------------------------------------------------
int app.runCopyrunCopy(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin, bool big, long (parameter) long deadlineUsdeadlineUs) @nogc nothrow
{
(unresolved type) SelOwnerSelOwner _error_ ownerowner;
owner.owner.dpydpy = dpy;
owner.owner.winwin = win;
owner.owner.targetsAtomtargetsAtom = XInternAtom(dpy, "TARGETS", False);
owner.owner.timestampAtomtimestampAtom = XInternAtom(dpy, "TIMESTAMP", False);
owner.owner.incrAtomincrAtom = XInternAtom(dpy, "INCR", False);
owner.owner.dataTargetdataTarget = XInternAtom(dpy, "UTF8_STRING", False);
const _error_ clipboardclipboard = XInternAtom(dpy, "CLIPBOARD", False);
static immutable (unresolved type) stringstring string smallsmall = "é漢🎈"; // 2+3+4 UTF-8 bytes
if (big)
{
enum (unresolved type) size_tsize_t (constant) size_t bigLen = 400 * 1024bigLen = 400 * 1024; // > INCR_THRESHOLD -> INCR send
auto _error_ pp = cast(ubyte*) malloc(bigLen);
foreach ((parameter) ii; 0 .. bigLen)
p[i] = cast(ubyte)('A' + i % 26);
owner.owner.datadata = p;
owner.owner.lenlen = bigLen;
}
else
{
owner.owner.datadata = cast(const ubyte*) small.small.ptrptr;
owner.owner.lenlen = small.small.lengthlength;
}
owner.owner.tsts = serverTimestamp(dpy, win);
XSetSelectionOwner(dpy, clipboard, win, owner.owner.tsts);
const _error_ gotgot = XGetSelectionOwner(dpy, clipboard) == win;
emitf("clip_offer", "selection=CLIPBOARD targets=[TARGETS,TIMESTAMP,UTF8_STRING] bytes=%zu ts=%lu owned=%d",
owner.owner.lenlen, owner.owner.tsts, cast(int) got);
if (!big)
{
// The PRIMARY contrast: identical machinery, one more line.
XSetSelectionOwner(dpy, XA_PRIMARY, win, owner.owner.tsts);
emit("clip_offer selection=PRIMARY targets=[TARGETS,TIMESTAMP,UTF8_STRING] note=same_machinery_one_line");
}
const _error_ fdfd = XConnectionNumber(dpy);
bool _error_ incrDoneincrDone = false;
while (nowUs() < deadlineUs)
{
while (XPending(dpy) > 0)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
switch (ev.ev.typetype)
{
case SelectionRequest:
handleSelectionRequest(owner, ev.xselectionrequest);
break;
case PropertyNotify:
if (handleIncrDelete(owner, ev.xproperty))
incrDone = true;
break;
case SelectionClear:
emitf("ownership_lost", "selection=%s ts=%lu",
an(dpy, ev.xselectionclear.ev.xselectionclear.selectionselection), ev.xselectionclear.time);
if (ev.xselectionclear.ev.xselectionclear.selectionselection == clipboard)
return 0; // the F16 "another app copies" requirement
break;
default:
break;
}
}
if (incrDone && big)
return 0; // copy-incr: one full INCR transfer served is the demo
pollfd _error_ pfdpfd;
pfd.pfd.fdfd = fd;
pfd.events = POLLIN;
poll(&pfd, 1, 100);
}
emit("auto_exit reason=deadline");
return 0;
}
int app.runPasterunPaste(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin) @nogc nothrow
{
const _error_ clipboardclipboard = XInternAtom(dpy, "CLIPBOARD", False);
const _error_ targetsAtomtargetsAtom = XInternAtom(dpy, "TARGETS", False);
const _error_ utf8utf8 = XInternAtom(dpy, "UTF8_STRING", False);
const _error_ incrAtomincrAtom = XInternAtom(dpy, "INCR", False);
const _error_ destdest = XInternAtom(dpy, "WSI_PASTE", False);
const _error_ tsts = serverTimestamp(dpy, win);
const _error_ ownerowner = XGetSelectionOwner(dpy, clipboard);
emitf("step", "name=XGetSelectionOwner selection=CLIPBOARD owner=0x%lx", owner);
if (owner == None)
{
printf("SKIP: nobody owns CLIPBOARD (run via run.sh with xclip)\n");
return 0;
}
// 1) what does the owner offer? (TARGETS first, per ICCCM)
XConvertSelection(dpy, clipboard, targetsAtom, dest, win, ts);
for (;;)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
if (ev.ev.typetype != SelectionNotify)
continue;
if (ev.xselection.ev.xselection.propertyproperty == None)
{
emit("clip_offer targets=[] note=owner_refused_TARGETS");
break;
}
Atom _error_ typetype;
int _error_ formatformat;
c_ulong _error_ nn;
auto _error_ datadata = readProperty(dpy, win, dest, True, type, format, n);
char[512] _error_ listlist;
(unresolved type) size_tsize_t _error_ pospos = 0;
foreach ((parameter) ii; 0 .. cast((unresolved type) size_tsize_t) n)
{
const _error_ aa = (cast(Atom*) data)[i];
pos += snprintf(list.list.ptrptr + pos, list.list.lengthlength - pos, "%s%s",
i ? ",".",".ptrptr : ""."".ptrptr, an(dpy, a));
if (pos >= list.list.lengthlength - 64)
break;
}
XFree(data);
emitf("clip_offer", "targets=[%s] count=%lu", list.list.ptrptr, n);
break;
}
// 2) convert the text itself (INCR receive when the owner chunks it)
XConvertSelection(dpy, clipboard, utf8, dest, win, ts);
for (;;)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
if (ev.ev.typetype != SelectionNotify)
continue;
if (ev.xselection.ev.xselection.propertyproperty == None)
{
emit("paste_data refused=1");
return 0;
}
static __gshared ubyte[4 * 1024 * 1024] ubyte[4 * 1024 * 1024] bufbuf;
bool _error_ wasIncrwasIncr;
int _error_ chunkschunks;
const _error_ totaltotal = fetchConverted(dpy, win, dest, incrAtom, buf.buf.ptrptr,
buf.buf.lengthlength, wasIncr, chunks);
emitf("paste_data", "fmt=UTF8_STRING bytes=%zu incr=%d chunks=%d",
total, cast(int) wasIncr, chunks);
if (total <= 64)
emitf("paste_text", "text=\"%.*s\"", cast(int) total, cast(char*) buf.buf.ptrptr);
return 0;
}
}
// -- XDND: both sides in one process ---------------------------------------------
// Target = `win` on `dpy`; source = a second connection (`sdpy`) with its own
// 1x1 unmapped window that owns XdndSelection. Atoms are server-global, so the
// same ids work on both connections; window ids likewise.
struct (struct) app.DndAtomsDndAtoms
{
Atom (field) _error_ app.DndAtoms.awareaware, (field) _error_ app.DndAtoms.selectionselection, (field) _error_ app.DndAtoms.enterenter, (field) _error_ app.DndAtoms.positionposition, (field) _error_ app.DndAtoms.statusstatus, (field) _error_ app.DndAtoms.leaveleave, (field) _error_ app.DndAtoms.dropdrop, (field) _error_ app.DndAtoms.finishedfinished,
(field) _error_ app.DndAtoms.actionCopyactionCopy, (field) _error_ app.DndAtoms.typeListtypeList, (field) _error_ app.DndAtoms.uriListuriList;
}
(struct) app.DndAtomsDndAtoms app.internDndinternDnd(Display* (parameter) Display* dpydpy) @nogc nothrow
{
(unresolved type) DndAtomsDndAtoms _error_ aa;
a.a.awareaware = XInternAtom(dpy, "XdndAware", False);
a.a.selectionselection = XInternAtom(dpy, "XdndSelection", False);
a.a.enterenter = XInternAtom(dpy, "XdndEnter", False);
a.a.positionposition = XInternAtom(dpy, "XdndPosition", False);
a.a.statusstatus = XInternAtom(dpy, "XdndStatus", False);
a.a.leaveleave = XInternAtom(dpy, "XdndLeave", False);
a.a.dropdrop = XInternAtom(dpy, "XdndDrop", False);
a.a.finishedfinished = XInternAtom(dpy, "XdndFinished", False);
a.a.actionCopyactionCopy = XInternAtom(dpy, "XdndActionCopy", False);
a.a.typeListtypeList = XInternAtom(dpy, "XdndTypeList", False);
a.a.uriListuriList = XInternAtom(dpy, "text/uri-list", False);
return a;
}
void app.sendDndMessagesendDndMessage(Display* (parameter) Display* dpydpy, Window (parameter) Window destdest, Atom (parameter) Atom typetype, Window from,
c_long l1, c_long l2, c_long l3, c_long l4) @nogc nothrow
{
XEvent _error_ evev;
ev.xclient.ev.xclient.typetype = ClientMessage;
ev.xclient.window = dest;
ev.xclient.message_type = type;
ev.xclient.ev.xclient.formatformat = 32;
ev.xclient.ev.xclient.datadata.l[0] = cast(c_long) from;
ev.xclient.ev.xclient.datadata.l[1] = l1;
ev.xclient.ev.xclient.datadata.l[2] = l2;
ev.xclient.ev.xclient.datadata.l[3] = l3;
ev.xclient.ev.xclient.datadata.l[4] = l4;
XSendEvent(dpy, dest, False, 0, &ev);
XFlush(dpy);
}
int app.runDndrunDnd(Display* (parameter) Display* dpydpy, Window (parameter) Window winwin, long (parameter) long deadlineUsdeadlineUs) @nogc nothrow
{
const _error_ dada = internDnd(dpy);
// -- target side: announce XdndAware (a window property, not an event mask)
c_long _error_ verver = XDND_VERSION;
XChangeProperty(dpy, win, da.da.awareaware, XA_ATOM, 32, PropModeReplace,
cast(ubyte*)&ver, 1);
XSync(dpy, False); // flush: the source connection reads this property next
emitf("step", "name=XdndAware_set version=%ld window=0x%lx", ver, win);
// -- source side: a second connection, as a separate client would be
Display* _error_ sdpysdpy = XOpenDisplay(null);
const _error_ srootsroot = XRootWindow(sdpy, XDefaultScreen(sdpy));
Window _error_ swinswin = XCreateSimpleWindow(sdpy, sroot, 0, 0, 1, 1, 0, 0, 0);
emitf("step", "name=source_connection fd=%d window=0x%lx",
XConnectionNumber(sdpy), swin);
XSelectInput(sdpy, swin, PropertyChangeMask); // for serverTimestamp
// Source checks the target's XdndAware before any message (spec step 1).
{
Atom _error_ typetype;
int _error_ formatformat;
c_ulong _error_ nn;
auto _error_ pp = readProperty(sdpy, win, da.da.awareaware, False, type, format, n);
emitf("dnd_source", "target_XdndAware=%ld", n >= 1 ? *cast(c_long*) p : -1);
if (p !is null)
XFree(p);
}
// Source owns XdndSelection and advertises 4 types -> the >3-types path:
// XdndEnter carries only 3 atoms; bit 0 of l[1] says "look at XdndTypeList".
(unresolved type) SelOwnerSelOwner _error_ srcsrc;
src.src.dpydpy = sdpy;
src.src.winwin = swin;
src.src.targetsAtomtargetsAtom = XInternAtom(sdpy, "TARGETS", False);
src.src.timestampAtomtimestampAtom = XInternAtom(sdpy, "TIMESTAMP", False);
src.src.incrAtomincrAtom = XInternAtom(sdpy, "INCR", False);
src.src.dataTargetdataTarget = da.da.uriListuriList;
static immutable (unresolved type) stringstring string uriuri = "file:///tmp/wsi-f16-dropped.txt\r\n";
src.src.datadata = cast(const ubyte*) uri.uri.ptrptr;
src.src.lenlen = uri.uri.lengthlength;
src.src.tsts = serverTimestamp(sdpy, swin);
XSetSelectionOwner(sdpy, da.da.selectionselection, swin, src.src.tsts);
Atom[4] _error_ typestypes = [da.da.uriListuriList, XInternAtom(sdpy, "UTF8_STRING", False),
XInternAtom(sdpy, "text/plain", False), XInternAtom(sdpy, "TEXT", False)];
XChangeProperty(sdpy, swin, da.da.typeListtypeList, XA_ATOM, 32, PropModeReplace,
cast(ubyte*) types.types.ptrptr, 4);
emitf("dnd_source", "owns=XdndSelection ts=%lu types=4 type_list_set=1", src.src.tsts);
void void sendEnterAndPosition(int x, int y) nothrow @nogcsendEnterAndPosition(int (parameter) int xx, int (parameter) int yy) @nogc nothrow
{
sendDndMessage(sdpy, win, da.da.enterenter, swin,
(cast(c_long) XDND_VERSION << 24) | 1 /* more-than-3-types bit */ ,
cast(c_long) types[0], cast(c_long) types[1], cast(c_long) types[2]);
sendDndMessage(sdpy, win, da.da.positionposition, swin, 0,
(x << 16) | y, cast(c_long) src.src.tsts, cast(c_long) da.da.actionCopyactionCopy);
emitf("dnd_source", "send=XdndEnter,XdndPosition version=5 more_types=1 pos=%d,%d", x, y);
}
sendEnterAndPosition(100, 100);
// -- the negotiation, multiplexing both connections' fds -------------------
pollfd[2] _error_ pfdspfds;
pfds[0].pfds[0].fdfd = XConnectionNumber(dpy);
pfds[1].pfds[1].fdfd = XConnectionNumber(sdpy);
pfds[0].events = pfds[1].events = POLLIN;
int _error_ statusCountstatusCount = 0;
bool _error_ leftOnceleftOnce = false, _error_ finishedfinished = false;
Time _error_ dropTimedropTime = 0;
while (!finished && nowUs() < deadlineUs)
{
// ---- target events (dpy) ----
while (XPending(dpy) > 0)
{
XEvent _error_ evev;
XNextEvent(dpy, &ev);
if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.enterenter)
{
const _error_ srcWinsrcWin = cast(Window) ev.xclient.ev.xclient.datadata.l[0];
const _error_ moreTypesmoreTypes = (ev.xclient.ev.xclient.datadata.l[1] & 1) != 0;
emitf("dnd_enter", "source=0x%lx version=%ld formats=[%s,%s,%s] more_types=%d",
srcWin, ev.xclient.ev.xclient.datadata.l[1] >> 24,
an(dpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[2]),
an(dpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[3]),
an(dpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[4]), cast(int) moreTypes);
if (moreTypes) // the full list lives on the source window
{
Atom _error_ typetype;
int _error_ formatformat;
c_ulong _error_ nn;
auto _error_ pp = readProperty(dpy, srcWin, da.da.typeListtypeList, False, type, format, n);
char[256] _error_ listlist;
(unresolved type) size_tsize_t _error_ pospos = 0;
foreach ((parameter) ii; 0 .. cast((unresolved type) size_tsize_t) n)
pos += snprintf(list.list.ptrptr + pos, list.list.lengthlength - pos, "%s%s",
i ? ",".",".ptrptr : ""."".ptrptr, an(dpy, (cast(Atom*) p)[i]));
XFree(p);
emitf("dnd_enter", "XdndTypeList=[%s] count=%lu", list.list.ptrptr, n);
}
}
else if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.positionposition)
{
const _error_ pospos = ev.xclient.ev.xclient.datadata.l[2];
emitf("dnd_position", "source=0x%lx pos=%ld,%ld action=%s",
ev.xclient.ev.xclient.datadata.l[0], pos >> 16, pos & 0xffff,
an(dpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[4]));
// XdndStatus: bit0 accept; empty rect (l[2]=l[3]=0) = keep sending
sendDndMessage(dpy, cast(Window) ev.xclient.ev.xclient.datadata.l[0], da.da.statusstatus,
win, 1, 0, 0, cast(c_long) da.da.actionCopyactionCopy);
emit("dnd_status send accept=1 action=XdndActionCopy");
}
else if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.leaveleave)
emit("dnd_leave source_aborted=1 cached_formats_dropped=1");
else if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.dropdrop)
{
dropTime = cast(Time) ev.xclient.ev.xclient.datadata.l[2];
emitf("dnd_drop", "source=0x%lx time=%lu", ev.xclient.ev.xclient.datadata.l[0], dropTime);
// data transfer = a perfectly ordinary selection conversion
XConvertSelection(dpy, da.da.selectionselection, da.da.uriListuriList, da.da.selectionselection,
win, dropTime);
emit("step name=XConvertSelection selection=XdndSelection target=text/uri-list");
}
else if (ev.ev.typetype == SelectionNotify)
{
static __gshared ubyte[4096] ubyte[4096] dbufdbuf;
bool _error_ wasIncrwasIncr;
int _error_ chunkschunks;
const _error_ totaltotal = fetchConverted(dpy, win, ev.xselection.ev.xselection.propertyproperty,
src.src.incrAtomincrAtom, dbuf.dbuf.ptrptr, dbuf.dbuf.lengthlength, wasIncr, chunks);
emitf("dnd_data", "fmt=text/uri-list bytes=%zu uri=\"%.*s\"",
total, cast(int)(total >= 2 ? total - 2 : total), cast(char*) dbuf.dbuf.ptrptr);
sendDndMessage(dpy, swin, da.da.finishedfinished, win, 1 /* accepted */ ,
cast(c_long) da.da.actionCopyactionCopy, 0, 0);
emit("dnd_finished send success=1 action=XdndActionCopy");
}
}
// ---- source events (sdpy) ----
while (XPending(sdpy) > 0)
{
XEvent _error_ evev;
XNextEvent(sdpy, &ev);
if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.statusstatus)
{
statusCount++;
emitf("dnd_source", "recv=XdndStatus accept=%ld action=%s n=%d",
ev.xclient.ev.xclient.datadata.l[1] & 1,
an(sdpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[4]), statusCount);
if (!leftOnce)
{
// exercise the abort path once: Leave, then re-enter
leftOnce = true;
sendDndMessage(sdpy, win, da.da.leaveleave, swin, 0, 0, 0, 0);
emit("dnd_source send=XdndLeave (abort path)");
sendEnterAndPosition(120, 80);
}
else
{
sendDndMessage(sdpy, win, da.da.dropdrop, swin, 0,
cast(c_long) src.src.tsts, 0, 0);
emit("dnd_source send=XdndDrop");
}
}
else if (ev.ev.typetype == SelectionRequest)
handleSelectionRequest(src, ev.xselectionrequest); // same code as clipboard
else if (ev.ev.typetype == ClientMessage && ev.xclient.message_type == da.da.finishedfinished)
{
emitf("dnd_source", "recv=XdndFinished success=%ld action=%s",
ev.xclient.ev.xclient.datadata.l[1] & 1, an(sdpy, cast(Atom) ev.xclient.ev.xclient.datadata.l[2]));
finished = true;
}
}
if (!finished)
poll(pfds.pfds.ptrptr, 2, 100);
}
XDestroyWindow(sdpy, swin);
XCloseDisplay(sdpy);
emitf("teardown", "dnd_complete=%d", cast(int) finished);
return finished ? 0 : 1;
}
// ---------------------------------------------------------------------------
int int D main(string[] args)main((alias) object.string = stringstring[] (parameter) string[] argsargs)
{
void instrument.initInstrument(const(char)* demoName) nothrow @nogcNames the demo, starts the monotonic clock, and emits init_start.
Call this first, before any platform API call.
initInstrument("f16_x11");
const(char)* (local variable) const(char)* modemode = "dnd";
foreach ((parameter) string aa; (parameter) string[] argsargs[1 .. $])
if ((local variable) string aa.(field) ulong string.lengthlength > 7 && (local variable) string aa[0 .. 7] == "--mode=")
(local variable) const(char)* modemode = ((local variable) string aa[7 .. $] ~ '\0').(field) immutable(char)* string.ptrptr;
const (local variable) const(char*) envAutoenvAuto = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_AUTO_EXIT");
const (local variable) const(bool) autoExitautoExit = (local variable) const(char*) envAutoenvAuto !is null && (local variable) const(char*) envAutoenvAuto[0] == '1';
const (local variable) const(int) deadlineUsdeadlineUs = (local variable) const(bool) autoExitautoExit ? 8_000_000 : 30_000_000;
Display* (local variable) _error_ dpydpy = XOpenDisplay(null);
if (dpy is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: no X11 display (XOpenDisplay returned null)\n");
return 0;
}
void instrument.emitf(scope const(char)* kind, scope const(char)* fmt, ...) nothrow @nogcEmit an event with a printf-formatted key=value ... payload.
emitf("step", "name=XOpenDisplay fd=%d mode=%s", XConnectionNumber(dpy), (local variable) const(char)* modemode);
const (local variable) const(_error_) screenscreen = XDefaultScreen(dpy);
Window (local variable) _error_ winwin = XCreateSimpleWindow(dpy, XRootWindow(dpy, screen), 0, 0,
320, 200, 1, XBlackPixel(dpy, screen), XWhitePixel(dpy, screen));
XStoreName(dpy, win, "Sparkles · F16 clipboard+dnd");
XSelectInput(dpy, win, StructureNotifyMask | PropertyChangeMask);
XMapWindow(dpy, win);
void instrument.emitf(scope const(char)* kind, scope const(char)* fmt, ...) nothrow @nogcEmit an event with a printf-formatted key=value ... payload.
emitf("window_created", "xid=0x%lx size=320x200", win);
int (local variable) int rcrc;
if (int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp((local variable) const(char)* modemode, "copy") == 0)
(local variable) int rcrc = runCopy(dpy, win, false, deadlineUs);
else if (int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp((local variable) const(char)* modemode, "copy-incr") == 0)
(local variable) int rcrc = runCopy(dpy, win, true, deadlineUs);
else if (int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp((local variable) const(char)* modemode, "paste") == 0)
(local variable) int rcrc = runPaste(dpy, win);
else
(local variable) int rcrc = runDnd(dpy, win, deadlineUs);
XDestroyWindow(dpy, win);
XCloseDisplay(dpy);
return (local variable) int rcrc;
}