libinput, XKB, and seat input routing

Follow input events and keyboard state through seat routing.

14 min readChapter 127 of 168Intermediate → advanced
In this chapter

Before you start#

Read character devices, TTY and input and Wayland surfaces and events. This chapter studies libinput 1.26.2 and libxkbcommon 1.7.0. The examples use self-authored key transitions, never your keyboard or pointer events.

Learning goals#

Follow a device event through libinput and a compositor seat, explain the separate jobs of an XKB keymap and state, and reason about focus, serials, device removal and safe event lifetime. Use a private synthetic key sequence to distinguish a keycode, keysym and text.

Source walkthrough#

Input events become focused delivery
  1. Device and libinput

    Dispatch translates device activity into typed events.

  2. Compositor seat

    Focus and grabs choose the permitted destination.

  3. XKB state

    A keymap and modifier state interpret keycodes.

  4. Client

    Receives focused events and applies its input policy.

Conceptual model. A representative seat pipeline, not a recording of input.

An input device produces reports through a kernel input driver. The evdev interface exposes event records to authorized userspace clients; a udev-backed libinput context discovers suitable devices and groups them into seats. The compositor normally receives these events, decides where input should go, and speaks the appropriate client protocol. This is a common architecture, not a guarantee that every environment uses libinput, udev, XKB or even a graphical compositor. Access control and a session manager can mediate device opening; applications should not assume they can open arbitrary event nodes.

A libinput context owns internal sources and queues. It is configured with callbacks such as restricted device opening and closing, rather than acquiring unrestricted authority by itself. A udev seat context associates devices with a seat name; another API can add device paths explicitly. Each device may offer keyboard, pointer, touch, tablet or other capabilities. One physical enclosure can expose several logical devices, and a seat may aggregate several devices. Device addition and removal are events; a device pointer should not be retained beyond its documented lifetime without an explicit reference. See libinput 1.26.2 src/libinput.h for the API ownership rules.

The compositor watches the libinput fd in its event loop. When it is ready, the compositor calls libinput_dispatch, then repeatedly retrieves pending events and destroys each event after extracting what it needs. A device associated with an event remains meaningful under the event's lifetime rules; retaining it for future operations requires the documented device reference. When a removal event arrives, stop scheduling new work for that device, remove policy state such as held buttons where appropriate, and release retained references only after callbacks that use them have completed. A name in a sysfs directory is not a permanent device identity and reappearance can create a new object.

Dispatch, extract, then destroy
Dispatch, extract, then destroy1. Kernel evdev to libinput_dispatch: device fd becomes readable. 2. libinput_dispatch to Compositor seat: libinput_get_event: typed event. 3. Compositor seat to libinput_dispatch: libinput_event_destroy after extraction. 4. Compositor seat to Client: deliver to the focused surface. 5. Client to Compositor seat (return): serial and focus checked by seat.Kernel evdevlibinput_dispatchCompositor seatClientdevice fd becomesreadablelibinput_get_event:typed eventlibinput_event_destroy after extractiondeliver to thefocused surfaceserial and focuschecked by seat
  1. Kernel evdevlibinput_dispatch

    device fd becomes readable

  2. libinput_dispatchCompositor seat

    libinput_get_event: typed event

  3. Compositor seatlibinput_dispatch

    libinput_event_destroy after extraction

  4. Compositor seatClient

    deliver to the focused surface

  5. ClientCompositor seat

    Return. serial and focus checked by seat

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Kernel evdev → libinput_dispatch: device fd becomes readable
  2. libinput_dispatch → Compositor seat: libinput_get_event: typed event
  3. Compositor seat → libinput_dispatch: libinput_event_destroy after extraction
  4. Compositor seat → Client: deliver to the focused surface
  5. Client → Compositor seat (return): serial and focus checked by seat
Conceptual model. A simplified routing sequence that names real libinput calls; compositor focus and grab policy vary, and this is not a recording of input.

From libinput 1.26.2 src/libinput.c, libinput_dispatch, MIT-style file license, with the remainder of the function omitted:

c
	count = epoll_wait(libinput->epoll_fd, ep, ARRAY_LENGTH(ep), 0);
	if (count < 0)
		return -errno;

The first statement polls the context's internal epoll fd with a zero timeout; it does not wait indefinitely for input. A negative result returns the negated error number to the caller. The surrounding loop examines each ready source, skips sources whose fd is already -1, dispatches live sources, then drops destroyed sources. Skipping a destroyed source matters because an earlier callback in the same dispatch can have removed it. The return of zero is not a promise that a keyboard event occurred; callers still drain the event queue and handle device changes and other event types. The API does not make an application's callbacks or retained pointers automatically thread-safe.

