// F06 keyboard & keymap on Wayland (../../f06-keyboard.md): scancode → keysym
// → text are three different questions, and Wayland answers NONE of them in
// the protocol — wl_keyboard.key carries a bare evdev scancode, and everything
// above it is the client's job, via libxkbcommon:
//
// 1. wl_keyboard.keymap hands the client an fd + size; the client mmaps it
// (MAP_PRIVATE, per the protocol) and compiles it with
// xkb_keymap_new_from_string. The compositor can re-send it at ANY time
// (layout switch, a new input device with a different map) — the demo
// logs every `keymap_event` and rebuilds xkb_keymap/xkb_state each time.
// 2. wl_keyboard.modifiers carries raw mask+group; xkb_state_update_mask
// applies it. Sym and UTF-8 for a key are then
// xkb_state_key_get_one_sym / xkb_state_key_get_utf8 at keycode
// (= evdev code + 8, the historical X11 offset xkbcommon keeps).
// 3. KEY REPEAT IS CLIENT-SIDE: wl_keyboard.repeat_info (v4+) only states
// rate/delay; no repeated key events ever arrive. The demo implements
// repeat with a timerfd (the F05 external-fd pattern) and proves both
// mandatory cancellations: on wl_keyboard.key release and on
// wl_keyboard.leave (focus loss).
// 4. Dead-key compose is ALSO client-side: pressed syms feed an
// xkb_compose_state (table from the locale); `compose state=…`
// transitions and the composed text are logged.
//
// Every press/release logs `key code=<evdev+8> sym=<name> text=<utf8>
// state=down|up repeat=0|1`. Headless weston advertises NO wl_seat, so the
// Tier-A run uses a wlroots headless compositor (sway) + wtype's
// zwp_virtual_keyboard_v1 injection — see ../../f06-keyboard.md for the
// choreography. No compositor → SKIP; no seat → SKIP (exit 0 both ways).
//
// Based on the scaffold (../scaffold/app.d, findings ../../scaffold.md);
// instrumentation contract: ./instrument.d.
module (module) appapp;
import c; // ImportC: wayland + xdg-shell + xkbcommon + timerfd/poll + wsi_*
import instrument;
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;
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.atoll = long core.stdc.stdlib.atoll(scope const(char*) nptr) nothrow @nogcatoll, (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv;
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.strcmp = int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp;
// ----------------------------------------------------------------- tunables
enum int (constant) int app.defaultWidth = 640defaultWidth = 640;
enum int (constant) int app.defaultHeight = 480defaultHeight = 480;
enum int (constant) int app.pollTimeoutMs = 250pollTimeoutMs = 250;
enum long (constant) long app.defaultCapUs = 30000000LdefaultCapUs = 30_000_000; // WSI_AUTO_EXIT=1 run length (override: WSI_F06_CAP_US)
// -------------------------------------------------------------------- state
/// One wl_shm-backed ARGB8888 buffer (scaffold pattern).
struct (struct) app.BufferOne wl_shm-backed ARGB8888 buffer (scaffold pattern).
Buffer
{
wl_buffer* (field) _error_ app.Buffer.handlehandle;
uint* (field) uint* app.Buffer.pixelspixels;
(alias) object.size_t = ulongsize_t (field) ulong app.Buffer.byteSizebyteSize;
int (field) int app.Buffer.widthwidth, (field) int app.Buffer.heightheight;
bool (field) bool app.Buffer.busybusy;
}
__gshared
{
wl_display* _error_ app.g_displayg_display;
wl_registry* _error_ app.g_registryg_registry;
wl_compositor* _error_ app.g_compositorg_compositor;
wl_shm* _error_ app.g_shmg_shm;
wl_seat* _error_ app.g_seatg_seat;
wl_keyboard* _error_ app.g_keyboardg_keyboard;
xdg_wm_base* _error_ app.g_wmBaseg_wmBase;
wl_surface* _error_ app.g_surfaceg_surface;
xdg_surface* _error_ app.g_xdgSurfaceg_xdgSurface;
xdg_toplevel* _error_ app.g_toplevelg_toplevel;
wl_callback* _error_ app.g_frameCbg_frameCb;
(struct) app.BufferOne wl_shm-backed ARGB8888 buffer (scaffold pattern).
Buffer[2] _error_ app.g_buffersg_buffers;
int (__gshared global) int app.g_widthg_width = defaultWidth;
int (__gshared global) int app.g_heightg_height = defaultHeight;
int (__gshared global) int app.g_pendingWidthg_pendingWidth, (__gshared global) int app.g_pendingHeightg_pendingHeight;
bool (__gshared global) bool app.g_configuredg_configured, (__gshared global) bool app.g_presentedg_presented;
bool (__gshared global) bool app.g_runningg_running = true;
bool (__gshared global) bool app.g_autoExitg_autoExit;
int (__gshared global) int app.g_framesg_frames, (__gshared global) int app.g_commitsg_commits;
// xkbcommon: the client-owned keyboard state machine.
xkb_context* _error_ app.g_xkbCtxg_xkbCtx;
xkb_keymap* _error_ app.g_xkbKeymapg_xkbKeymap;
xkb_state* _error_ app.g_xkbStateg_xkbState;
xkb_compose_table* _error_ app.g_composeTableg_composeTable;
xkb_compose_state* _error_ app.g_composeStateg_composeState;
// Client-side repeat (the F05 timerfd pattern).
int (__gshared global) int app.g_repeatTimerfdg_repeatTimerfd = -1;
int (__gshared global) int app.g_repeatRateg_repeatRate; // characters/second, 0 = disabled (repeat_info)
int (__gshared global) int app.g_repeatDelayMsg_repeatDelayMs;
uint (__gshared global) uint app.g_repeatKeycodeg_repeatKeycode; // 0 = no repeat armed
// Findings counters.
int (__gshared global) int app.g_keymapCountg_keymapCount, (__gshared global) int app.g_keyEventsg_keyEvents, (__gshared global) int app.g_repeatSynthg_repeatSynth;
int (__gshared global) int app.g_cancelReleaseg_cancelRelease, (__gshared global) int app.g_cancelFocusg_cancelFocus;
}
// -------------------------------------------------------- key-event helpers
/// Escape control bytes as \xNN so `text=` stays one parseable log token.
void void app.sanitize(const(char)* src, char* dst, ulong dstLen) nothrow @nogcEscape control bytes as \xNN so text= stays one parseable log token.
sanitize(const(char)* (parameter) const(char)* srcsrc, char* (parameter) char* dstdst, (alias) object.size_t = ulongsize_t (parameter) ulong dstLendstLen) nothrow @nogc
{
static char char app.sanitize.hexDigit(uint v) pure nothrow @nogc @safehexDigit(uint (parameter) uint vv) nothrow @nogc
{
return cast(char)((parameter) uint vv < 10 ? '0' + (parameter) uint vv : 'a' + (parameter) uint vv - 10);
}
(alias) object.size_t = ulongsize_t (local variable) ulong jj;
for ((alias) object.size_t = ulongsize_t (local variable) ulong ii = 0; src[i] != 0 && j + 5 < dstLen; i++)
{
immutable ubyte (local variable) immutable(ubyte) bb = (parameter) const(char)* srcsrc[(local variable) ulong ii];
if ((local variable) immutable(ubyte) bb >= 0x20 && (local variable) immutable(ubyte) bb != 0x7f)
(parameter) char* dstdst[(local variable) ulong jj++] = (local variable) immutable(ubyte) bb;
else
{
(parameter) char* dstdst[(local variable) ulong jj++] = '\\';
(parameter) char* dstdst[(local variable) ulong jj++] = 'x';
(parameter) char* dstdst[(local variable) ulong jj++] = char app.sanitize.hexDigit(uint v) pure nothrow @nogc @safehexDigit((local variable) immutable(ubyte) bb >> 4);
(parameter) char* dstdst[(local variable) ulong jj++] = char app.sanitize.hexDigit(uint v) pure nothrow @nogc @safehexDigit((local variable) immutable(ubyte) bb & 0xf);
}
}
(parameter) char* dstdst[(local variable) ulong jj] = 0;
}
/// Resolve keycode → (sym name, UTF-8 text) through the current xkb_state.
void void app.resolveKey(uint keycode, ref char[64] symName, ref char[64] text) nothrow @nogcResolve keycode → (sym name, UTF-8 text) through the current xkb_state.
resolveKey(uint (parameter) uint keycodekeycode, ref char[64] (parameter) char[64] symNamesymName, ref char[64] (parameter) char[64] texttext) nothrow @nogc
{
(parameter) char[64] symNamesymName[0] = '?';
(parameter) char[64] symNamesymName[1] = 0;
(parameter) char[64] texttext[0] = 0;
if (g_xkbState is null)
return;
immutable (local variable) immutable(_error_) symsym = xkb_state_key_get_one_sym(g_xkbState, keycode);
xkb_keysym_get_name(sym, symName.symName.ptrptr, symName.symName.lengthlength);
xkb_state_key_get_utf8(g_xkbState, keycode, text.text.ptrptr, text.text.lengthlength);
}
void void app.logKeyLine(uint keycode, in char[64] symName, const(char)* text, bool down, int repeat) nothrow @nogclogKeyLine(uint (parameter) uint keycodekeycode, in char[64] (parameter) const(char[64]) symNamesymName, const(char)* (parameter) const(char)* texttext, bool (parameter) bool downdown, int (parameter) int repeatrepeat) nothrow @nogc
{
char[128] (local variable) char[128] safesafe = void;
void app.sanitize(const(char)* src, char* dst, ulong dstLen) nothrow @nogcEscape control bytes as \xNN so text= stays one parseable log token.
sanitize((parameter) const(char)* texttext, (local variable) char[128] safesafe.(constant) char* char[128].ptr = &safeptr, (local variable) char[128] safesafe.(constant) ulong char[128].length = 128LUlength);
instrEvent("key", "code=%u sym=%s text=%s state=%s repeat=%d",
keycode, symName.symName.ptrptr, safe.safe.ptrptr, down ? "down"."down".ptrptr : "up"."up".ptrptr, repeat);
}
// --------------------------------------------------- client-side key repeat
/// Arm the repeat timerfd: one-shot delay, then the rate interval — exactly
/// the contract wl_keyboard.repeat_info describes but does not implement.
void void app.armRepeat(uint keycode) nothrow @nogcArm the repeat timerfd: one-shot delay, then the rate interval — exactly
the contract wl_keyboard.repeat_info describes but does not implement.
armRepeat(uint (parameter) uint keycodekeycode) nothrow @nogc
{
if ((__gshared global) int app.g_repeatRateg_repeatRate <= 0) // rate 0 = "repeat disabled" per the protocol
return;
if (g_xkbKeymap is null || !xkb_keymap_key_repeats(g_xkbKeymap, keycode))
return; // keymap says this key does not repeat (modifiers etc.)
(__gshared global) uint app.g_repeatKeycodeg_repeatKeycode = (parameter) uint keycodekeycode;
immutable long (local variable) immutable(long) periodNsperiodNs = 1_000_000_000L / (__gshared global) int app.g_repeatRateg_repeatRate;
itimerspec (local variable) _error_ itsits;
its.it_value.tv_sec = g_repeatDelayMs / 1000;
its.it_value.tv_nsec = cast(long)(g_repeatDelayMs % 1000) * 1_000_000;
its.it_interval.tv_sec = periodNs / 1_000_000_000;
its.it_interval.tv_nsec = periodNs % 1_000_000_000;
timerfd_settime(g_repeatTimerfd, 0, &its, null);
instrEvent("repeat_arm", "code=%u delay_ms=%d rate_hz=%d", keycode,
g_repeatDelayMs, g_repeatRate);
}
void void app.cancelRepeat(const(char)* reason) nothrow @nogccancelRepeat(const(char)* (parameter) const(char)* reasonreason) nothrow @nogc
{
if ((__gshared global) uint app.g_repeatKeycodeg_repeatKeycode == 0)
return;
itimerspec (local variable) _error_ zerozero; // all-zero it_value disarms the timer
timerfd_settime(g_repeatTimerfd, 0, &zero, null);
instrEvent("repeat_cancel", "reason=%s code=%u", reason, g_repeatKeycode);
(__gshared global) uint app.g_repeatKeycodeg_repeatKeycode = 0;
}
/// Repeat timer fired: synthesize `repeat=1` key events from the *current*
/// xkb_state (so held-key repeats track live modifier changes).
void void app.drainRepeatTimer() nothrow @nogcRepeat timer fired: synthesize repeat=1 key events from the current
xkb_state (so held-key repeats track live modifier changes).
drainRepeatTimer() nothrow @nogc
{
ulong (local variable) ulong expirationsexpirations;
if (read(g_repeatTimerfd, &expirations, expirations.expirations.sizeofsizeof) != (local variable) ulong expirationsexpirations.sizeof)
return;
if ((__gshared global) uint app.g_repeatKeycodeg_repeatKeycode == 0)
return; // raced with a cancel — the read already cleared the fd
char[64] (local variable) char[64] symNamesymName = void, (local variable) char[64] texttext = void;
void app.resolveKey(uint keycode, ref char[64] symName, ref char[64] text) nothrow @nogcResolve keycode → (sym name, UTF-8 text) through the current xkb_state.
resolveKey((__gshared global) uint app.g_repeatKeycodeg_repeatKeycode, (local variable) char[64] symNamesymName, (local variable) char[64] texttext);
foreach ((local variable) ulong ii; 0 .. (local variable) ulong expirationsexpirations)
{
(__gshared global) int app.g_repeatSynthg_repeatSynth++;
void app.logKeyLine(uint keycode, in char[64] symName, const(char)* text, bool down, int repeat) nothrow @nogclogKeyLine((__gshared global) uint app.g_repeatKeycodeg_repeatKeycode, (local variable) char[64] symNamesymName, (local variable) char[64] texttext.(constant) char* char[64].ptr = &textptr, true, 1);
}
}
// ------------------------------------------------------ wl_keyboard events
/// wl_keyboard.keymap: the compositor serializes the WHOLE keymap into an fd.
/// Can arrive again at any time (layout switch / new device) — each arrival
/// replaces the client's xkb_keymap + xkb_state.
extern (C) void app.onKbKeymapwl_keyboard.keymap: the compositor serializes the WHOLE keymap into an fd.
Can arrive again at any time (layout switch / new device) — each arrival
replaces the client's xkb_keymap + xkb_state.
onKbKeymap(void* data, wl_keyboard* kb, uint format, int (parameter) int fdfd,
uint (parameter) uint sizesize) nothrow @nogc
{
g_keymapCount++;
instrEvent("keymap_event", "format=%u size=%u n=%d", format, size, g_keymapCount);
if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
{
close(fd);
return;
}
// "From version 7 onwards, the fd must be mapped with MAP_PRIVATE."
void* _error_ memmem = mmap(null, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mem is cast(void*)-1)
{
close(fd);
return;
}
auto _error_ keymapkeymap = xkb_keymap_new_from_string(g_xkbCtx, cast(const(char)*) mem,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
munmap(mem, size);
close(fd);
if (keymap is null)
{
instrEvent("keymap_parse_failed");
return;
}
if (g_xkbState !is null)
xkb_state_unref(g_xkbState);
if (g_xkbKeymap !is null)
xkb_keymap_unref(g_xkbKeymap);
g_xkbKeymap = keymap;
g_xkbState = xkb_state_new(keymap);
const _error_ layout0layout0 = xkb_keymap_layout_get_name(keymap, 0);
instrEvent("keymap_parsed", "layouts=%u layout0=%s",
xkb_keymap_num_layouts(keymap), layout0 !is null ? layout0 : "(unnamed)"."(unnamed)".ptrptr);
cancelRepeat("keymap_replaced"); // old keycode semantics are gone
}
extern (C) void app.onKbEnteronKbEnter(void* data, wl_keyboard* kb, uint serial,
wl_surface* s, wl_array* keys) nothrow @nogc
{
instrEvent("keyboard_enter", "serial=%u pressed=%zu", serial, keys.keys.sizesize / 4);
}
/// Focus loss kills the repeat timer — the second mandatory cancellation.
extern (C) void app.onKbLeaveFocus loss kills the repeat timer — the second mandatory cancellation.
onKbLeave(void* data, wl_keyboard* kb, uint serial,
wl_surface* s) nothrow @nogc
{
instrEvent("keyboard_leave", "serial=%u", serial);
if (g_repeatKeycode != 0)
g_cancelFocus++;
cancelRepeat("focus_leave");
}
extern (C) void app.onKbKeyonKbKey(void* data, wl_keyboard* kb, uint serial, uint time,
uint key, uint state) nothrow @nogc
{
immutable _error_ keycodekeycode = key + 8; // evdev → XKB keycode (the historical +8)
immutable _error_ downdown = state == WL_KEYBOARD_KEY_STATE_PRESSED;
g_keyEvents++;
char[64] _error_ symNamesymName = void, _error_ texttext = void;
resolveKey(keycode, symName, text);
// Dead-key compose: pressed syms feed the compose state machine; while
// composing, the would-be text is withheld; on COMPOSED the machine —
// not the keymap — provides the final text.
if (down && g_composeState !is null && g_xkbState !is null)
{
immutable _error_ symsym = xkb_state_key_get_one_sym(g_xkbState, keycode);
if (xkb_compose_state_feed(g_composeState, sym) == XKB_COMPOSE_FEED_ACCEPTED)
{
immutable _error_ statusstatus = xkb_compose_state_get_status(g_composeState);
if (status == XKB_COMPOSE_COMPOSING)
{
instrEvent("compose", "state=composing");
text[0] = 0; // a dead key produces no text of its own
}
else if (status == XKB_COMPOSE_COMPOSED)
{
xkb_compose_state_get_utf8(g_composeState, text.text.ptrptr, text.text.lengthlength);
char[128] _error_ safesafe = void;
sanitize(text.text.ptrptr, safe.safe.ptrptr, safe.safe.lengthlength);
instrEvent("compose", "state=composed text=%s", safe.safe.ptrptr);
xkb_compose_state_reset(g_composeState);
}
else if (status == XKB_COMPOSE_CANCELLED)
{
instrEvent("compose", "state=cancelled");
xkb_compose_state_reset(g_composeState);
text[0] = 0;
}
}
}
logKeyLine(keycode, symName, text.text.ptrptr, down, 0);
if (down)
armRepeat(keycode); // a new press re-targets the repeat
else if (keycode == g_repeatKeycode)
{
g_cancelRelease++;
cancelRepeat("release"); // the first mandatory cancellation
}
}
/// Raw masks + group from the compositor; xkb_state_update_mask applies them.
/// The client never tracks modifiers itself — pressed keys on *other* clients
/// (grabs, compositor shortcuts) would desync it.
extern (C) void app.onKbModifiersRaw masks + group from the compositor; xkb_state_update_mask applies them.
The client never tracks modifiers itself — pressed keys on other clients
(grabs, compositor shortcuts) would desync it.
onKbModifiers(void* data, wl_keyboard* kb, uint serial,
uint depressed, uint latched, uint locked, uint group) nothrow @nogc
{
if (g_xkbState !is null)
xkb_state_update_mask(g_xkbState, depressed, latched, locked, 0, 0, group);
instrEvent("modifiers", "depressed=0x%x latched=0x%x locked=0x%x group=%u",
depressed, latched, locked, group);
}
/// The whole protocol-side repeat story: two integers, no events.
extern (C) void app.onKbRepeatInfoThe whole protocol-side repeat story: two integers, no events.
onKbRepeatInfo(void* data, wl_keyboard* kb, int rate, int delay) nothrow @nogc
{
g_repeatRate = rate;
g_repeatDelayMs = delay;
instrEvent("repeat_info", "rate_hz=%d delay_ms=%d", rate, delay);
}
__gshared wl_keyboard_listener _error_ app.g_keyboardListenerg_keyboardListener = {
&onKbKeymap, &onKbEnter, &onKbLeave, &onKbKey, &onKbModifiers, &onKbRepeatInfo
};
// ---------------------------------------------------------- shm buffer pool
bool app.ensureBufferensureBuffer(ref (struct) app.BufferOne wl_shm-backed ARGB8888 buffer (scaffold pattern).
Buffer (parameter) Buffer bb, int (parameter) int ww, int (parameter) int hh) nothrow @nogc
{
if (b.b.handlehandle !is null && (b.b.widthwidth != w || b.b.heightheight != h))
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (b.b.handlehandle !is null)
return true;
immutable _error_ stridestride = w * 4;
immutable _error_ sizesize = cast((unresolved type) size_tsize_t)(stride) * h;
immutable _error_ fdfd = memfd_create("wsi-f06", MFD_CLOEXEC);
if (fd < 0)
return false;
if (ftruncate(fd, cast(long) size) != 0)
{
close(fd);
return false;
}
void* _error_ memmem = mmap(null, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mem is cast(void*)-1)
{
close(fd);
return false;
}
wl_shm_pool* _error_ poolpool = wsi_shm_create_pool(g_shm, fd, cast(int) size);
b.b.handlehandle = wsi_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
wsi_shm_pool_destroy(pool);
close(fd);
wsi_buffer_add_listener(b.b.handlehandle, &g_bufferListener, &b);
b.b.pixelspixels = cast(uint*) mem;
b.b.byteSizebyteSize = size;
b.b.widthwidth = w;
b.b.heightheight = h;
b.b.busybusy = false;
return true;
}
void app.paintpaint(ref (struct) app.BufferOne wl_shm-backed ARGB8888 buffer (scaffold pattern).
Buffer (parameter) Buffer bb, int frame) nothrow @nogc
{
immutable uint _error_ colorcolor = (frame & 1) ? 0xff30_3050 : 0xff50_3030;
immutable _error_ nn = cast((unresolved type) size_tsize_t) b.b.widthwidth * b.b.heightheight;
foreach ((parameter) ii; 0 .. n)
b.b.pixelspixels[i] = color;
}
void void app.render() nothrow @nogcrender() nothrow @nogc
{
(struct) app.BufferOne wl_shm-backed ARGB8888 buffer (scaffold pattern).
Buffer* (local variable) _error_ bufbuf = null;
foreach (ref (parameter) bb; g_buffers)
if (!b.b.busybusy)
{
buf = &b;
break;
}
if (buf is null)
return; // both busy: the release handler re-renders
if (!ensureBuffer(*buf, g_width, g_height))
{
(__gshared global) bool app.g_runningg_running = false;
return;
}
paint(*buf, g_frames);
assert(buf.(field) _error_ buf.widthwidth == (__gshared global) int app.g_widthg_width && buf.(field) _error_ buf.heightheight == (__gshared global) int app.g_heightg_height,
"committed buffer size does not match the acked configure size");
wsi_surface_attach(g_surface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(g_surface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
if (g_frameCb is null)
{
g_frameCb = wsi_surface_frame(g_surface);
wsi_callback_add_listener(g_frameCb, &g_frameListener, null);
}
wsi_surface_commit(g_surface);
buf.buf.busybusy = true;
(__gshared global) int app.g_commitsg_commits++;
}
// ------------------------------------------------------ remaining listeners
extern (C) void app.onGlobalonGlobal(void* data, wl_registry* reg, uint name,
const(char)* iface, uint ver) nothrow @nogc
{
static uint uint capped(uint advertised, uint want) nothrow @nogccapped(uint (parameter) uint advertisedadvertised, uint (parameter) uint wantwant) nothrow @nogc
{
return advertised < want ? advertised : want;
}
if (strcmp(iface, wl_compositor_interface.name) == 0)
g_compositor = cast(wl_compositor*) wsi_registry_bind(reg, name,
&wl_compositor_interface, capped(ver, 4));
else if (strcmp(iface, wl_shm_interface.name) == 0)
g_shm = cast(wl_shm*) wsi_registry_bind(reg, name, &wl_shm_interface, 1);
else if (strcmp(iface, xdg_wm_base_interface.name) == 0)
g_wmBase = cast(xdg_wm_base*) wsi_registry_bind(reg, name, &xdg_wm_base_interface, 1);
else if (strcmp(iface, wl_seat_interface.name) == 0)
g_seat = cast(wl_seat*) wsi_registry_bind(reg, name, &wl_seat_interface,
capped(ver, 7)); // v4+: wl_keyboard.repeat_info
}
extern (C) void app.onGlobalRemoveonGlobalRemove(void* data, wl_registry* reg, uint name) nothrow @nogc
{
}
extern (C) void app.onWmBasePingonWmBasePing(void* data, xdg_wm_base* (parameter) xdg_wm_base* bb, uint serial) nothrow @nogc
{
wsi_wm_base_pong(b, serial);
}
/// Capabilities are dynamic: the keyboard is acquired when announced and
/// released if it disappears (hotplug semantics, even for virtual devices).
extern (C) void app.onSeatCapabilitiesCapabilities are dynamic: the keyboard is acquired when announced and
released if it disappears (hotplug semantics, even for virtual devices).
onSeatCapabilities(void* data, wl_seat* s, uint caps) nothrow @nogc
{
instrEvent("seat_capabilities", "caps=0x%x keyboard=%d", caps,
(caps & WL_SEAT_CAPABILITY_KEYBOARD) != 0);
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && g_keyboard is null)
{
g_keyboard = wsi_seat_get_keyboard(s);
wsi_keyboard_add_listener(g_keyboard, &g_keyboardListener, null);
instrEvent("keyboard_bound");
}
else if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD) && g_keyboard !is null)
{
cancelRepeat("capability_lost");
wsi_keyboard_release(g_keyboard);
g_keyboard = null;
instrEvent("keyboard_released");
}
}
extern (C) void app.onSeatNameonSeatName(void* data, wl_seat* s, const(char)* name) nothrow @nogc
{
instrEvent("seat_name", "name=%s", name);
}
extern (C) void app.onToplevelConfigureonToplevelConfigure(void* data, xdg_toplevel* t, int (parameter) int ww, int (parameter) int hh,
wl_array* states) nothrow @nogc
{
g_pendingWidth = w;
g_pendingHeight = h;
}
extern (C) void app.onXdgSurfaceConfigureonXdgSurfaceConfigure(void* data, xdg_surface* s, uint serial) nothrow @nogc
{
immutable _error_ ww = g_pendingWidth > 0 ? g_pendingWidth : defaultWidth;
immutable _error_ hh = g_pendingHeight > 0 ? g_pendingHeight : defaultHeight;
wsi_xdg_surface_ack_configure(s, serial);
immutable _error_ resizedresized = w != g_width || h != g_height;
g_width = w;
g_height = h;
if (!g_configured)
{
g_configured = true;
instrFirstConfigure();
render();
}
else if (resized)
{
instrResize(w, h, 1);
render();
}
}
extern (C) void app.onToplevelCloseonToplevelClose(void* data, xdg_toplevel* t) nothrow @nogc
{
instrCloseRequested();
g_running = false;
}
extern (C) void app.onToplevelConfigureBoundsonToplevelConfigureBounds(void* data, xdg_toplevel* t, int (parameter) int ww, int (parameter) int hh) nothrow @nogc
{
}
extern (C) void app.onToplevelWmCapabilitiesonToplevelWmCapabilities(void* data, xdg_toplevel* t, wl_array* caps) nothrow @nogc
{
}
extern (C) void app.onBufferReleaseonBufferRelease(void* data, wl_buffer* (parameter) wl_buffer* bb) nothrow @nogc
{
auto _error_ bufbuf = cast((unresolved type) BufferBuffer*) data;
buf.buf.busybusy = false;
if (g_running && g_configured && g_frameCb is null)
render();
}
extern (C) void app.onFrameDoneonFrameDone(void* data, wl_callback* cb, uint timeMs) nothrow @nogc
{
wsi_callback_destroy(cb);
g_frameCb = null;
g_frames++;
if (!g_presented)
{
g_presented = true;
instrFirstPixelPresented();
}
render();
}
__gshared wl_registry_listener _error_ app.g_registryListenerg_registryListener = {&onGlobal, &onGlobalRemove};
__gshared xdg_wm_base_listener _error_ app.g_wmBaseListenerg_wmBaseListener = {&onWmBasePing};
__gshared wl_seat_listener _error_ app.g_seatListenerg_seatListener = {&onSeatCapabilities, &onSeatName};
__gshared xdg_surface_listener _error_ app.g_xdgSurfaceListenerg_xdgSurfaceListener = {&onXdgSurfaceConfigure};
__gshared xdg_toplevel_listener _error_ app.g_toplevelListenerg_toplevelListener = {
&onToplevelConfigure, &onToplevelClose,
&onToplevelConfigureBounds, &onToplevelWmCapabilities
};
__gshared wl_buffer_listener _error_ app.g_bufferListenerg_bufferListener = {&onBufferRelease};
__gshared wl_callback_listener _error_ app.g_frameListenerg_frameListener = {&onFrameDone};
// ------------------------------------------- the F05 multiplexing loop again
/// wl fd + repeat timerfd in one poll — the same canonical
/// prepare_read/poll/read_events pump F05 documents; the repeat timer is
/// exactly the kind of "external fd" that pattern exists for.
bool bool app.pumpOnce(int timeoutMs) nothrow @nogcwl fd + repeat timerfd in one poll — the same canonical
prepare_read/poll/read_events pump F05 documents; the repeat timer is
exactly the kind of "external fd" that pattern exists for.
pumpOnce(int (parameter) int timeoutMstimeoutMs) nothrow @nogc
{
while (wl_display_prepare_read(g_display) != 0)
if (wl_display_dispatch_pending(g_display) < 0)
return false;
wl_display_flush(g_display);
pollfd[2] (local variable) _error_ pfdspfds;
pfds[0].(__error)[0].fdfd = wl_display_get_fd(g_display);
pfds[0].events = POLLIN;
pfds[1].(__error)[1].fdfd = g_repeatTimerfd;
pfds[1].events = POLLIN;
immutable (local variable) immutable(_error_) rr = poll(pfds.pfds.ptrptr, 2, timeoutMs);
if (r < 0)
{
wl_display_cancel_read(g_display);
return false;
}
if (pfds[0].revents & POLLIN)
{
if (wl_display_read_events(g_display) < 0)
return false;
if (wl_display_dispatch_pending(g_display) < 0)
return false;
}
else
wl_display_cancel_read(g_display);
if (pfds[1].revents & POLLIN)
void app.drainRepeatTimer() nothrow @nogcRepeat timer fired: synthesize repeat=1 key events from the current
xkb_state (so held-key repeats track live modifier changes).
drainRepeatTimer();
return true;
}
// ----------------------------------------------------------------- teardown
void void app.teardown() nothrow @nogcteardown() nothrow @nogc
{
if (g_composeState !is null)
xkb_compose_state_unref(g_composeState);
if (g_composeTable !is null)
xkb_compose_table_unref(g_composeTable);
if (g_xkbState !is null)
xkb_state_unref(g_xkbState);
if (g_xkbKeymap !is null)
xkb_keymap_unref(g_xkbKeymap);
if (g_xkbCtx !is null)
xkb_context_unref(g_xkbCtx);
foreach (ref (parameter) bb; g_buffers)
if (b.b.handlehandle !is null)
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (g_frameCb !is null)
wsi_callback_destroy(g_frameCb);
if (g_keyboard !is null)
wsi_keyboard_release(g_keyboard);
if (g_toplevel !is null)
wsi_toplevel_destroy(g_toplevel);
if (g_xdgSurface !is null)
wsi_xdg_surface_destroy(g_xdgSurface);
if (g_surface !is null)
wsi_surface_destroy(g_surface);
if (g_wmBase !is null)
wsi_wm_base_destroy(g_wmBase);
if (g_seat !is null)
wl_proxy_destroy(cast(wl_proxy*) g_seat);
if (g_shm !is null)
wl_proxy_destroy(cast(wl_proxy*) g_shm);
if (g_compositor !is null)
wl_proxy_destroy(cast(wl_proxy*) g_compositor);
if (g_registry !is null)
wl_proxy_destroy(cast(wl_proxy*) g_registry);
wl_display_disconnect(g_display);
if ((__gshared global) int app.g_repeatTimerfdg_repeatTimerfd >= 0)
close(g_repeatTimerfd);
}
// --------------------------------------------------------------------- main
int int D main()main()
{
instrInit("f06-wayland");
const (local variable) const(char*) autoEnvautoEnv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_AUTO_EXIT");
(__gshared global) bool app.g_autoExitg_autoExit = (local variable) const(char*) autoEnvautoEnv !is null && *(local variable) const(char*) autoEnvautoEnv == '1';
long (local variable) long capUscapUs = (constant) long app.defaultCapUs = 30000000LdefaultCapUs;
if (const (local variable) const(char*) capcap = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_F06_CAP_US"))
if (long core.stdc.stdlib.atoll(scope const(char*) nptr) nothrow @nogcatoll((local variable) const(char*) capcap) > 0)
(local variable) long capUscapUs = long core.stdc.stdlib.atoll(scope const(char*) nptr) nothrow @nogcatoll((local variable) const(char*) capcap);
// 1. Connect (SKIP cleanly on hosts without a compositor).
g_display = wl_display_connect(null);
if (g_display is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: no Wayland compositor (wl_display_connect returned null)\n");
return 0;
}
// 2. Registry: bind the window globals + the seat.
g_registry = wsi_display_get_registry(g_display);
wsi_registry_add_listener(g_registry, &g_registryListener, null);
wl_display_roundtrip(g_display);
if (g_compositor is null || g_shm is null || g_wmBase is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: compositor lacks a required global (wl_compositor/wl_shm/xdg_wm_base)\n");
void app.teardown() nothrow @nogcteardown();
return 0;
}
if (g_seat is null)
{
// Headless weston's documented shape: no input backend, no seat.
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: compositor advertises no wl_seat (headless weston?) — keyboard demo needs a seat\n");
void app.teardown() nothrow @nogcteardown();
return 0;
}
wsi_wm_base_add_listener(g_wmBase, &g_wmBaseListener, null);
wsi_seat_add_listener(g_seat, &g_seatListener, null);
// 3. xkbcommon context + locale compose table (client-side dead keys).
g_xkbCtx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
const(char)* (local variable) const(char)* localelocale = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("LC_ALL");
if ((local variable) const(char)* localelocale is null || *(local variable) const(char)* localelocale == 0)
(local variable) const(char)* localelocale = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("LC_CTYPE");
if ((local variable) const(char)* localelocale is null || *(local variable) const(char)* localelocale == 0)
(local variable) const(char)* localelocale = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("LANG");
if ((local variable) const(char)* localelocale is null || *(local variable) const(char)* localelocale == 0)
(local variable) const(char)* localelocale = "C";
g_composeTable = xkb_compose_table_new_from_locale(g_xkbCtx, locale,
XKB_COMPOSE_COMPILE_NO_FLAGS);
if (g_composeTable !is null)
g_composeState = xkb_compose_state_new(g_composeTable, XKB_COMPOSE_STATE_NO_FLAGS);
instrEvent("compose_table", "locale=%s status=%s", locale,
g_composeTable !is null ? "ok"."ok".ptrptr : "unavailable"."unavailable".ptrptr);
// 4. The repeat timerfd (armed/disarmed by key events; see armRepeat).
(__gshared global) int app.g_repeatTimerfdg_repeatTimerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
if ((__gshared global) int app.g_repeatTimerfdg_repeatTimerfd < 0)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: timerfd unavailable\n");
void app.teardown() nothrow @nogcteardown();
return 0;
}
// 5. Window object tree (scaffold handshake) — the demo must be a mapped,
// focused toplevel to receive wl_keyboard events at all.
g_surface = wsi_compositor_create_surface(g_compositor);
g_xdgSurface = wsi_wm_base_get_xdg_surface(g_wmBase, g_surface);
wsi_xdg_surface_add_listener(g_xdgSurface, &g_xdgSurfaceListener, null);
g_toplevel = wsi_xdg_surface_get_toplevel(g_xdgSurface);
wsi_toplevel_add_listener(g_toplevel, &g_toplevelListener, null);
wsi_toplevel_set_title(g_toplevel, "wsi-f06-keyboard");
wsi_toplevel_set_app_id(g_toplevel, "wsi-f06-keyboard");
instrWindowCreated();
wsi_surface_commit(g_surface); // mandatory initial no-buffer commit
// 6. Event loop: wl fd + repeat timerfd in one poll (see pumpOnce).
while ((__gshared global) bool app.g_runningg_running)
{
if (!bool app.pumpOnce(int timeoutMs) nothrow @nogcwl fd + repeat timerfd in one poll — the same canonical
prepare_read/poll/read_events pump F05 documents; the repeat timer is
exactly the kind of "external fd" that pattern exists for.
pumpOnce((constant) int app.pollTimeoutMs = 250pollTimeoutMs))
break;
if ((__gshared global) bool app.g_autoExitg_autoExit && instrNowUs() > (local variable) long capUscapUs)
(__gshared global) bool app.g_runningg_running = false;
}
// 7. Teardown + the numbers.
void app.teardown() nothrow @nogcteardown();
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("ok: keymaps=%d key_events=%d repeats_synthesized=%d "
~ "cancel_on_release=%d cancel_on_focus_loss=%d\n",
(__gshared global) int app.g_keymapCountg_keymapCount, (__gshared global) int app.g_keyEventsg_keyEvents, (__gshared global) int app.g_repeatSynthg_repeatSynth, (__gshared global) int app.g_cancelReleaseg_cancelRelease, (__gshared global) int app.g_cancelFocusg_cancelFocus);
return 0;
}