- Zig 92.9%
- C 3.8%
- Nix 3.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Index the key table with a usize rather than the u64 difference of two keycodes, which is already known to be below max_keycode_span. Make `zig build check` compile both test binaries for -Dtarget, so that a target the tests cannot run on is still compiled for; the unit tests alone never reach the keymap compiler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3dz2xoroVgeNuLkoAjqTu |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-xkb
XKB keymaps, keyboard state and Compose sequences in Zig, for a Wayland client, with no libxkbcommon.
A Wayland compositor tells its clients how the keyboard is laid out by
sending a keymap as text, and tells them which modifiers are down and which
layout is in use as six numbers. Turning a key press into a keysym and a
character from those takes a keymap compiler and a model of the keyboard's
state; turning dead_acute then e into é takes a Compose table. This
library is all three, written in Zig, and it answers the way libxkbcommon
1.13.2 does. Its tests check that key by key against libxkbcommon itself.
The API documentation is generated from the doc comments and published at https://jeff.jcollie.page/zig-xkb/.
const xkb = @import("xkb");
// wl_keyboard.keymap: the text the compositor mapped for us.
var keymap = try xkb.Keymap.fromText(gpa, text, null);
defer keymap.deinit();
var state: xkb.State = .init(&keymap);
// wl_keyboard.modifiers
_ = state.updateMask(depressed, latched, locked, 0, 0, group);
// wl_keyboard.key: evdev codes are XKB keycodes less eight.
const keycode = key + 8;
const sym = state.keyGetOneSym(keycode);
if (state.keyGetUtf32(keycode)) |char| typed(char);
const ctrl = state.modNameIsActive("Control", .effective_mods) orelse false;
What it does
Keymaps. Keymap.fromText compiles the XKB_KEYMAP_FORMAT_TEXT_V1 text
of wl_keyboard.keymap: one xkb_keymap block with its keycodes, types,
compatibility and symbols sections written out in full. It parses the whole
language, reading both the compact form compositors send (keysyms as
hexadecimal) and the named form xkbcli compile-keymap prints. It makes the
same decisions libxkbcommon leaves to the compiler:
- the automatic key type for a key that names none (
ALPHABETIC,FOUR_LEVEL_SEMIALPHABETICand the rest, decided by what the first levels hold and their case); - the symbol interpretations, which give each key its virtual modifiers and decide whether it repeats, tried from the most specific to the least;
- what each virtual modifier maps to, which is the real modifiers of every key that sets it plus any mapping written out explicitly;
- and which key-type entries are dead because they name a virtual modifier that maps to nothing.
An include is refused rather than followed, since a compiled keymap has
nothing left to include. A geometry section is skipped. When compilation
fails, a Diagnostic gives the line, column and reason.
State. State.updateMask takes the six numbers of
wl_keyboard.modifiers. Virtual modifiers in the masks are resolved to real
ones, and the layout wraps into range as libxkbcommon wraps it. After that,
for any key:
keyGetLayoutandkeyGetLevelsay where the key is;keyGetSymsandkeyGetOneSymsay what it produces, with Caps Lock applied unless the key's type used Lock to choose the level;keyGetUtf32andkeyGetUtf8say what it types, including the Control transformation (Control+ais 1, and on a Cyrillic layoutControltakes the key's Latin letter);keyGetConsumedModssays which modifiers the key used up choosing its level, counted the XKB way or the GTK way;modIndexIsActiveandmodNameIsActivesay whether a modifier is on.
Keysyms. Keysym is every keysym libxkbcommon knows by name.
Keysym.named("Shift_L") is checked when compiled and Keysym.fromName
works at run time; both also take U20AC and 0x... forms. A keysym
converts to its name, its character (toUtf32, toUtf8) and its upper and
lower case. The tables are read at build time from the libxkbcommon release
named in build.zig.zon. That tarball is only read: nothing from it is
compiled or linked.
Compose. compose.Table.fromLocale finds the Compose file the way libX11
does: $XCOMPOSEFILE, then $XDG_CONFIG_HOME/XCompose (or
~/.config/XCompose), then ~/.XCompose, and failing those the locale's own
file, which compose.dir in the X locale directory names after
locale.alias has had its say. include "%L", %H and %S work as in
libX11. A compose.State walks the table a keysym at a time and says when a
sequence is composing, composed or cancelled, and what it produced.
The environment is passed in as a compose.Environment rather than read by
the library. Environment.fromMap and localeFromMap fill it from a
process's environment map.
The locale data comes with the library. zig build makes an X locale
directory from the libX11 release named in build.zig.zon, with the same
preprocessing libX11's own build does. The files are not kept in this
repository. A program installs the directory beside itself as
share/X11/locale, and fromLocale looks there when XLOCALEDIR is not
set, before the places libX11 is usually installed:
const xkb_dep = b.dependency("xkb", .{});
exe.root_module.addImport("xkb", xkb_dep.module("xkb"));
b.installDirectory(.{
.source_dir = xkb_dep.namedLazyPath("x11_locale"),
.install_dir = .prefix,
.install_subdir = "share/X11/locale",
});
zig build locale installs it into zig-out on its own. Its Compose
files match libX11's byte for byte, with one exception: an include of
another locale's file is written relative to the locale directory (%S)
rather than as an absolute path, so the directory works wherever it is
installed.
What it does not do
It is a client's half of XKB. Actions are parsed and set aside: the
compositor runs them and sends the result, so there is no update_key
here. There are no keyboard LEDs and no RMLVO names (us, pc105,
grp:alt_shift_toggle), and it does not read xkeyboard-config's
directories. A keymap made from names is the compositor's to compile; this
library reads what it compiled.
The text format is version 1, the only one Wayland sends. Keycodes are held in a single array, so a keymap whose keys span more than 65,536 codes is refused.
When compose.dir has no entry for a locale, the en_US.UTF-8 file is used,
as libxkbcommon does for a locale that is installed. libxkbcommon also asks
the C library whether the locale is valid and fails if not; this library
does not have that check.
How it is checked
zig build test runs the unit tests and then compares this library with
libxkbcommon itself. The comparison data is made during the build, not kept
in the repository:
- libxkbcommon 1.13.2 is compiled from the release tarball the keysym tables
come from. Its keymap parser is generated from that release's
parser.yby bison, which the devshell provides; only these tests need it, so a program depending on zig-xkb never does. - The three small C programs in
tests/referenceare linked against it and run. - Their output becomes a module the tests embed.
The tests check:
- Keysyms. Every named keysym, and every Unicode keysym that has a case: its name, character, upper and lower case, and whether it counts as lower or upper case.
- Keymaps. Five keymaps compiled from xkeyboard-config 2.48, which is
also a build dependency:
us,de,fr,us(intl), andus,ruwith a layout toggle. Each is checked in both its compact and named forms. Under 23 modifier and layout states, the test checks every key's layout, level, keysyms, UTF-8, UTF-32 and consumed modifiers, plus the modifiers and layouts in effect. - Compose. Every sequence in libX11's
en_US.UTF-8Compose file, from the locale directory the build makes, both as the table holds it and as a state composes it.
Moving to a new libxkbcommon, xkeyboard-config or libX11 release is a matter
of changing build.zig.zon. The comparison data follows automatically.
Keymaps and Compose files come from outside the program, so both parsers are
fuzzed. Each target in tests/fuzz.zig runs once against a smoke input as
part of zig build test. zig build test --fuzz runs them under Zig's own
coverage-guided fuzzer, and zig build fuzz-run under a loop of our own
that mutates a corpus of real keymaps and Compose files and checks every
input for leaks as well as crashes; tools/fuzz.zig explains it.
nix develop -c zig build test --fuzz=1M
nix develop -c zig build fuzz-run -- --seconds 600
nix develop -c zig build fuzz-run -- --target keymap --seconds 60
Using it
zig fetch --save git+https://git.jcollie.dev/jeff/zig-xkb.git
const xkb = b.dependency("xkb", .{}).module("xkb");
exe.root_module.addImport("xkb", xkb);
A Nix build of something that depends on this library needs the libxkbcommon
and libX11 tarballs in its --system directory along with the library
itself. zon2nix finds all three, since it follows transitive dependencies.
Building
zig-xkb needs Zig 0.17.0. Everything needed is in the flake's devshell:
nix develop -c zig build test --summary all
nix develop -c zig build check # the fuzz driver and docs server too
nix develop -c zig build docs-serve # the API documentation, on port 8000
nix flake check # the tests in the Nix sandbox
For Zig 0.16.0, use the zig-0.16 branch, which holds the last of zig-xkb
to build with it:
git clone -b zig-0.16 https://git.jcollie.dev/jeff/zig-xkb.git
or, as a dependency:
zig fetch --save https://git.jcollie.dev/jeff/zig-xkb/archive/zig-0.16.tar.gz
The v0.1.0 tag also predates the move to 0.17 and builds with 0.16.0.
Repository
The code lives at https://git.jcollie.dev/jeff/zig-xkb:
git clone https://git.jcollie.dev/jeff/zig-xkb.git
It is mirrored on Tangled at https://tangled.org/jcollie.dev/zig-xkb, and
is on Radicle as rad:z2pfZFSnMWawK275QEtWMfXGtTvUH:
rad clone rad:z2pfZFSnMWawK275QEtWMfXGtTvUH
References cited
XKB
- The xkbcommon project. libxkbcommon, version 1.13.2. 2026. MIT license.
The model followed throughout: its keymap compiler (
src/xkbcomp), state (src/state.c), keysyms (src/keysym.c,src/keysym-utf.c,src/keysym-case-mappings.c) and Compose (src/compose). https://github.com/xkbcommon/libxkbcommon - The xkbcommon project. The XKB keymap text format, V1. libxkbcommon documentation. https://xkbcommon.org/doc/current/keymap-text-format-v1.html
- Fortune, Erik. The X Keyboard Extension: Protocol Specification. X Window System Standard, X Version 11, Release 7. X Consortium, 1996. https://www.x.org/releases/current/doc/kbproto/xkbproto.html
Wayland
- The Wayland project. Wayland protocol specification: wl_keyboard. https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_keyboard
Compose
- X.Org Foundation. Compose(5): X client mappings for multi-key input sequences. X.Org manual pages. https://www.x.org/releases/current/doc/man/man5/Compose.5.xhtml
- X.Org Foundation. libX11, version 1.8.13. The X locale database:
locale.alias,compose.dirand each locale's Compose file, made at build time from the release'snlsdirectory, andcpprules.inandnls/Makefile.amfor how libX11 preprocesses them. https://gitlab.freedesktop.org/xorg/lib/libx11
Test data
- The xkeyboard-config authors. xkeyboard-config, version 2.48. freedesktop.org. The layouts the tests' keymaps are compiled from, fetched at build time, which is also when libxkbcommon's own answers for them are made. https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config
License
MIT. The test data in tests/data is under its sources' licenses, which
REUSE.toml records. The project follows the REUSE
specification, and reuse lint checks it.