blog · git · desktop · images · contact
2017-04-02
Wenn man ein anderes Betriebssystem benutzt hat, bevor man zu etwas mit X11 gewechselt ist, wird einem recht früh aufgefallen sein, dass es in X mehr als ein Clipboard gibt:
Diese beiden Clipboards überschneiden sich auch oft nicht. Man kann den Inhalt des „Strg+C-Cliboards“ behalten und währenddessen über das „Clipboard der mittleren Maustaste“ etwas anderes kopieren und einfügen.
Wie funktioniert das? Gibt es mehr als ein Clipboard? Wie viele? Unterstützt jeder X-Client jede Art von Clipboard?
Hier ist der passende Abschnitt in ICCCM zu diesem Thema.
Zuerst einmal: Im X11-Land heißen „Clipboards“ eigentlich „Selections“.
Ja, es gibt mehr als eine Selection und sie arbeiten alle unabhängig voneinander. Es ist sogar so, dass man so viele Selections benutzen kann, wie man möchte. Zumindest in der Theorie. Wenn man Selections benutzt, dann weist man verschiedene X-Clients an, miteinander zu reden. Das heißt natürlich, dass die Clients eine gemeinsame Sprache sprechen und sich darüber einig sein müssen, welche Selections sie verwenden wollen. Man kann also nicht einfach eine neue Selection erfinden und dann erwarten, dass Firefox damit kompatibel ist.
Aus der Vogelperspektive sieht das alles ungefähr so aus:
Client A X Server Client B
----------------------------------------------------------------
(1) | I own selection FOO! |
| -------------------> |
| Write sel. FOO to BAR! | (2)
| <--------------------- |
| Write sel. FOO to BAR! |
| <--------------------- |
| Here is FOO.
| -------------------------:-----------------------> |
Okay, got it. |
| <------------------------:------------------------ |
Punkt (1) heißt, dass jeder Client zu jeder Zeit „Ownership“ einer Selection für sich beanspruchen kann. Der Client sagt das dem X-Server nur – es werden noch keine Daten übertragen. Das ist ein wichtiger Punkt. Der X-Server ist in diesem Fall eigentlich nichts weiter als eine Vermittlungsstelle. Er merkt sich, welcher Client welche Selection für sich beansprucht.
Bei (2) fragt ein anderer Client den X-Server nach dem Inhalt der Selection „FOO“. Der X-Server gibt diese Anfrage dann nur an den aktuellen Owner weiter. Client A ist dann dafür verantwortlich, die Daten tatsächlich zu Client B zu übermitteln.
Ich habe das oben „Selection FOO“ genannt, um schon ein bisschen anzureißen, dass das ein ziemlich willkürlicher Bezeichner ist. Wenn man schonmal mit X11 etwas gemacht hat, wird man nicht überrascht sein, dass Selections über „Atoms“ identifiziert werden.
Kurze Wiederholung: Über Atoms kann man in X11 irgendwas identifizieren und die Dinger sind im Wesentlichen Strings. Intern werden ihnen zwar Nummern zugeordnet, aber man muss den X-Server nur selten fragen, „was ist der Name für Atom Nummer 42?“ In der Regel hantiert man als Programmierer nur mit den Strings.
Es gibt drei Selections mit „Standardnamen“:
PRIMARY
: Das „Clipboard der mittleren Maustaste“SECONDARY
: Heutzutage quasi unbenutztCLIPBOARD
: Das „Strg+C-Clipboard“„Standard“ heißt, dass diese Namen in ICCCM 2.6.1 spezifiziert sind. Und, ja, es ist verwirrend, dass eine der Selections „Clipboard“ heißt.
Mit dem, was wir bisher wissen, können wir den X-Server nach den
derzeitigen Ownern fragen. Hier ist xowners.c
:
#include <stdio.h>
#include <X11/Xlib.h>
int
main()
{
Display *dpy;
Window owner;
Atom sel;
char *selections[] = { "PRIMARY", "SECONDARY", "CLIPBOARD", "FOOBAR" };
size_t i;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
for (i = 0; i < sizeof selections / sizeof selections[0]; i++)
{
sel = XInternAtom(dpy, selections[i], False);
owner = XGetSelectionOwner(dpy, sel);
printf("Owner of '%s': 0x%lX\n", selections[i], owner);
}
return 0;
}
Übersetzen kann man dieses und die folgenden Programme mit:
cc -Wall -Wextra -o xowners xowners.c -lX11
FOOBAR
ist ein erfundener Name. Es ist völlig zulässig, den zu
benutzen, aber man darf nicht erwarten, dass er mit allen Clients
funktioniert. :-)
Wie man dann sehen kann, gibt das Programm Window-IDs aus:
$ ./xowners
Owner of 'PRIMARY': 0x60080F
Owner of 'SECONDARY': 0x0
Owner of 'CLIPBOARD': 0x1E00024
Owner of 'FOOBAR': 0x0
Windows sind ein weiteres grundlegendes Mittel in der X11-Welt, damit Clients miteinander kommunizieren können. Sprich, nicht jedes Window ist notwendigerweise ein sichtbares Rechteck mit Pixeln drin. „Unmapped Windows“ können problemlos in einer X-Session existieren (und üblicherweise gibt es davon auch einige).
Mit xwininfo
kann man mehr über die beiden genannten Fenster
herausfinden:
$ xwininfo -id 0x60080F | grep '^xwininfo'
xwininfo: Window id: 0x60080f "xiate"
$ xwininfo -id 0x1E00024 | grep '^xwininfo'
xwininfo: Window id: 0x1e00024 "lariza"
Aha, xiate
ist also derzeit „Besitzer“ der PRIMARY
-Selection und
lariza
der des CLIPBOARD
.
Die volle Ausgabe verrät noch mehr:
$ xwininfo -id 0x60080F
xwininfo: Window id: 0x60080f "xiate"
Absolute upper-left X: -100
Absolute upper-left Y: -100
Relative upper-left X: -100
Relative upper-left Y: -100
Width: 10
Height: 10
Depth: 0
...
Map State: IsUnMapped
...
Es ist also wirklich ein unsichtbares Fenster. Clients machen das oft so. Sie erstellen Fenster, deren einziger Zweck es ist, eine Selection zu verwalten. Sie könnten auch ihre sichtbaren Fenster dazu benutzen, das bringt jedoch ein paar Probleme mit sich. Sichtbare Fenster sind manchmal eher kurzlebig und sobald ein Fenster wieder gelöscht wird, verliert man auch den „Ownership“ einer Selection.
So weit, so gut. Und so einfach.
Es fängt an, kompliziert zu werden, sobald einem auffällt, dass manche Clients das Clipboard ja für Text benutzen, andere für Bilder, wieder andere für Audio-Daten und dann ganz andere Clients für Datentypen, von denen man noch nie etwas gehört hat.
Und dann gibt es noch Situationen, in denen ein Client dieselben Daten in verschiedenen Formen anbieten möchte. Ein Beispiel hierfür: Man markiere Text in einem Webbrowser und kopiere ihn. Fügt man ihn in Vim ein, erhält man Plain Text. Fügt man ihn aber zum Beispiel in LibreOffice Writer ein, dann erhält man auf einmal formatierten Text mit fetter Schrift und Codeblöcken und allem drum und dran.
Man erinnere sich an das Diagramm am Anfang. Schritt 2 war: Client B sagt dem X-Server, er solle „FOO“ nach „BAR“ schreiben lassen. (Was „BAR“ ist, habe ich noch nicht erklärt, aber da kommen wir gleich hin.) Eigentlich müsste es aber heißen: „Schreibe ‚FOO‘ nach ‚BAR‘ und zwar konvertiert zu ‚BAZ‘“. Mit anderen Worten, Client B kann den Inhalt der Selection als Text anfragen oder als Bild oder als irgendetwas anderes.
Deswegen heißt der Bibliotheksaufruf für „gib’ mir den Inhalt einer
Selection“ XConvertSelection()
statt XGetSelection()
.
Das ist nun ein Beispiel für „Client B“:
#include <stdio.h>
#include <X11/Xlib.h>
void
show_utf8_prop(Display *dpy, Window w, Atom p)
{
Atom da, incr, type;
int di;
unsigned long size, dul;
unsigned char *prop_ret = NULL;
/* Dummy call to get type and size. */
XGetWindowProperty(dpy, w, p, 0, 0, False, AnyPropertyType,
&type, &di, &dul, &size, &prop_ret);
XFree(prop_ret);
incr = XInternAtom(dpy, "INCR", False);
if (type == incr)
{
printf("Data too large and INCR mechanism not implemented\n");
return;
}
/* Read the data in one go. */
printf("Property size: %lu\n", size);
XGetWindowProperty(dpy, w, p, 0, size, False, AnyPropertyType,
&da, &di, &dul, &dul, &prop_ret);
printf("%s", prop_ret);
fflush(stdout);
XFree(prop_ret);
/* Signal the selection owner that we have successfully read the
* data. */
XDeleteProperty(dpy, w, p);
}
int
main()
{
Display *dpy;
Window owner, target_window, root;
int screen;
Atom sel, target_property, utf8;
XEvent ev;
XSelectionEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
sel = XInternAtom(dpy, "CLIPBOARD", False);
utf8 = XInternAtom(dpy, "UTF8_STRING", False);
owner = XGetSelectionOwner(dpy, sel);
if (owner == None)
{
printf("'CLIPBOARD' has no owner\n");
return 1;
}
printf("0x%lX\n", owner);
/* The selection owner will store the data in a property on this
* window: */
target_window = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
/* That's the property used by the owner. Note that it's completely
* arbitrary. */
target_property = XInternAtom(dpy, "PENGUIN", False);
/* Request conversion to UTF-8. Not all owners will be able to
* fulfill that request. */
XConvertSelection(dpy, sel, utf8, target_property, target_window,
CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionNotify:
sev = (XSelectionEvent*)&ev.xselection;
if (sev->property == None)
{
printf("Conversion could not be performed.\n");
return 1;
}
else
{
show_utf8_prop(dpy, target_window, target_property);
return 0;
}
break;
}
}
}
Mehr Code als erwartet? Tjoa. Nur Geduld. Wir gehen es einzeln durch.
Erst einmal enthüllen wir, was „BAR“ ist. Der Code oben erzeugt ein
Fenster namens target_window
und dann fragt er ein Atom namens
target_property
an. Diese beiden Dinge zusammengenommen ergeben „BAR“.
Wenn Client A den Inhalt der Selection zu Client B schicken will, dann
tut er das, indem er den Inhalt in die genannte Property des genannten
Windows schreibt. Das ist so ziemlich der einzige Weg, mit dem zwei
X-Clients beliebige Daten über den X-Server kommunizieren können.
Man erinnere sich, dass X11 netzwerktransparent ist. Die Clients A und B müssen nicht auf demselben Host laufen. Sie müssen noch nicht einmal dieselben Netzwerkprotokolle sprechen. Vielleicht spricht der eine TCP/IP und der andere … irgendwas anderes. ICCCM bringt hier als Beispiel das DECnet, was aber heute vermutlich niemand mehr benutzt. Das Ergebnis ist jedenfalls, dass die beiden Clients nicht direkt kommunizieren dürfen, sondern alles irgendwie über den X-Server schicken müssen.
Okay. Unser Ziel „BAR“ ist also ein Fenster und eine Property.
Wir brauchen noch einen Content Type. Ich habe hier UTF8_STRING
benutzt. Dieses Atom wird man in ICCCM nicht finden. UTF-8 hat noch
nicht einmal existiert, als ICCCM ursprünglich veröffentlicht wurde.
Neuere Clients kommen damit jedoch klar.
Wir bitten den X-Server dann, die Konvertierung „durchzuführen“:
XConvertSelection()
. Jetzt nochmal genau auf das Diagramm am Anfang
schauen. Es gibt keine unmittelbare Antwort auf XConvertSelection()
.
Der X-Server vermittelt die Anfrage nur weiter und schickt sie an Client
A, sofern es im Moment überhaupt einen Client gibt, der die Selection
für sich in Anspruch nimmt. Dann, irgendwann in der Zukunft, erledigt
Client A seine Arbeit – oder vielleicht auch nicht. Das heißt, dass wir
nur auf ein X-Event warten können. Dafür ist die Schleife am Ende da.
Das Event SelectionNotify
sagt uns, dass eine Konvertierung einer
Selection durchgeführt wurde oder fehlgeschlagen ist. Wir können dann
vielleicht weitermachen und die Property an unserem Fenster auslesen;
Client A sollte seine Daten dort hingeschrieben haben.
Erwähnenswert weiterhin:
XDeleteProperty()
teilt Client A mit, dass wir die
von ihm übermittelten Daten erfolgreich gelesen haben.Das ist nun die andere Richtung. Ein Client beansprucht CLIPBOARD
für
sich und liefert Daten, sofern man nach UTF8_STRING
als Typ fragt. Das
wäre also Client A:
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <X11/Xlib.h>
void
send_no(Display *dpy, XSelectionRequestEvent *sev)
{
XSelectionEvent ssev;
char *an;
an = XGetAtomName(dpy, sev->target);
printf("Denying request of type '%s'\n", an);
if (an)
XFree(an);
/* All of these should match the values of the request. */
ssev.type = SelectionNotify;
ssev.requestor = sev->requestor;
ssev.selection = sev->selection;
ssev.target = sev->target;
ssev.property = None; /* signifies "nope" */
ssev.time = sev->time;
XSendEvent(dpy, sev->requestor, True, NoEventMask, (XEvent *)&ssev);
}
void
send_utf8(Display *dpy, XSelectionRequestEvent *sev, Atom utf8)
{
XSelectionEvent ssev;
time_t now_tm;
char *now, *an;
now_tm = time(NULL);
now = ctime(&now_tm);
an = XGetAtomName(dpy, sev->property);
printf("Sending data to window 0x%lx, property '%s'\n", sev->requestor, an);
if (an)
XFree(an);
XChangeProperty(dpy, sev->requestor, sev->property, utf8, 8, PropModeReplace,
(unsigned char *)now, strlen(now));
ssev.type = SelectionNotify;
ssev.requestor = sev->requestor;
ssev.selection = sev->selection;
ssev.target = sev->target;
ssev.property = sev->property;
ssev.time = sev->time;
XSendEvent(dpy, sev->requestor, True, NoEventMask, (XEvent *)&ssev);
}
int
main()
{
Display *dpy;
Window owner, root;
int screen;
Atom sel, utf8;
XEvent ev;
XSelectionRequestEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
/* We need a window to receive messages from other clients. */
owner = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
sel = XInternAtom(dpy, "CLIPBOARD", False);
utf8 = XInternAtom(dpy, "UTF8_STRING", False);
/* Claim ownership of the clipboard. */
XSetSelectionOwner(dpy, sel, owner, CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionClear:
printf("Lost selection ownership\n");
return 1;
break;
case SelectionRequest:
sev = (XSelectionRequestEvent*)&ev.xselectionrequest;
printf("Requestor: 0x%lx\n", sev->requestor);
/* Property is set to None by "obsolete" clients. */
if (sev->target != utf8 || sev->property == None)
send_no(dpy, sev);
else
send_utf8(dpy, sev, utf8);
break;
}
}
}
Es wird ein unsichtbares Fenster erzeugt und dann die Selection
CLIPBOARD
für sich beansprucht. Wie man sieht, besitzt also nicht „der
Client“ eine Selection, sondern ein konkretes Fenster.
Das Programm wartet dann auf Events. SelectionClear
ist einfach:
Irgendein anderer Client kann Ownership des Clipboards übernommen haben.
Ja, das kann zu jedem Zeitpunkt einfach so passieren.
SelectionRequest
wird vom X-Server zu Client A geschickt. Das ist das
Event, das erzeugt wird, wenn ein anderer Client XConvertSelection()
ausführt. Wir prüfen dann ganz einfach, ob der Zieltyp target
gleich
UTF8_STRING
ist. Falls nicht, verweigern wir die Konversion.
Andernfalls rufen wir letztendlich XChangeProperty()
auf, um am
angegebenen Zielfenster die angegebene Property zu setzen. Wenn wir
damit fertig sind, schicken wir selbst ein SelectionNotify
-Event an
Client B.
Dieser Client schickt nun die aktuelle Uhrzeit mit Datum an den Anfragesteller. Das habe ich als Beispiel gewählt, um zu zeigen, dass bei Clipboards keine Nutzdaten im X-Server gespeichert werden. Stattdessen werden die Daten nur auf Anfrage konvertiert (und vielleicht sogar dann erst erzeugt).
TARGETS
Ein paar besondere Inhaltstypen gibt es. Man kann den Owner einer
Selection zum Beispiel nach dem Typ TARGETS
fragen. Dabei wird keine
tatsächliche Konvertierung der Nutzdaten passieren, sondern man bekommt
als Antwort nur eine Liste von Atomen. Jedes davon ist ein gültiges
Target für den aktuellen Inhalt dieses Clipboards.
#include <stdio.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
void
show_targets(Display *dpy, Window w, Atom p)
{
Atom type, *targets;
int di;
unsigned long i, nitems, dul;
unsigned char *prop_ret = NULL;
char *an = NULL;
/* Read the first 1024 atoms from this list of atoms. We don't
* expect the selection owner to be able to convert to more than
* 1024 different targets. :-) */
XGetWindowProperty(dpy, w, p, 0, 1024 * sizeof (Atom), False, XA_ATOM,
&type, &di, &nitems, &dul, &prop_ret);
printf("Targets:\n");
targets = (Atom *)prop_ret;
for (i = 0; i < nitems; i++)
{
an = XGetAtomName(dpy, targets[i]);
printf(" '%s'\n", an);
if (an)
XFree(an);
}
XFree(prop_ret);
XDeleteProperty(dpy, w, p);
}
int
main()
{
Display *dpy;
Window target_window, root;
int screen;
Atom sel, targets, target_property;
XEvent ev;
XSelectionEvent *sev;
dpy = XOpenDisplay(NULL);
if (!dpy)
{
fprintf(stderr, "Could not open X display\n");
return 1;
}
screen = DefaultScreen(dpy);
root = RootWindow(dpy, screen);
sel = XInternAtom(dpy, "CLIPBOARD", False);
targets = XInternAtom(dpy, "TARGETS", False);
target_property = XInternAtom(dpy, "PENGUIN", False);
target_window = XCreateSimpleWindow(dpy, root, -10, -10, 1, 1, 0, 0, 0);
XConvertSelection(dpy, sel, targets, target_property, target_window,
CurrentTime);
for (;;)
{
XNextEvent(dpy, &ev);
switch (ev.type)
{
case SelectionNotify:
sev = (XSelectionEvent*)&ev.xselection;
if (sev->property == None)
{
printf("Conversion could not be performed.\n");
return 1;
}
else
{
show_targets(dpy, target_window, target_property);
return 0;
}
break;
}
}
}
Wenn man das nun laufen lässt, während ein typischer GTK-Client Owner einer Selection ist, dann sieht man etwas Interessantes:
$ ./xtargets
Targets:
'TIMESTAMP'
'TARGETS'
'MULTIPLE'
'SAVE_TARGETS'
'UTF8_STRING'
'COMPOUND_TEXT'
'TEXT'
'STRING'
'text/plain;charset=utf-8'
'text/plain'
X11 ist alt und es gibt viele Konventionen, um Datentypen anzugeben. Manche davon sind Altlasten, andere missverständlich, viele gar nicht von ICCCM erwähnt. MIME-Types sind heute vermutlich okay, aber in ICCCM liest man nichts davon.
Das fühlt sich recht chaotisch an. Das ist der Preis, den man zahlen muss, wenn man noch mit 30-Jahre alten Clients kompatibel sein will.
xclip
handhabenIch habe mich schon lange gefragt, wieso ich denn mit xclip
kein Bilder einfügen kann. Sollte doch eigentlich einfach sein: xclip
-o >foo.img
. Naja, nee. Jetzt weiß ich, wie es geht, und dann ist es
tatsächlich einfach. :-)
Zunächst kopiere man ein Bild mit einem Tool wie The GIMP.
xclip
kann den Typ TARGETS
erfragen:
$ xclip -o -target TARGETS -selection clipboard
TIMESTAMP
TARGETS
MULTIPLE
SAVE_TARGETS
image/png
image/tiff
image/x-icon
image/x-ico
image/x-win-bitmap
image/vnd.microsoft.icon
application/ico
image/ico
image/icon
text/ico
image/bmp
image/x-bmp
image/x-MS-bmp
image/jpeg
Man wähle etwas Geeignetes aus und frage dann die Daten an:
$ xclip -o -target image/png -selection clipboard >foo.png
$ file foo.png
foo.png: PNG image data, 373 x 309, 8-bit/color RGBA, non-interlaced
Nicht kompliziert. Auf diese Weise kann man mit xclip
dann auch
Bilddaten kopieren, man muss nur mit -t
einen MIME-Typ angeben.
(Es sei übrigens ergänzend erwähnt, dass die damalige xclip
-Version
aus Ubuntu 12.04 sehr wahrscheinlich noch nicht mit TARGETS
umgehen
konnte. Ich habe kein 12.04 mehr zur Hand, um es auszuprobieren, aber
die Version, die man bei Ubuntu herunterladen kann, erwähnt
in den Command Line Options kein -target
.)
Aufgefallen ist vielleicht, dass Programm Nummer 2 oben abbricht, wenn
da irgendwas mit INCR
vorkommt. Das ist einer der vielen Hacks im
Selection-Umfeld.
Eine Property an einem Fenster kann nur eine beschränkte Menge an Daten enthalten, da die Property im Speicher des X-Servers existiert. Will man also mehrere Megabyte mittels Selections übermitteln, dann muss man dafür als Sender seine Daten in kleinere Chunks aufteilen. Client B muss die Daten entsprechend wieder zusammensetzen. So ein Chunk ist wohl ungefähr 256 kB groß. Nicht so viel, reicht aber tatsächlich in den meisten Fällen. Schade ist, dass das jeder Client neu für sich implementieren muss. Das macht Clients komplexer.
Im Alltag ist sowas vielleicht schon passiert: Man öffnet ein Fenster, markiert Text, drückt Strg+C, schließt das Fenster – und dann? Ist der Text wieder aus dem Clipboard raus. Klar, denn es gibt ja gar kein Clipboard im Server. Der eben geschlossene Client hat die Daten vorgehalten und darauf gewartet, dass jemand danach fragt. Jetzt ist dieser Client weg und damit auch die Daten. Von anderen Betriebssystemen ist man das anders gewohnt. Aber selbst wenn alle Betriebssysteme so arbeiten würden, wäre das Verhalten immer noch ein bisschen lästig.
Es gibt dafür keine „saubere“ Lösung in X11. Stattdessen schlägt ICCCM vor, einen Clipboard-Manager zu schreiben. Der funktioniert ungefähr so:
Das klingt danach, als seien da einige Race Conditions im Spiel.
Außerdem geht es kaputt, wenn ein Client den TARGETS
-Typ nicht
unterstützt. Es wird allerdings von ICCCM verlangt, dass ein Client auf
eine TARGETS
-Anfrage antworten kann, also „sollte“ das schon
funktionieren.
Ich denke, es ist wichtig, zu verstehen, dass der X-Server nur eine Vermittlungsstelle ist. Die Clients reden mittels des Server direkt miteinander und tauschen so die Nutzdaten aus. Es gibt kein Clipboard „im“ Server. Daten werden nur auf Anfrage konvertiert beziehungsweise erzeugt. Man kann so viele Selections benutzen, wie man möchte, aber nicht alle Clients unterstützen alle davon.
Eine schließende Anmerkung: Auf den ersten Blick wirken X11-Selections ziemlich einfach. Ich befürchte aber, dass die bald annähernd so komplex sind wie Zeitzonen. Selbst das „Standard“-Utility xclip ist nicht vollständig ICCCM-kompatibel und enthält hier und da ein „FIXME“. Es gibt viele Race Conditions und Sonderfälle.
tl; dr: Wenn irgendwie möglich, benutz’ eine Bibliothek dafür.
Der ursprüngliche Code von 2017 hat XSelectInput()
aufgerufen. Ich
wollte dem X-Server sagen, dass meine Fenster Events wie
SelectionNotify
empfangen sollen. Das war aber falsch. Es gibt für
diese Events schlichtweg keine Masken und man muss sie nicht
selektieren.
Danke, Ulrich!