3d5b717d
Handle non-text key encoding
a73x 2026-04-08 11:36
Commit message
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 | } | ||