From raw activity to useful events

The kernel's evdev records include event type, code and value, and synchronization records delimit related reports. libinput interprets device capabilities and applies device-specific handling, producing typed higher-level events such as pointer motion, scroll, button, touch and gesture events. Its device quirks affect classification or behavior for particular hardware; configuration options such as tapping and acceleration have defaults and availability that vary by device. A pointer motion delta after acceleration is not necessarily proportional to physical distance: acceleration considers policy and motion history. Timestamps help preserve ordering and estimate latency, but users must use the units and clock contract of the specific API and must not casually compare them to wall-clock timestamps.

Tapping translates a touchpad gesture into button-like behavior when supported and enabled; it is not a hardware switch present on every pointer. Button scrolling may interpret motion while a chosen button is held. Gestures may consist of begin, update and end events, including cancellation. A compositor needs to handle removal or loss of focus during an active gesture, not only its happy end. Raw evdev, libinput's transformed events, and the Wayland events a client receives are different boundaries. A client cannot reverse all compositor acceleration, focus policy and device-specific processing from one delivered motion event.

A compositor's seat manages capabilities and focus for keyboard, pointer and touch. Hit-testing finds the appropriate surface under a pointer, accounting for scene transforms and input regions. Keyboard focus can differ from pointer focus. A pointer grab may keep delivery associated with an interaction until its release even when the pointer leaves the original surface; implicit and explicit grab rules depend on protocol and policy. Touch contacts have IDs and lifetimes distinct from a mouse pointer. On destruction of the target surface or seat capability, the compositor must cancel or redirect interactions according to its protocol obligations and avoid sending to dead resources.

Wayland input serials are server-issued evidence of a specific event or state transition. Some requests, such as beginning an interactive move, require an appropriate recent serial; the compositor validates it in context. A serial is not a global authorization token or cryptographic secret, and a client cannot use an unrelated serial to take arbitrary focus. This limits confused interactions but does not turn every Wayland client into a complete sandbox. See the Wayland 1.23.0 seat and keyboard protocol for the event and serial contracts.

Keymaps, state, and text are different

libxkbcommon compiles a keymap and maintains keyboard state; it does not read the physical keyboard. An XKB context holds configuration and logging settings. A keymap describes keys, symbols, types, modifier mappings and layouts. Names often supplied as rules, model, layout, variant and options are commonly abbreviated RMLVO. These names select data from an installed configuration; a keymap can alternatively come from explicit text. Compilation can fail when files or requested layouts are missing. Release the context and keymap references on every failure path, and keep them alive as long as state objects need them.

An xkb_state tracks depressed, latched and locked modifiers and layout groups. Key presses and releases can update those components. A modifier is not necessarily a single dedicated physical key: multiple keys can contribute to one modifier, and latching and locking have different persistence. A group selects a layout or level of interpretation depending on the compiled map. The keymap is the ruleset; state is the changing set of active conditions. Replacing the keymap requires rebuilding or updating the corresponding state in a coherent way. See libxkbcommon 1.7.0 src/state.c and keymap implementation.

From xkb_state_update_key in that pinned src/state.c, which carries the Silicon Graphics permissive license notice, with declarations and following filter logic omitted:

c
    const struct xkb_key *key = XkbKey(state->keymap, kc);

    if (!key)
        return 0;

The lookup uses the state's keymap and the supplied keycode. If that code has no key, the function returns zero changed components rather than manufacturing a key. The surrounding implementation snapshots prior components, applies filters for the direction, updates modifier key counts and derives effective state. Counts prevent releasing one of multiple contributors from necessarily clearing a modifier still held by another. A caller must submit a coherent press/release sequence; calling only presses indefinitely is not a faithful keyboard state.

On a common evdev-based XKB setup, an evdev keycode is converted to the corresponding XKB keycode by adding eight. The Wayland keyboard protocol transmits its key value according to its own definition, and clients using an XKB keymap commonly perform this conversion for lookup. It is a convention of the relevant keymap and protocol setup, not a universal law for every input device, keymap or API. A keysym such as a or Shift_L represents a key meaning, not necessarily a Unicode character. A key can produce multiple symbols, a dead key can begin a compose sequence, and input methods can produce text unrelated to a one-key one-character mapping. Compose state is a separate state machine with composing, composed, cancelled and nothing outcomes; a client should not assume every keysym immediately produces one Unicode code point.

