47eefde2
feat: full terminal integration — waystty works
a73x 2026-04-08 09:33
Wire all modules into a working terminal: font→atlas→PTY→vt→render loop. Fix glyph shader to use per-instance glyph_size+bearing instead of cell_size, update Instance struct and vertex attribute descriptions to match new layout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
diff --git a/shaders/cell.vert b/shaders/cell.vert index e82987b..dcfdd79 100644 --- a/shaders/cell.vert +++ b/shaders/cell.vert @@ -8,16 +8,20 @@ layout(push_constant) uniform PushConstants { layout(location = 0) in vec2 in_unit_pos; layout(location = 1) in vec2 in_cell_pos; layout(location = 2) in vec4 in_uv_rect; layout(location = 3) in vec4 in_fg_color; layout(location = 4) in vec4 in_bg_color; layout(location = 2) in vec2 in_glyph_size; layout(location = 3) in vec2 in_glyph_bearing; layout(location = 4) in vec4 in_uv_rect; layout(location = 5) in vec4 in_fg_color; layout(location = 6) in vec4 in_bg_color; layout(location = 0) out vec2 out_uv; layout(location = 1) out vec4 out_fg; layout(location = 2) out vec4 out_bg; void main() { vec2 pixel_pos = (in_cell_pos + in_unit_pos) * pc.cell_size; vec2 cell_origin = in_cell_pos * pc.cell_size; vec2 glyph_origin = cell_origin + in_glyph_bearing; vec2 pixel_pos = glyph_origin + in_unit_pos * in_glyph_size; vec2 ndc = (pixel_pos / pc.viewport_size) * 2.0 - 1.0; gl_Position = vec4(ndc, 0.0, 1.0); diff --git a/src/main.zig b/src/main.zig index 5ef28ab..7c4ca2a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,7 +33,181 @@ pub fn main() !void { return runDrawSmokeTest(alloc); } std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); return runTerminal(alloc); } fn runTerminal(alloc: std.mem.Allocator) !void { // === font first, to know cell size === var font_lookup = try font.lookupMonospace(alloc); defer font_lookup.deinit(alloc); const font_size: u32 = 16; var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, font_size); defer face.deinit(); const cell_w = face.cellWidth(); const cell_h = face.cellHeight(); // === grid size === const cols: u16 = 80; const rows: u16 = 24; const initial_w: u32 = @as(u32, cols) * cell_w; const initial_h: u32 = @as(u32, rows) * cell_h; // === wayland === var conn = try wayland_client.Connection.init(); defer conn.deinit(); const window = try conn.createWindow(alloc, "waystty"); defer window.deinit(); // Set window to desired terminal dimensions window.width = initial_w; window.height = initial_h; _ = conn.display.roundtrip(); // === keyboard === var keyboard = try wayland_client.Keyboard.init(alloc, conn.globals.seat.?); defer keyboard.deinit(); // === renderer === var ctx = try renderer.Context.init( alloc, @ptrCast(conn.display), @ptrCast(window.surface), window.width, window.height, ); defer ctx.deinit(); // === glyph atlas === var atlas = try font.Atlas.init(alloc, 1024, 1024); defer atlas.deinit(); // Upload empty atlas first (so descriptor set is valid) try ctx.uploadAtlas(atlas.pixels); // === terminal === var term = try vt.Terminal.init(alloc, .{ .cols = cols, .rows = rows, .max_scrollback = 1000, }); defer term.deinit(); // === pty === const shell: [:0]const u8 = blk: { const shell_env = std.posix.getenv("SHELL") orelse "/bin/sh"; break :blk try alloc.dupeZ(u8, shell_env); }; defer alloc.free(shell); var p = try pty.Pty.spawn(.{ .cols = cols, .rows = rows, .shell = shell }); defer p.deinit(); // === instance buffer === var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; defer instances.deinit(alloc); try instances.ensureTotalCapacity(alloc, @as(usize, cols) * rows); // === main loop === const wl_fd = conn.display.getFd(); var pollfds = [_]std.posix.pollfd{ .{ .fd = wl_fd, .events = std.posix.POLL.IN, .revents = 0 }, .{ .fd = p.master_fd, .events = std.posix.POLL.IN, .revents = 0 }, }; var read_buf: [8192]u8 = undefined; while (!window.should_close and p.isChildAlive()) { // Flush any pending wayland requests _ = conn.display.flush(); // Poll with 16ms timeout so we get a render tick even without events _ = std.posix.poll(&pollfds, 16) catch {}; // Wayland events: prepare_read / read_events / dispatch_pending if (pollfds[0].revents & std.posix.POLL.IN != 0) { if (conn.display.prepareRead()) { _ = conn.display.readEvents(); } } _ = conn.display.dispatchPending(); // PTY output if (pollfds[1].revents & std.posix.POLL.IN != 0) { while (true) { const n = p.read(&read_buf) catch |err| switch (err) { error.WouldBlock => break, error.InputOutput => break, // child likely exited else => return err, }; if (n == 0) break; term.write(read_buf[0..n]); } } // Keyboard events → write to pty keyboard.tickRepeat(); for (keyboard.event_queue.items) |ev| { if (ev.action == .release) continue; if (ev.utf8_len > 0) { _ = try p.write(ev.utf8[0..ev.utf8_len]); } // Keys without UTF-8 (arrows, function keys) are skipped for v1 } keyboard.event_queue.clearRetainingCapacity(); // === render === try term.snapshot(); instances.clearRetainingCapacity(); const term_rows = term.render_state.row_data.items(.cells); var row_idx: u32 = 0; while (row_idx < term_rows.len) : (row_idx += 1) { const raw_cells = term_rows[row_idx].items(.raw); var col_idx: u32 = 0; while (col_idx < raw_cells.len) : (col_idx += 1) { const cp = raw_cells[col_idx].codepoint(); if (cp == 0 or cp == ' ') continue; const uv = atlas.getOrInsert(&face, @intCast(cp)) catch continue; try instances.append(alloc, .{ .cell_pos = .{ @floatFromInt(col_idx), @floatFromInt(row_idx) }, .glyph_size = .{ @floatFromInt(uv.width), @floatFromInt(uv.height) }, .glyph_bearing = .{ @floatFromInt(uv.bearing_x), // bearing_y in freetype is from baseline going up; our quad origin is top-left going down // cell_h - bearing_y gives the offset from the top of the cell @as(f32, @floatFromInt(cell_h)) - @as(f32, @floatFromInt(uv.bearing_y)), }, .uv_rect = .{ uv.u0, uv.v0, uv.u1, uv.v1 }, .fg = .{ 0.9, 0.9, 0.9, 1.0 }, .bg = .{ 0.08, 0.08, 0.08, 1.0 }, }); } } // Re-upload atlas if new glyphs were added if (atlas.dirty) { try ctx.uploadAtlas(atlas.pixels); atlas.dirty = false; } if (instances.items.len > 0) { try ctx.uploadInstances(instances.items); } try ctx.drawCells( @intCast(instances.items.len), .{ @floatFromInt(cell_w), @floatFromInt(cell_h) }, ); } _ = try ctx.vkd.deviceWaitIdle(ctx.device); } fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { @@ -88,6 +262,11 @@ fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { const instances = [_]renderer.Instance{ .{ .cell_pos = .{ 40.0, 12.0 }, .glyph_size = .{ @floatFromInt(glyph_uv.width), @floatFromInt(glyph_uv.height) }, .glyph_bearing = .{ @floatFromInt(glyph_uv.bearing_x), @as(f32, @floatFromInt(face.cellHeight())) - @as(f32, @floatFromInt(glyph_uv.bearing_y)), }, .uv_rect = .{ glyph_uv.u0, glyph_uv.v0, glyph_uv.u1, glyph_uv.v1 }, .fg = .{ 1.0, 1.0, 1.0, 1.0 }, .bg = .{ 0.0, 0.0, 0.0, 1.0 }, diff --git a/src/renderer.zig b/src/renderer.zig index a8adf59..fc2def4 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -211,10 +211,12 @@ pub const Vertex = extern struct { /// Per-instance data (binding 1, per-instance rate) pub const Instance = extern struct { cell_pos: [2]f32, // location 1 uv_rect: [4]f32, // location 2 fg: [4]f32, // location 3 bg: [4]f32, // location 4 cell_pos: [2]f32, // location 1 glyph_size: [2]f32, // location 2 glyph_bearing: [2]f32, // location 3 uv_rect: [4]f32, // location 4 fg: [4]f32, // location 5 bg: [4]f32, // location 6 }; const BufferResult = struct { @@ -533,9 +535,11 @@ pub const Context = struct { const attr_descs = [_]vk.VertexInputAttributeDescription{ .{ .location = 0, .binding = 0, .format = .r32g32_sfloat, .offset = 0 }, .{ .location = 1, .binding = 1, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "cell_pos") }, .{ .location = 2, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "uv_rect") }, .{ .location = 3, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "fg") }, .{ .location = 4, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "bg") }, .{ .location = 2, .binding = 1, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "glyph_size") }, .{ .location = 3, .binding = 1, .format = .r32g32_sfloat, .offset = @offsetOf(Instance, "glyph_bearing") }, .{ .location = 4, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "uv_rect") }, .{ .location = 5, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "fg") }, .{ .location = 6, .binding = 1, .format = .r32g32b32a32_sfloat, .offset = @offsetOf(Instance, "bg") }, }; const vertex_input_info = vk.PipelineVertexInputStateCreateInfo{ @@ -1085,7 +1089,7 @@ pub const Context = struct { }); const clear_value = vk.ClearValue{ .color = .{ .float_32 = .{ 0.0, 0.0, 0.0, 1.0 } }, .color = .{ .float_32 = .{ 0.08, 0.08, 0.08, 1.0 } }, }; self.vkd.cmdBeginRenderPass(self.command_buffer, &vk.RenderPassBeginInfo{