a73x

3d5b717d

Handle non-text key encoding

a73x   2026-04-08 11:36

Commit message
Handle non-text key encoding

build.zig
Old New
@@ -50,6 +50,7 @@ pub fn build(b: *std.Build) void {
50 .root_source_file = b.path("src/main.zig"), 50 .root_source_file = b.path("src/main.zig"),
51 .target = target, 51 .target = target,
52 .optimize = optimize, 52 .optimize = optimize,
53 .link_libc = true,
53 }); 54 });
54 exe_mod.addImport("vt", vt_mod); 55 exe_mod.addImport("vt", vt_mod);
55 exe_mod.addImport("wayland-client", wayland_mod); 56 exe_mod.addImport("wayland-client", wayland_mod);
@@ -98,6 +99,7 @@ pub fn build(b: *std.Build) void {
98 .root_source_file = b.path("src/main.zig"), 99 .root_source_file = b.path("src/main.zig"),
99 .target = target, 100 .target = target,
100 .optimize = optimize, 101 .optimize = optimize,
102 .link_libc = true,
101 }); 103 });
102 main_test_mod.addImport("vt", vt_mod); 104 main_test_mod.addImport("vt", vt_mod);
103 main_test_mod.addImport("wayland-client", wayland_mod); 105 main_test_mod.addImport("wayland-client", wayland_mod);
src/main.zig
Old New
@@ -5,6 +5,10 @@ const wayland_client = @import("wayland-client");
5 const renderer = @import("renderer"); 5 const renderer = @import("renderer");
6 const font = @import("font"); 6 const font = @import("font");
7 7
8 const c = @cImport({
9 @cInclude("xkbcommon/xkbcommon-keysyms.h");
10 });
11
8 pub fn main() !void { 12 pub fn main() !void {
9 var gpa: std.heap.DebugAllocator(.{}) = .init; 13 var gpa: std.heap.DebugAllocator(.{}) = .init;
10 defer _ = gpa.deinit(); 14 defer _ = gpa.deinit();
@@ -119,6 +123,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
119 }; 123 };
120 124
121 var read_buf: [8192]u8 = undefined; 125 var read_buf: [8192]u8 = undefined;
126 var key_buf: [32]u8 = undefined;
122 127
123 while (!window.should_close and p.isChildAlive()) { 128 while (!window.should_close and p.isChildAlive()) {
124 // Flush any pending wayland requests 129 // Flush any pending wayland requests
@@ -154,8 +159,9 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
154 if (ev.action == .release) continue; 159 if (ev.action == .release) continue;
155 if (ev.utf8_len > 0) { 160 if (ev.utf8_len > 0) {
156 _ = try p.write(ev.utf8[0..ev.utf8_len]); 161 _ = try p.write(ev.utf8[0..ev.utf8_len]);
162 } else if (try encodeKeyboardEvent(&term, ev, &key_buf)) |encoded| {
163 _ = try p.write(encoded);
157 } 164 }
158 // Keys without UTF-8 (arrows, function keys) are skipped for v1
159 } 165 }
160 keyboard.event_queue.clearRetainingCapacity(); 166 keyboard.event_queue.clearRetainingCapacity();
161 167
@@ -210,6 +216,60 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
210 _ = try ctx.vkd.deviceWaitIdle(ctx.device); 216 _ = try ctx.vkd.deviceWaitIdle(ctx.device);
211 } 217 }
212 218
219 fn encodeKeyboardEvent(
220 term: *const vt.Terminal,
221 ev: wayland_client.KeyboardEvent,
222 buf: []u8,
223 ) !?[]const u8 {
224 const key = mapKeysymToInputKey(ev.keysym) orelse return null;
225 const encoded = try term.encodeKey(
226 key,
227 .{
228 .shift = ev.modifiers.shift,
229 .ctrl = ev.modifiers.ctrl,
230 .alt = ev.modifiers.alt,
231 .super = ev.modifiers.super,
232 },
233 switch (ev.action) {
234 .press => .press,
235 .release => .release,
236 .repeat => .repeat,
237 },
238 buf,
239 );
240
241 if (encoded.len == 0) return null;
242 return encoded;
243 }
244
245 fn mapKeysymToInputKey(keysym: u32) ?vt.InputKey {
246 return switch (keysym) {
247 c.XKB_KEY_Up => .arrow_up,
248 c.XKB_KEY_Down => .arrow_down,
249 c.XKB_KEY_Left => .arrow_left,
250 c.XKB_KEY_Right => .arrow_right,
251 c.XKB_KEY_Home => .home,
252 c.XKB_KEY_End => .end,
253 c.XKB_KEY_Page_Up => .page_up,
254 c.XKB_KEY_Page_Down => .page_down,
255 c.XKB_KEY_Insert => .insert,
256 c.XKB_KEY_Delete => .delete,
257 c.XKB_KEY_F1 => .f1,
258 c.XKB_KEY_F2 => .f2,
259 c.XKB_KEY_F3 => .f3,
260 c.XKB_KEY_F4 => .f4,
261 c.XKB_KEY_F5 => .f5,
262 c.XKB_KEY_F6 => .f6,
263 c.XKB_KEY_F7 => .f7,
264 c.XKB_KEY_F8 => .f8,
265 c.XKB_KEY_F9 => .f9,
266 c.XKB_KEY_F10 => .f10,
267 c.XKB_KEY_F11 => .f11,
268 c.XKB_KEY_F12 => .f12,
269 else => null,
270 };
271 }
272
213 fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { 273 fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
214 var conn = try wayland_client.Connection.init(); 274 var conn = try wayland_client.Connection.init();
215 defer conn.deinit(); 275 defer conn.deinit();
@@ -295,6 +355,25 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void {
295 std.debug.print("done\n", .{}); 355 std.debug.print("done\n", .{});
296 } 356 }
297 357
358 test "encodeKeyboardEvent encodes left arrow" {
359 var term = try vt.Terminal.init(std.testing.allocator, .{
360 .cols = 80,
361 .rows = 24,
362 });
363 defer term.deinit();
364
365 var buf: [32]u8 = undefined;
366 const encoded = (try encodeKeyboardEvent(&term, .{
367 .keysym = c.XKB_KEY_Left,
368 .modifiers = .{},
369 .action = .press,
370 .utf8 = [_]u8{0} ** 8,
371 .utf8_len = 0,
372 }, &buf)).?;
373
374 try std.testing.expectEqualStrings("\x1b[D", encoded);
375 }
376
298 fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { 377 fn runRenderSmokeTest(alloc: std.mem.Allocator) !void {
299 var conn = try wayland_client.Connection.init(); 378 var conn = try wayland_client.Connection.init();
300 defer conn.deinit(); 379 defer conn.deinit();
src/vt.zig
Old New
@@ -50,6 +50,10 @@
50 const std = @import("std"); 50 const std = @import("std");
51 const ghostty_vt = @import("ghostty-vt"); 51 const ghostty_vt = @import("ghostty-vt");
52 52
53 pub const InputAction = ghostty_vt.input.KeyAction;
54 pub const InputKey = ghostty_vt.input.Key;
55 pub const InputMods = ghostty_vt.input.KeyMods;
56
53 pub const Terminal = struct { 57 pub const Terminal = struct {
54 alloc: std.mem.Allocator, 58 alloc: std.mem.Allocator,
55 inner: ghostty_vt.Terminal, 59 inner: ghostty_vt.Terminal,
@@ -105,6 +109,23 @@ pub const Terminal = struct {
105 self.stream.nextSlice(bytes); 109 self.stream.nextSlice(bytes);
106 } 110 }
107 111
112 pub fn encodeKey(
113 self: *const Terminal,
114 key: InputKey,
115 mods: InputMods,
116 action: InputAction,
117 buf: []u8,
118 ) ![]const u8 {
119 var writer: std.Io.Writer = .fixed(buf);
120 try ghostty_vt.input.encodeKey(&writer, .{
121 .action = action,
122 .key = key,
123 .mods = mods,
124 .consumed_mods = .{},
125 }, .fromTerminal(&self.inner));
126 return writer.buffered();
127 }
128
108 pub fn resize(self: *Terminal, new_cols: u16, new_rows: u16) !void { 129 pub fn resize(self: *Terminal, new_cols: u16, new_rows: u16) !void {
109 try self.inner.resize(self.alloc, @intCast(new_cols), @intCast(new_rows)); 130 try self.inner.resize(self.alloc, @intCast(new_cols), @intCast(new_rows));
110 } 131 }
@@ -158,3 +179,15 @@ test "Terminal.resize" {
158 try std.testing.expectEqual(@as(u16, 120), term.cols()); 179 try std.testing.expectEqual(@as(u16, 120), term.cols());
159 try std.testing.expectEqual(@as(u16, 40), term.rows()); 180 try std.testing.expectEqual(@as(u16, 40), term.rows());
160 } 181 }
182
183 test "Terminal encodes non-text arrow keys" {
184 var term = try Terminal.init(std.testing.allocator, .{
185 .cols = 80,
186 .rows = 24,
187 });
188 defer term.deinit();
189
190 var buf: [32]u8 = undefined;
191 const encoded = try term.encodeKey(.arrow_left, .{}, .press, &buf);
192 try std.testing.expectEqualStrings("\x1b[D", encoded);
193 }