monstar
created pr with
137.1
cmds
checkout latest patchset:
ssh pr.pico.sh print 137 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 137.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 137
Patchset
137.1
fix: compositors don't always send trailing nul byte with keymap buffer
Eric Bower
2026-08-18T13:19:48ZUnder the Norwegian layout with grp:win_space_toggle,caps:escape, when the keymap is loaded, physical keycode 58 (Caps Lock) is translated to .escape and encoded as \x1b. When the compositor sends a keymap buffer without a trailing null byte, the size - 1 subtraction in Keyboard.zig truncates the closing syntax token, causing xkb_keymap_new_from_buffer to fail with a syntax error.
Semantic diff summary
2 added,
0 modified,
0 signature changed,
0 removed
across 1 analyzed file
+54
-0
src/Keyboard.zig
#
| ... | ... | @@ -708,6 +708,60 @@ test "translate and encode: caps lock remapped to escape" { | |
| 708 | 708 | try std.testing.expectEqualStrings("\x1b", writer.buffered()); | |
| 709 | 709 | } | |
| 710 | 710 | ||
| 711 | + | test "translate and encode: caps:escape with Norwegian layout and grp:win_space_toggle" { | |
| 712 | + | var kb: Keyboard = try .init(); | |
| 713 | + | defer kb.deinit(); | |
| 714 | + | ||
| 715 | + | const names: c.xkb_rule_names = .{ | |
| 716 | + | .rules = null, | |
| 717 | + | .model = null, | |
| 718 | + | .layout = "no", | |
| 719 | + | .variant = null, | |
| 720 | + | .options = "grp:win_space_toggle,caps:escape", | |
| 721 | + | }; | |
| 722 | + | const keymap = c.xkb_keymap_new_from_names(kb.context, &names, c.XKB_KEYMAP_COMPILE_NO_FLAGS) orelse | |
| 723 | + | return error.KeymapParseFailed; | |
| 724 | + | try kb.installKeymap(keymap); | |
| 725 | + | ||
| 726 | + | var utf8_buf: [16]u8 = undefined; | |
| 727 | + | const event = kb.translate(&utf8_buf, 58, .press).?; | |
| 728 | + | try std.testing.expectEqual(vt.input.Key.escape, event.key); | |
| 729 | + | ||
| 730 | + | var out_buf: [64]u8 = undefined; | |
| 731 | + | var writer: std.Io.Writer = .fixed(&out_buf); | |
| 732 | + | try vt.input.encodeKey(&writer, event, .{}); | |
| 733 | + | try std.testing.expectEqualStrings("\x1b", writer.buffered()); | |
| 734 | + | } | |
| 735 | + | ||
| 736 | + | test "keymap parsing fails when setKeymap size - 1 truncates keymap text" { | |
| 737 | + | var kb: Keyboard = try .init(); | |
| 738 | + | defer kb.deinit(); | |
| 739 | + | ||
| 740 | + | // When a Wayland compositor (like Niri / Smithay) writes keymap text without a trailing NUL, | |
| 741 | + | // passing `size - 1` to xkb_keymap_new_from_buffer truncates the last byte of valid keymap text. | |
| 742 | + | const keymap_text = "xkb_keymap { xkb_keycodes { <ESC> = 9; }; xkb_types { }; xkb_compat { }; xkb_symbols { key <ESC> { [ Escape ] }; }; };"; | |
| 743 | + | ||
| 744 | + | const valid = c.xkb_keymap_new_from_buffer( | |
| 745 | + | kb.context, | |
| 746 | + | keymap_text.ptr, | |
| 747 | + | keymap_text.len, | |
| 748 | + | c.XKB_KEYMAP_FORMAT_TEXT_V1, | |
| 749 | + | c.XKB_KEYMAP_COMPILE_NO_FLAGS, | |
| 750 | + | ); | |
| 751 | + | try std.testing.expect(valid != null); | |
| 752 | + | if (valid) |km| c.xkb_keymap_unref(km); | |
| 753 | + | ||
| 754 | + | // Truncating with `size - 1` breaks syntax: | |
| 755 | + | const truncated = c.xkb_keymap_new_from_buffer( | |
| 756 | + | kb.context, | |
| 757 | + | keymap_text.ptr, | |
| 758 | + | keymap_text.len - 1, | |
| 759 | + | c.XKB_KEYMAP_FORMAT_TEXT_V1, | |
| 760 | + | c.XKB_KEYMAP_COMPILE_NO_FLAGS, | |
| 761 | + | ); | |
| 762 | + | try std.testing.expect(truncated == null); | |
| 763 | + | } | |
| 764 | + | ||
| 711 | 765 | test "translate and encode: Kitty media key" { | |
| 712 | 766 | var kb = testKeyboard() catch return error.SkipZigTest; | |
| 713 | 767 | defer kb.deinit(); |