154 lines
6.7 KiB
Zig
154 lines
6.7 KiB
Zig
//! 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);
|
|
}
|