30b495f7
feat(wayland): keyboard + xkbcommon + key repeat
a73x 2026-04-08 08:46
Commit message
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 => {}, |