Evdev codes become XKB keycodes
Evdev codes become XKB keycodesKEY_LEFTSHIFT: Evdev code 42 in the kernel header. → XKB keycode 50: Shift key after the usual +8 offset. (add 8). KEY_A: Evdev code 30 in the kernel header. → XKB keycode 38: A key after the usual +8 offset. (add 8). KEY_LEFTSHIFT: Evdev code 42 in the kernel header. → Keysym A or a: Chosen by the effective Shift state. (Shift changes the lookup). KEY_A: Evdev code 30 in the kernel header. → Keysym A or a: Chosen by the effective Shift state. (lookup result)add 8add 8Shift changes thelookuplookup resultKEY_LEFTSHIFTEvdev code 42 in the kernelheader.KEY_AEvdev code 30 in the kernelheader.XKB keycode 50Shift key after the usual +8offset.XKB keycode 38A key after the usual +8offset.Keysym A or aChosen by the effectiveShift state.
KEY_LEFTSHIFTEvdev code 42 in the kernel header.
XKB keycode 50Shift key after the usual +8 offset.
KEY_AEvdev code 30 in the kernel header.
XKB keycode 38A key after the usual +8 offset.
KEY_LEFTSHIFTEvdev code 42 in the kernel header.
Keysym A or aChosen by the effective Shift state.
KEY_AEvdev code 30 in the kernel header.
Keysym A or aChosen by the effective Shift state.
Read the text version
  • KEY_LEFTSHIFT: Evdev code 42 in the kernel header. → XKB keycode 50: Shift key after the usual +8 offset. (add 8)
  • KEY_A: Evdev code 30 in the kernel header. → XKB keycode 38: A key after the usual +8 offset. (add 8)
  • KEY_LEFTSHIFT: Evdev code 42 in the kernel header. → Keysym A or a: Chosen by the effective Shift state. (Shift changes the lookup)
  • KEY_A: Evdev code 30 in the kernel header. → Keysym A or a: Chosen by the effective Shift state. (lookup result)
Conceptual model. Evdev codes are from Linux v6.12 include/uapi/linux/input-event-codes.h; the +8 offset is the usual evdev-to-XKB convention, not a universal rule. Synthetic presses, no device read.

Worked key sequence and repeat

Consider a US-layout keymap using the usual evdev-plus-eight mapping. A synthetic left Shift press at evdev code 42 corresponds to XKB keycode 50; a synthetic A-key press at evdev 30 corresponds to XKB keycode 38. Initially the effective Shift modifier is inactive. Update the Shift press, then look up keycode 38: the result is the uppercase keysym A for this map. Release keycode 38 and then keycode 50; effective Shift becomes inactive. A later keycode 38 lookup yields lowercase a. This illustrates one map, not an assertion about another layout or user configuration. A group switch could map the same physical key to a different symbol without changing its evdev code.

As a counterexample, suppose a dead-accent keysym starts a compose sequence. It may produce no committed text yet; a following key may complete an accented character, while an invalid continuation may cancel. Treating the first keysym as already committed UTF-8 loses composition. Conversely, a text input method can replace or combine text without corresponding one-to-one physical presses. Applications should distinguish physical shortcuts, keysyms and text input policy.

The Wayland keyboard's repeat information lets the client implement rate-based repetition of an eligible held key. A rate of zero disables this client-side repetition. Since protocol version 10, a compositor may instead send key events in the repeated state, including when it has sent a zero repeat rate; delay and rate describe policy, not proof that the server sends repeated physical key events. See the Wayland keyboard protocol. Repeat is cancelled when appropriate on release, focus loss or object teardown. Not every environment or key repeats, and compositor/toolkit policy differs. A focus change while a key is down requires careful state handling so a client does not keep a stuck modifier or repeat timer after it no longer receives the matching release.

Event ownership across focus changes

An event object is a snapshot of a particular input transition, not the device itself. After the compositor has extracted the event type, timestamp and relevant values, it destroys the event; it must separately reference any device it wants to keep in a longer-lived seat structure. The device may be logically removed even while that reference keeps its userspace object allocated. A surviving object reference therefore prevents a dangling pointer but does not bring hardware back or authorize future reads. The removal event is where policy can cancel timers, end gestures, and clear associations with the removed device. A newly discovered device with the same display name is still a new lifecycle.

From libinput 1.26.2 src/libinput.c, libinput_event_destroy, MIT-style permission notice in the file header (the earlier switch over tablet event types is omitted):

c
/* ... (lines omitted) ... */
	if (event->device)
		libinput_device_unref(event->device);

	free(event);
}

The destroy function releases the device reference the event carries and then frees the event. Event destruction and device lifetime are separate: this reference is dropped here, while other holders keep the device object alive. The same file calls libinput_device_ref where an event is queued for a device. The excerpt does not show whether the physical device is still connected or how a removal event is delivered.

