kick/tests/xleader.c
Andrew Hamilton 08202dc6c3 fix X11 app startup: no forced toolkit backends; X11 windows join tabs at first map
GDK_BACKEND/QT_QPA_PLATFORM forced to wayland stripped toolkits' fallback,
killing X11-only builds like emacs-gtk ('cannot open display' against a
healthy Xwayland). Children now self-select from WAYLAND_DISPLAY/DISPLAY,
and XAUTHORITY is cleared.

Separately, X11 membership decisions moved from window-creation time to
first map: GTK leader windows never map, yet claimed the tab slot at
creation, leaving a permanent black tab while the real frame was demoted
to a stacked member -- and Alt-cycling onto that phantom main member
crashed on wlr_seat_keyboard_notify_enter(NULL). Never-mapped windows now
free silently; keyboard-entry paths guard on live surfaces. New
tests/xleader.c probe (unmapped leader + mapped frame) locks the behaviour
in.
2026-08-24 10:43:41 +10:00

85 lines
2.9 KiB
C

/* xleader: an X11 twin-window probe that mimics how GTK apps appear on
* X11 -- an invisible "leader" window is created first and never mapped,
* then the real frame maps. A compositor that claims windows at map time
* (instead of creation time) gives this program exactly one tab; one that
* claims at creation time steals the tab slot for the leader, which then
* renders as a permanent black tab while the real frame gets demoted to a
* tab-group member.
*
* Both windows carry _NET_WM_PID so process-group matching can claim them.
* The frame fills itself with a solid colour so pixel checks can tell it
* apart from an unrendered tab.
*
* Build: cc xleader.c -o xleader -lX11 $(pkg-config --cflags xcb xcb-icccm)
*/
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void set_pid(Display *d, Window w) {
pid_t pid = getpid();
Atom net_pid = XInternAtom(d, "_NET_WM_PID", False);
XChangeProperty(d, w, net_pid, XA_CARDINAL, 32, PropModeReplace,
(unsigned char *)&pid, 1);
}
static void fill_frame(Display *d, Window w, int wpx, int hpx)
{
XWMHints hints;
memset(&hints, 0, sizeof(hints));
hints.flags = InputHint | StateHint;
hints.input = True;
hints.initial_state = NormalState;
XSetWMHints(d, w, &hints);
XGCValues gcv;
GC gc = XCreateGC(d, w, 0, &gcv);
XSetForeground(d, gc, 0x306090UL); /* muted blue: visible, not black */
XFillRectangle(d, w, gc, 0, 0, wpx, hpx);
XFreeGC(d, gc);
XFlush(d);
}
int main(int argc, char **argv)
{
int wpx = argc > 1 ? atoi(argv[1]) : 640;
int hpx = argc > 2 ? atoi(argv[2]) : 480;
Display *d = XOpenDisplay(NULL);
if (!d) {
fprintf(stderr, "xleader: cannot open display\n");
return 1;
}
int scr = DefaultScreen(d);
Window root = RootWindow(d, scr);
/* The leader: created, given WM_CLASS + pid, never mapped. */
Window leader = XCreateSimpleWindow(d, root, 0, 0, 8, 8, 0, 0, 0);
XStoreName(d, leader, "xleader-leader");
set_pid(d, leader);
/* The real frame: created and mapped. The background pixel is the
probe colour so the window stays "filled" even right after a resize
clears its backing pixmap (before any Expose-driven repaint lands). */
Window frame = XCreateSimpleWindow(d, root, 0, 0, wpx, hpx, 0,
BlackPixel(d, scr),
0x306090UL);
XStoreName(d, frame, "xleader-frame");
set_pid(d, frame);
XSelectInput(d, frame, ExposureMask | StructureNotifyMask);
XMapWindow(d, frame);
fill_frame(d, frame, wpx, hpx);
XFlush(d);
for (;;) {
XEvent ev;
XNextEvent(d, &ev); /* blocks: keeps the client alive */
if (ev.type == Expose && ev.xexpose.window == frame)
fill_frame(d, frame, wpx, hpx); /* stay painted after resizes */
}
return 0;
}