30b495f7
feat(wayland): keyboard + xkbcommon + key repeat
a73x 2026-04-08 08:46
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>
diff --git a/build.zig b/build.zig index 29b5596..7ed1acd 100644 --- a/build.zig +++ b/build.zig @@ -32,6 +32,7 @@ pub fn build(b: *std.Build) void { }); wayland_mod.addImport("wayland", wayland_generated_mod); wayland_mod.linkSystemLibrary("wayland-client", .{}); wayland_mod.linkSystemLibrary("xkbcommon", .{}); _ = wayland_dep; // referenced via Scanner // vt module — wraps ghostty-vt diff --git a/src/wayland.zig b/src/wayland.zig index 6affd67..0b9d5fa 100644 --- a/src/wayland.zig +++ b/src/wayland.zig @@ -4,6 +4,107 @@ const wayland = @import("wayland"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const c = @cImport({ @cInclude("xkbcommon/xkbcommon.h"); @cInclude("sys/mman.h"); @cInclude("unistd.h"); }); pub const KeyboardEvent = struct { keysym: u32, modifiers: Modifiers, action: Action, utf8: [8]u8, utf8_len: u8, pub const Action = enum { press, release, repeat }; }; pub const Modifiers = struct { ctrl: bool = false, shift: bool = false, alt: bool = false, super: bool = false, }; pub const Keyboard = struct { alloc: std.mem.Allocator, wl_keyboard: *wl.Keyboard, xkb_ctx: ?*c.xkb_context, xkb_keymap: ?*c.xkb_keymap = null, xkb_state: ?*c.xkb_state = null, event_queue: std.ArrayList(KeyboardEvent), current_mods: Modifiers = .{}, // repeat info repeat_rate: u32 = 25, repeat_delay: u32 = 500, last_keycode: ?u32 = null, last_key_time_ns: i128 = 0, next_repeat_time_ns: i128 = 0, has_focus: bool = false, pub fn init(alloc: std.mem.Allocator, seat: *wl.Seat) !*Keyboard { const kb = try alloc.create(Keyboard); errdefer alloc.destroy(kb); var event_queue: std.ArrayList(KeyboardEvent) = .empty; errdefer event_queue.deinit(alloc); try event_queue.ensureTotalCapacity(alloc, 64); kb.* = .{ .alloc = alloc, .wl_keyboard = try seat.getKeyboard(), .xkb_ctx = c.xkb_context_new(c.XKB_CONTEXT_NO_FLAGS), .event_queue = event_queue, }; kb.wl_keyboard.setListener(*Keyboard, keyboardListener, kb); return kb; } pub fn deinit(self: *Keyboard) void { if (self.xkb_state) |s| c.xkb_state_unref(s); if (self.xkb_keymap) |m| c.xkb_keymap_unref(m); if (self.xkb_ctx) |ctx| c.xkb_context_unref(ctx); self.wl_keyboard.release(); self.event_queue.deinit(self.alloc); self.alloc.destroy(self); } /// Called from the main loop. If a key is currently held and the repeat timer /// has expired, push synthetic repeat events to the queue. pub fn tickRepeat(self: *Keyboard) void { const keycode = self.last_keycode orelse return; const state = self.xkb_state orelse return; const now = std.time.nanoTimestamp(); if (now < self.next_repeat_time_ns) return; const keysym = c.xkb_state_key_get_one_sym(state, keycode); var utf8: [8]u8 = undefined; const len = c.xkb_state_key_get_utf8(state, keycode, &utf8, utf8.len); const ev = KeyboardEvent{ .keysym = keysym, .modifiers = self.current_mods, .action = .repeat, .utf8 = utf8, .utf8_len = @intCast(@min(len, 8)), }; self.event_queue.append(self.alloc, ev) catch |err| { std.log.err("keyboard repeat append failed: {any}", .{err}); return; }; // Schedule next repeat at 1/rate seconds from now const interval_ms: i128 = if (self.repeat_rate > 0) @divTrunc(1000, @as(i128, self.repeat_rate)) else 40; self.next_repeat_time_ns = now + interval_ms * std.time.ns_per_ms; } }; pub const Globals = struct { compositor: ?*wl.Compositor = null, wm_base: ?*xdg.WmBase = null, @@ -94,6 +195,101 @@ pub const Connection = struct { } }; fn keyboardListener(_: *wl.Keyboard, event: wl.Keyboard.Event, kb: *Keyboard) void { switch (event) { .keymap => |k| { if (k.format != .xkb_v1) { _ = c.close(k.fd); return; } const map_mem = c.mmap(null, k.size, c.PROT_READ, c.MAP_PRIVATE, k.fd, 0); if (map_mem == c.MAP_FAILED) { _ = c.close(k.fd); return; } defer _ = c.munmap(map_mem, k.size); defer _ = c.close(k.fd); const new_keymap = c.xkb_keymap_new_from_string( kb.xkb_ctx, @ptrCast(@alignCast(map_mem.?)), c.XKB_KEYMAP_FORMAT_TEXT_V1, c.XKB_KEYMAP_COMPILE_NO_FLAGS, ) orelse return; const new_state = c.xkb_state_new(new_keymap) orelse { c.xkb_keymap_unref(new_keymap); return; }; if (kb.xkb_state) |s| c.xkb_state_unref(s); if (kb.xkb_keymap) |m| c.xkb_keymap_unref(m); kb.xkb_keymap = new_keymap; kb.xkb_state = new_state; }, .enter => { kb.has_focus = true; }, .leave => { kb.has_focus = false; kb.last_keycode = null; }, .key => |k| { const state = kb.xkb_state orelse return; const keycode: u32 = k.key + 8; // evdev -> xkb offset const keysym = c.xkb_state_key_get_one_sym(state, keycode); var utf8: [8]u8 = undefined; const len = c.xkb_state_key_get_utf8(state, keycode, &utf8, utf8.len); const action: KeyboardEvent.Action = if (k.state == .pressed) .press else .release; const ev = KeyboardEvent{ .keysym = keysym, .modifiers = kb.current_mods, .action = action, .utf8 = utf8, .utf8_len = @intCast(@min(len, 8)), }; kb.event_queue.append(kb.alloc, ev) catch |err| { std.log.err("keyboard event queue append failed: {any}", .{err}); return; }; if (action == .press) { kb.last_keycode = keycode; kb.last_key_time_ns = std.time.nanoTimestamp(); kb.next_repeat_time_ns = kb.last_key_time_ns + @as(i128, kb.repeat_delay) * std.time.ns_per_ms; } else if (kb.last_keycode == keycode) { kb.last_keycode = null; } }, .modifiers => |m| { const state = kb.xkb_state orelse return; _ = c.xkb_state_update_mask( state, m.mods_depressed, m.mods_latched, m.mods_locked, 0, 0, m.group, ); kb.current_mods = .{ .ctrl = c.xkb_state_mod_name_is_active(state, "Control", c.XKB_STATE_MODS_EFFECTIVE) > 0, .shift = c.xkb_state_mod_name_is_active(state, "Shift", c.XKB_STATE_MODS_EFFECTIVE) > 0, .alt = c.xkb_state_mod_name_is_active(state, "Mod1", c.XKB_STATE_MODS_EFFECTIVE) > 0, .super = c.xkb_state_mod_name_is_active(state, "Mod4", c.XKB_STATE_MODS_EFFECTIVE) > 0, }; }, .repeat_info => |r| { kb.repeat_rate = @intCast(r.rate); kb.repeat_delay = @intCast(r.delay); }, } } fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void { switch (event) { .ping => |p| wm_base.pong(p.serial), @@ -102,8 +298,8 @@ fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { switch (event) { .configure => |c| { surface.ackConfigure(c.serial); .configure => |cfg| { surface.ackConfigure(cfg.serial); window.configured = true; }, } @@ -111,9 +307,9 @@ fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: * fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { switch (event) { .configure => |c| { if (c.width > 0) window.width = @intCast(c.width); if (c.height > 0) window.height = @intCast(c.height); .configure => |cfg| { if (cfg.width > 0) window.width = @intCast(cfg.width); if (cfg.height > 0) window.height = @intCast(cfg.height); }, .close => window.should_close = true, .configure_bounds => {},