//! xkeyboard-config — keyboard layouts, compiled from the X11 xkeyboard-config database //! into native Zig. It turns a physical key (a USB HID usage, as the input module delivers) //! plus a modifier state into a **keysym** and, when the key produces one, a **character** //! (a Unicode scalar). This is the piece that lets a `KeyEvent.keycode` become a //! `KeyEvent.character`, without shipping an X11 runtime. //! //! The layout tables in `generated/layouts.zig` are produced by //! `tools/make-xkeyboard-config.py` (see ./README.md to regenerate). Those tables are //! deliberately pure data — each key carries its up-to-four levels and an XKB *type*. The //! type -> level selection semantics (which modifier picks which level) live here, so the //! data and the policy are separable. //! //! Scope (documented in README.md): group 1 only, no dead-key/compose composition (a dead //! key returns its keysym with no character), and a curated set of key types. Layouts: //! us, gb, de, fr, es, dvorak. //! //! Upstream xkeyboard-config and keysymdef.h are MIT/X11 licensed; see vendor/COPYING and //! vendor/PROVENANCE.md. const std = @import("std"); const generated = @import("layouts"); pub const Level = generated.Level; pub const KeyType = generated.KeyType; pub const Key = generated.Key; pub const Layout = generated.Layout; /// The generated layouts, by name — as pointers, so they share identity with `all` and /// `byName` (and match the `*const Layout` that `map` takes). pub const us: *const Layout = &generated.us; pub const gb: *const Layout = &generated.gb; pub const de: *const Layout = &generated.de; pub const fr: *const Layout = &generated.fr; pub const es: *const Layout = &generated.es; pub const dvorak: *const Layout = &generated.dvorak; /// Every generated layout, for enumeration (e.g. a settings UI). pub const all = generated.all; /// The modifier state that selects a key's level. `level3` is AltGr (ISO Level3 Shift); /// `control` is accepted for completeness but does not affect level selection here. pub const Modifiers = struct { shift: bool = false, caps_lock: bool = false, level3: bool = false, control: bool = false, }; /// The result of a lookup: the X11 `keysym`, and the `character` it produces (a Unicode /// scalar) when it is a printable key — null for keys that produce none (Return, F1, a /// bare dead key, an unmapped key). pub const Mapping = struct { keysym: u32, character: ?u21, }; /// Which level (0..3) a key of `kind` selects under `mods`. XKB's canonical semantics: /// Shift picks the odd level, AltGr (level3) adds 2, and Caps acts like Shift for the /// alphabetic types. See the XKB "key types" — this covers the ones the vendored layouts /// use; anything else falls back to shift-or-not. fn selectLevel(kind: KeyType, mods: Modifiers) usize { const shift_or_caps = mods.shift != mods.caps_lock; // XOR: Caps behaves like Shift const low: usize = if (mods.shift) 1 else 0; const high: usize = if (mods.level3) 2 else 0; return switch (kind) { .one_level => 0, .two_level, .keypad, .other => low, .alphabetic => if (shift_or_caps) 1 else 0, .four_level => low + high, .four_level_alphabetic => (if (shift_or_caps) @as(usize, 1) else 0) + high, // Caps affects only the base pair, not the AltGr pair. .four_level_semialphabetic => if (mods.level3) 2 + low else (if (shift_or_caps) @as(usize, 1) else 0), }; } /// Map a physical key (`hid_usage`, a USB HID keyboard-page usage) under `mods` on /// `layout` to its keysym and character. Falls back gracefully when the selected level is /// undefined for the key: it drops the AltGr component, then the shift component, so a key /// with only a base/shift pair still yields something sensible under AltGr. pub fn map(layout: *const Layout, hid_usage: u8, mods: Modifiers) Mapping { const key = &layout.keys[hid_usage]; var level = selectLevel(key.kind, mods); // Fall back to a defined level: full -> without AltGr -> base. if (key.levels[level].keysym == 0 and key.levels[level].unicode == 0) { const candidates = [_]usize{ level & 1, 0 }; for (candidates) |candidate| { if (key.levels[candidate].keysym != 0 or key.levels[candidate].unicode != 0) { level = candidate; break; } } } const chosen = key.levels[level]; return .{ .keysym = chosen.keysym, .character = if (chosen.unicode != 0) @intCast(chosen.unicode) else null, }; } /// Look up a layout by its name (`"us"`, `"gb"`, ...), or null if unknown. pub fn byName(name: []const u8) ?*const Layout { for (all) |layout| { if (std.mem.eql(u8, layout.name, name)) return layout; } return null; } // --- tests (host-run via `zig build test`) --------------------------------- const testing = std.testing; // USB HID usages used in the tests (keyboard page 0x07). const hid_a: u8 = 0x04; const hid_1: u8 = 0x1e; const hid_3: u8 = 0x20; test "us: letters obey shift and caps" { try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{}).character); try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .shift = true }).character); try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .caps_lock = true }).character); // Shift + Caps cancels for an alphabetic key. try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{ .shift = true, .caps_lock = true }).character); } test "us: digits and their shifted symbols" { try testing.expectEqual(@as(?u21, '1'), map(us, hid_1, .{}).character); try testing.expectEqual(@as(?u21, '!'), map(us, hid_1, .{ .shift = true }).character); try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{}).character); try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character); // A digit is not alphabetic: Caps alone must not shift it. try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{ .caps_lock = true }).character); } test "layouts differ: GB pound vs US hash on shift+3" { try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character); try testing.expectEqual(@as(?u21, '£'), map(gb, hid_3, .{ .shift = true }).character); } test "french azerty places q where us has a" { try testing.expectEqual(@as(?u21, 'q'), map(fr, hid_a, .{}).character); try testing.expectEqual(@as(?u21, 'Q'), map(fr, hid_a, .{ .shift = true }).character); } test "byName resolves and rejects" { try testing.expect(byName("us") == us); try testing.expect(byName("gb") == gb); try testing.expect(byName("nonsense") == null); } test "unmapped key yields no character" { // HID 0x00 is not a key; every level is empty. try testing.expectEqual(@as(?u21, null), map(us, 0x00, .{}).character); }