a73x

3d5b717d

Handle non-text key encoding

a73x   2026-04-08 11:36


diff --git a/build.zig b/build.zig
index ffd6045..97ded6d 100644
--- a/build.zig
+++ b/build.zig
@@ -50,6 +50,7 @@ pub fn build(b: *std.Build) void {
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    exe_mod.addImport("vt", vt_mod);
    exe_mod.addImport("wayland-client", wayland_mod);
@@ -98,6 +99,7 @@ pub fn build(b: *std.Build) void {
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    main_test_mod.addImport("vt", vt_mod);
    main_test_mod.addImport("wayland-client", wayland_mod);
diff --git a/src/main.zig b/src/main.zig
index 7c4ca2a..0b97c28 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -5,6 +5,10 @@ const wayland_client = @import("wayland-client");
const renderer = @import("renderer");
const font = @import("font");

const c = @cImport({
    @cInclude("xkbcommon/xkbcommon-keysyms.h");
});

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
@@ -119,6 +123,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    };

    var read_buf: [8192]u8 = undefined;
    var key_buf: [32]u8 = undefined;

    while (!window.should_close and p.isChildAlive()) {
        // Flush any pending wayland requests
@@ -154,8 +159,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
            if (ev.action == .release) continue;
            if (ev.utf8_len > 0) {
                _ = try p.write(ev.utf8[0..ev.utf8_len]);
            } else if (try encodeKeyboardEvent(&term, ev, &key_buf)) |encoded| {
                _ = try p.write(encoded);
            }
            // Keys without UTF-8 (arrows, function keys) are skipped for v1
        }
        keyboard.event_queue.clearRetainingCapacity();

@@ -210,6 +216,60 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    _ = try ctx.vkd.deviceWaitIdle(ctx.device);
}

fn encodeKeyboardEvent(
    term: *const vt.Terminal,
    ev: wayland_client.KeyboardEvent,
    buf: []u8,
) !?[]const u8 {
    const key = mapKeysymToInputKey(ev.keysym) orelse return null;
    const encoded = try term.encodeKey(
        key,
        .{
            .shift = ev.modifiers.shift,
            .ctrl = ev.modifiers.ctrl,
            .alt = ev.modifiers.alt,
            .super = ev.modifiers.super,
        },
        switch (ev.action) {
            .press => .press,
            .release => .release,
            .repeat => .repeat,
        },
        buf,
    );

    if (encoded.len == 0) return null;
    return encoded;
}

fn mapKeysymToInputKey(keysym: u32) ?vt.InputKey {
    return switch (keysym) {
        c.XKB_KEY_Up => .arrow_up,
        c.XKB_KEY_Down => .arrow_down,
        c.XKB_KEY_Left => .arrow_left,
        c.XKB_KEY_Right => .arrow_right,
        c.XKB_KEY_Home => .home,
        c.XKB_KEY_End => .end,
        c.XKB_KEY_Page_Up => .page_up,
        c.XKB_KEY_Page_Down => .page_down,
        c.XKB_KEY_Insert => .insert,
        c.XKB_KEY_Delete => .delete,
        c.XKB_KEY_F1 => .f1,
        c.XKB_KEY_F2 => .f2,
        c.XKB_KEY_F3 => .f3,
        c.XKB_KEY_F4 => .f4,
        c.XKB_KEY_F5 => .f5,
        c.XKB_KEY_F6 => .f6,
        c.XKB_KEY_F7 => .f7,
        c.XKB_KEY_F8 => .f8,
        c.XKB_KEY_F9 => .f9,
        c.XKB_KEY_F10 => .f10,
        c.XKB_KEY_F11 => .f11,
        c.XKB_KEY_F12 => .f12,
        else => null,
    };
}

fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
    var conn = try wayland_client.Connection.init();
    defer conn.deinit();
@@ -295,6 +355,25 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
    std.debug.print("done\n", .{});
}

test "encodeKeyboardEvent encodes left arrow" {
    var term = try vt.Terminal.init(std.testing.allocator, .{
        .cols = 80,
        .rows = 24,
    });
    defer term.deinit();

    var buf: [32]u8 = undefined;
    const encoded = (try encodeKeyboardEvent(&term, .{
        .keysym = c.XKB_KEY_Left,
        .modifiers = .{},
        .action = .press,
        .utf8 = [_]u8{0} ** 8,
        .utf8_len = 0,
    }, &buf)).?;

    try std.testing.expectEqualStrings("\x1b[D", encoded);
}

fn runRenderSmokeTest(alloc: std.mem.Allocator) !void {
    var conn = try wayland_client.Connection.init();
    defer conn.deinit();
diff --git a/src/vt.zig b/src/vt.zig
index b506046..71c55bf 100644
--- a/src/vt.zig
+++ b/src/vt.zig
@@ -50,6 +50,10 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");

pub const InputAction = ghostty_vt.input.KeyAction;
pub const InputKey = ghostty_vt.input.Key;
pub const InputMods = ghostty_vt.input.KeyMods;

pub const Terminal = struct {
    alloc: std.mem.Allocator,
    inner: ghostty_vt.Terminal,
@@ -105,6 +109,23 @@ pub const Terminal = struct {
        self.stream.nextSlice(bytes);
    }

    pub fn encodeKey(
        self: *const Terminal,
        key: InputKey,
        mods: InputMods,
        action: InputAction,
        buf: []u8,
    ) ![]const u8 {
        var writer: std.Io.Writer = .fixed(buf);
        try ghostty_vt.input.encodeKey(&writer, .{
            .action = action,
            .key = key,
            .mods = mods,
            .consumed_mods = .{},
        }, .fromTerminal(&self.inner));
        return writer.buffered();
    }

    pub fn resize(self: *Terminal, new_cols: u16, new_rows: u16) !void {
        try self.inner.resize(self.alloc, @intCast(new_cols), @intCast(new_rows));
    }
@@ -158,3 +179,15 @@ test "Terminal.resize" {
    try std.testing.expectEqual(@as(u16, 120), term.cols());
    try std.testing.expectEqual(@as(u16, 40), term.rows());
}

test "Terminal encodes non-text arrow keys" {
    var term = try Terminal.init(std.testing.allocator, .{
        .cols = 80,
        .rows = 24,
    });
    defer term.deinit();

    var buf: [32]u8 = undefined;
    const encoded = try term.encodeKey(.arrow_left, .{}, .press, &buf);
    try std.testing.expectEqualStrings("\x1b[D", encoded);
}