a73x

30b495f7

feat(wayland): keyboard + xkbcommon + key repeat

a73x   2026-04-08 08:46

Commit message
feat(wayland): keyboard + xkbcommon + key repeat

Implements Tasks 5.4 and 5.5: Keyboard struct with xkbcommon keymap
loading via mmap, press/release event queuing, modifier state tracking,
and client-side key repeat via tickRepeat().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

build.zig
Old New
@@ -32,6 +32,7 @@ pub fn build(b: *std.Build) void {
32 }); 32 });
33 wayland_mod.addImport("wayland", wayland_generated_mod); 33 wayland_mod.addImport("wayland", wayland_generated_mod);
34 wayland_mod.linkSystemLibrary("wayland-client", .{}); 34 wayland_mod.linkSystemLibrary("wayland-client", .{});
35 wayland_mod.linkSystemLibrary("xkbcommon", .{});
35 _ = wayland_dep; // referenced via Scanner 36 _ = wayland_dep; // referenced via Scanner
36 37
37 // vt module — wraps ghostty-vt 38 // vt module — wraps ghostty-vt
src/wayland.zig
Old New
@@ -4,6 +4,107 @@ const wayland = @import("wayland");
4 const wl = wayland.client.wl; 4 const wl = wayland.client.wl;
5 const xdg = wayland.client.xdg; 5 const xdg = wayland.client.xdg;
6 6
7 const c = @cImport({
8 @cInclude("xkbcommon/xkbcommon.h");
9 @cInclude("sys/mman.h");
10 @cInclude("unistd.h");
11 });
12
13 pub const KeyboardEvent = struct {
14 keysym: u32,
15 modifiers: Modifiers,
16 action: Action,
17 utf8: [8]u8,
18 utf8_len: u8,
19
20 pub const Action = enum { press, release, repeat };
21 };
22
23 pub const Modifiers = struct {
24 ctrl: bool = false,
25 shift: bool = false,
26 alt: bool = false,
27 super: bool = false,
28 };
29
30 pub const Keyboard = struct {
31 alloc: std.mem.Allocator,
32 wl_keyboard: *wl.Keyboard,
33 xkb_ctx: ?*c.xkb_context,
34 xkb_keymap: ?*c.xkb_keymap = null,
35 xkb_state: ?*c.xkb_state = null,
36 event_queue: std.ArrayList(KeyboardEvent),
37 current_mods: Modifiers = .{},
38 // repeat info
39 repeat_rate: u32 = 25,
40 repeat_delay: u32 = 500,
41 last_keycode: ?u32 = null,
42 last_key_time_ns: i128 = 0,
43 next_repeat_time_ns: i128 = 0,
44 has_focus: bool = false,
45
46 pub fn init(alloc: std.mem.Allocator, seat: *wl.Seat) !*Keyboard {
47 const kb = try alloc.create(Keyboard);
48 errdefer alloc.destroy(kb);
49
50 var event_queue: std.ArrayList(KeyboardEvent) = .empty;
51 errdefer event_queue.deinit(alloc);
52 try event_queue.ensureTotalCapacity(alloc, 64);
53
54 kb.* = .{
55 .alloc = alloc,
56 .wl_keyboard = try seat.getKeyboard(),
57 .xkb_ctx = c.xkb_context_new(c.XKB_CONTEXT_NO_FLAGS),
58 .event_queue = event_queue,
59 };
60 kb.wl_keyboard.setListener(*Keyboard, keyboardListener, kb);
61 return kb;
62 }
63
64 pub fn deinit(self: *Keyboard) void {
65 if (self.xkb_state) |s| c.xkb_state_unref(s);
66 if (self.xkb_keymap) |m| c.xkb_keymap_unref(m);
67 if (self.xkb_ctx) |ctx| c.xkb_context_unref(ctx);
68 self.wl_keyboard.release();
69 self.event_queue.deinit(self.alloc);
70 self.alloc.destroy(self);
71 }
72
73 /// Called from the main loop. If a key is currently held and the repeat timer
74 /// has expired, push synthetic repeat events to the queue.
75 pub fn tickRepeat(self: *Keyboard) void {
76 const keycode = self.last_keycode orelse return;
77 const state = self.xkb_state orelse return;
78
79 const now = std.time.nanoTimestamp();
80 if (now < self.next_repeat_time_ns) return;
81
82 const keysym = c.xkb_state_key_get_one_sym(state, keycode);
83 var utf8: [8]u8 = undefined;
84 const len = c.xkb_state_key_get_utf8(state, keycode, &utf8, utf8.len);
85
86 const ev = KeyboardEvent{
87 .keysym = keysym,
88 .modifiers = self.current_mods,
89 .action = .repeat,
90 .utf8 = utf8,
91 .utf8_len = @intCast(@min(len, 8)),
92 };
93
94 self.event_queue.append(self.alloc, ev) catch |err| {
95 std.log.err("keyboard repeat append failed: {any}", .{err});
96 return;
97 };
98
99 // Schedule next repeat at 1/rate seconds from now
100 const interval_ms: i128 = if (self.repeat_rate > 0)
101 @divTrunc(1000, @as(i128, self.repeat_rate))
102 else
103 40;
104 self.next_repeat_time_ns = now + interval_ms * std.time.ns_per_ms;
105 }
106 };
107
7 pub const Globals = struct { 108 pub const Globals = struct {
8 compositor: ?*wl.Compositor = null, 109 compositor: ?*wl.Compositor = null,
9 wm_base: ?*xdg.WmBase = null, 110 wm_base: ?*xdg.WmBase = null,
@@ -94,6 +195,101 @@ pub const Connection = struct {
94 } 195 }
95 }; 196 };
96 197
198 fn keyboardListener(_: *wl.Keyboard, event: wl.Keyboard.Event, kb: *Keyboard) void {
199 switch (event) {
200 .keymap => |k| {
201 if (k.format != .xkb_v1) {
202 _ = c.close(k.fd);
203 return;
204 }
205 const map_mem = c.mmap(null, k.size, c.PROT_READ, c.MAP_PRIVATE, k.fd, 0);
206 if (map_mem == c.MAP_FAILED) {
207 _ = c.close(k.fd);
208 return;
209 }
210 defer _ = c.munmap(map_mem, k.size);
211 defer _ = c.close(k.fd);
212
213 const new_keymap = c.xkb_keymap_new_from_string(
214 kb.xkb_ctx,
215 @ptrCast(@alignCast(map_mem.?)),
216 c.XKB_KEYMAP_FORMAT_TEXT_V1,
217 c.XKB_KEYMAP_COMPILE_NO_FLAGS,
218 ) orelse return;
219
220 const new_state = c.xkb_state_new(new_keymap) orelse {
221 c.xkb_keymap_unref(new_keymap);
222 return;
223 };
224
225 if (kb.xkb_state) |s| c.xkb_state_unref(s);
226 if (kb.xkb_keymap) |m| c.xkb_keymap_unref(m);
227 kb.xkb_keymap = new_keymap;
228 kb.xkb_state = new_state;
229 },
230 .enter => {
231 kb.has_focus = true;
232 },
233 .leave => {
234 kb.has_focus = false;
235 kb.last_keycode = null;
236 },
237 .key => |k| {
238 const state = kb.xkb_state orelse return;
239 const keycode: u32 = k.key + 8; // evdev -> xkb offset
240 const keysym = c.xkb_state_key_get_one_sym(state, keycode);
241
242 var utf8: [8]u8 = undefined;
243 const len = c.xkb_state_key_get_utf8(state, keycode, &utf8, utf8.len);
244
245 const action: KeyboardEvent.Action = if (k.state == .pressed) .press else .release;
246
247 const ev = KeyboardEvent{
248 .keysym = keysym,
249 .modifiers = kb.current_mods,
250 .action = action,
251 .utf8 = utf8,
252 .utf8_len = @intCast(@min(len, 8)),
253 };
254
255 kb.event_queue.append(kb.alloc, ev) catch |err| {
256 std.log.err("keyboard event queue append failed: {any}", .{err});
257 return;
258 };
259
260 if (action == .press) {
261 kb.last_keycode = keycode;
262 kb.last_key_time_ns = std.time.nanoTimestamp();
263 kb.next_repeat_time_ns = kb.last_key_time_ns + @as(i128, kb.repeat_delay) * std.time.ns_per_ms;
264 } else if (kb.last_keycode == keycode) {
265 kb.last_keycode = null;
266 }
267 },
268 .modifiers => |m| {
269 const state = kb.xkb_state orelse return;
270 _ = c.xkb_state_update_mask(
271 state,
272 m.mods_depressed,
273 m.mods_latched,
274 m.mods_locked,
275 0,
276 0,
277 m.group,
278 );
279 kb.current_mods = .{
280 .ctrl = c.xkb_state_mod_name_is_active(state, "Control", c.XKB_STATE_MODS_EFFECTIVE) > 0,
281 .shift = c.xkb_state_mod_name_is_active(state, "Shift", c.XKB_STATE_MODS_EFFECTIVE) > 0,
282 .alt = c.xkb_state_mod_name_is_active(state, "Mod1", c.XKB_STATE_MODS_EFFECTIVE) > 0,
283 .super = c.xkb_state_mod_name_is_active(state, "Mod4", c.XKB_STATE_MODS_EFFECTIVE) > 0,
284 };
285 },
286 .repeat_info => |r| {
287 kb.repeat_rate = @intCast(r.rate);
288 kb.repeat_delay = @intCast(r.delay);
289 },
290 }
291 }
292
97 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void { 293 fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
98 switch (event) { 294 switch (event) {
99 .ping => |p| wm_base.pong(p.serial), 295 .ping => |p| wm_base.pong(p.serial),
@@ -102,8 +298,8 @@ fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase)
102 298
103 fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { 299 fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void {
104 switch (event) { 300 switch (event) {
105 .configure => |c| { 301 .configure => |cfg| {
106 surface.ackConfigure(c.serial); 302 surface.ackConfigure(cfg.serial);
107 window.configured = true; 303 window.configured = true;
108 }, 304 },
109 } 305 }
@@ -111,9 +307,9 @@ fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *
111 307
112 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { 308 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void {
113 switch (event) { 309 switch (event) {
114 .configure => |c| { 310 .configure => |cfg| {
115 if (c.width > 0) window.width = @intCast(c.width); 311 if (cfg.width > 0) window.width = @intCast(cfg.width);
116 if (c.height > 0) window.height = @intCast(c.height); 312 if (cfg.height > 0) window.height = @intCast(cfg.height);
117 }, 313 },
118 .close => window.should_close = true, 314 .close => window.should_close = true,
119 .configure_bounds => {}, 315 .configure_bounds => {},