Focus can change between events. Suppose a pointer press is delivered to surface A, then the pointer crosses into surface B while the button remains held. Depending on the protocol grab and compositor policy, motion and the eventual release can remain associated with the original interaction rather than being treated as a fresh click on B. Destroying A while this happens requires a cancellation or cleanup path. Keyboard focus, pointer focus and touch contact routing are separate; changing one does not automatically justify giving every kind of event to the same client. Serials connect particular interaction requests to server-observed events but do not replace focus checks.

A keymap replacement is another boundary. The compositor or client must not mix keycodes interpreted under one map with modifier masks from an unrelated map. A client receiving a new keymap needs to build the appropriate XKB objects, handle parse failure, and reconcile state before interpreting future events. Modifier events can communicate state separately from key presses; blindly replaying an incomplete history may produce different state. A compose sequence can also span several key events, so focus loss or input-method cancellation may require discarding pending composition rather than committing a half-entered character. These decisions belong to toolkit and protocol policy, not to the kernel evdev record alone.

Practice#

If a C compiler, pkg-config, libxkbcommon development headers, and XKB configuration data are installed, this self-contained program compiles a private temporary executable and supplies its own key events. It never opens an input device. A missing dependency or keymap is a valid outcome.

sh
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
cat > "$tmpdir/key.c" <<'C'
#include <stdio.h>
#include <xkbcommon/xkbcommon.h>
int main(void) {
    struct xkb_rule_names names = { .rules="evdev", .model="pc105", .layout="us" };
    struct xkb_context *c = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
    if (!c) return 1;
    struct xkb_keymap *k = xkb_keymap_new_from_names(c, &names, XKB_KEYMAP_COMPILE_NO_FLAGS);
    if (!k) { xkb_context_unref(c); return 2; }
    struct xkb_state *s = xkb_state_new(k);
    if (!s) { xkb_keymap_unref(k); xkb_context_unref(c); return 3; }
    xkb_state_update_key(s, 50, XKB_KEY_DOWN);
    printf("shifted keysym: %x\n", xkb_state_key_get_one_sym(s, 38));
    xkb_state_update_key(s, 50, XKB_KEY_UP);
    printf("unshifted keysym: %x\n", xkb_state_key_get_one_sym(s, 38));
    xkb_state_unref(s); xkb_keymap_unref(k); xkb_context_unref(c);
    return 0;
}
C
if command -v cc >/dev/null && pkg-config --exists xkbcommon; then
  cc "$tmpdir/key.c" -o "$tmpdir/key" $(pkg-config --cflags --libs xkbcommon) && "$tmpdir/key"
else
  printf '%s\n' 'compiler or xkbcommon development files unavailable'
fi
rm -rf "$tmpdir"
trap - EXIT

With the specified usual US map the expected hexadecimal keysyms are 41 and 61. A keymap load failure is not evidence of a broken keyboard; it may indicate missing configuration data. The program's synthetic input tests libxkbcommon on the local installed version, not libinput, Wayland focus or the pinned source version. If unavailable, use the supplied Shift/A sequence above: tabulate effective Shift and expected symbol at each transition, and explain the missing-map failure path. Do not read live event devices to complete the exercise.

Knowledge check#

  1. What does libinput_dispatch returning zero prove about keyboard input?
  2. Why must a device reference be retained when keeping it beyond an event?
  3. What changes when Shift is pressed in the supplied US keymap example?
  4. Why is the Wayland key value not automatically a Unicode character?
  5. What must happen to a client's repeat timer after focus loss?

Answers#

  1. It reports successful dispatch work, not that any particular keyboard event was produced. The caller drains and inspects the event queue.
  2. Removal can invalidate a borrowed pointer after the event or subsequent dispatch; the documented reference keeps the device object alive, although the physical device may still be gone.
  3. The state records an active modifier; keycode 38 resolves to A instead of a in that particular map. The keymap itself did not change.
  4. It identifies a key under a protocol and keymap convention. Layout, modifiers, compose and input methods determine meaning and text; one keysym need not equal one code point.
  5. Cancel or otherwise reconcile it with focus and release state, so the client does not synthesize repeats for a key it no longer owns.

Checkpoint#

Submit a four-row Shift press, A press, A release, Shift release table and a diagram distinguishing libinput dispatch, compositor focus, XKB lookup and text composition. Pass when the table names keycodes and state separately, and the diagram handles device removal. If not, run the synthetic program or work through the supplied state sequence again. This is practice, not certification.

Sources#

Consult libinput dispatch, libinput API lifetimes, libinput evdev handling, xkb state, and Wayland keyboard protocol. Hardware, permissions, installed configuration and compositor policy determine runtime behavior.

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.