a73x

b44b035d

Improve terminal responsiveness and state handling

a73x   2026-04-08 17:33


diff --git a/.codex b/.codex
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.codex
diff --git a/src/main.zig b/src/main.zig
index c347b40..d0abf95 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -161,13 +161,14 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
    var key_buf: [32]u8 = undefined;
    var last_window_w = window.width;
    var last_window_h = window.height;
    var render_pending = true;

    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 {};
        const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs());
        _ = std.posix.poll(&pollfds, computePollTimeoutMs(repeat_timeout_ms, render_pending)) catch {};

        // Wayland events: prepare_read / read_events / dispatch_pending
        if (pollfds[0].revents & std.posix.POLL.IN != 0) {
@@ -187,6 +188,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
                };
                if (n == 0) break;
                term.write(read_buf[0..n]);
                render_pending = true;
            }
        }

@@ -236,8 +238,11 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
            }
            last_window_w = window.width;
            last_window_h = window.height;
            render_pending = true;
        }

        if (!shouldRenderFrame(render_pending, false, false)) continue;

        // === render ===
        try term.snapshot();

@@ -318,10 +323,12 @@ fn runTerminal(alloc: std.mem.Allocator) !void {
            error.OutOfDateKHR => {
                _ = try ctx.vkd.deviceWaitIdle(ctx.device);
                try ctx.recreateSwapchain(window.width, window.height);
                render_pending = true;
                continue;
            },
            else => return err,
        };
        render_pending = false;
    }

    _ = try ctx.vkd.deviceWaitIdle(ctx.device);
@@ -343,6 +350,23 @@ fn isClipboardPasteEvent(ev: wayland_client.KeyboardEvent) bool {
        ev.keysym == c.XKB_KEY_V;
}

fn remainingRepeatTimeoutMs(deadline_ns: ?i128) ?i32 {
    const deadline = deadline_ns orelse return null;
    const now = std.time.nanoTimestamp();
    if (deadline <= now) return 0;
    const remaining_ns = deadline - now;
    return @intCast(@divTrunc(remaining_ns + std.time.ns_per_ms - 1, std.time.ns_per_ms));
}

fn computePollTimeoutMs(next_repeat_in_ms: ?i32, render_pending: bool) i32 {
    if (render_pending) return 0;
    return next_repeat_in_ms orelse -1;
}

fn shouldRenderFrame(terminal_dirty: bool, window_dirty: bool, forced: bool) bool {
    return terminal_dirty or window_dirty or forced;
}

fn appendCellInstances(
    alloc: std.mem.Allocator,
    instances: *std.ArrayListUnmanaged(renderer.Instance),
@@ -439,6 +463,19 @@ fn mapKeysymToInputKey(keysym: u32) ?vt.InputKey {
    };
}

test "event loop waits indefinitely when idle and wakes for imminent repeat" {
    try std.testing.expectEqual(@as(i32, -1), computePollTimeoutMs(null, false));
    try std.testing.expectEqual(@as(i32, 0), computePollTimeoutMs(5, true));
    try std.testing.expectEqual(@as(i32, 17), computePollTimeoutMs(17, false));
}

test "event loop redraws only when terminal or window state changed" {
    try std.testing.expect(shouldRenderFrame(true, false, false));
    try std.testing.expect(shouldRenderFrame(false, true, false));
    try std.testing.expect(shouldRenderFrame(false, false, true));
    try std.testing.expect(!shouldRenderFrame(false, false, false));
}

fn runDrawSmokeTest(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 c216316..3d4912c 100644
--- a/src/vt.zig
+++ b/src/vt.zig
@@ -49,6 +49,7 @@

const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
const color = ghostty_vt.color;
const DeviceAttributesCallback = std.meta.Child(
    @FieldType(ghostty_vt.TerminalStream.Handler.Effects, "device_attributes"),
);
@@ -97,13 +98,19 @@ pub const Terminal = struct {
            .cols = @intCast(opts.cols),
            .rows = @intCast(opts.rows),
            .max_scrollback = @intCast(opts.max_scrollback),
            .colors = .{
                .background = color.DynamicRGB.init(.{ .r = 0x00, .g = 0x00, .b = 0x00 }),
                .foreground = color.DynamicRGB.init(.{ .r = 0xff, .g = 0xff, .b = 0xff }),
                .cursor = .unset,
                .palette = .default,
            },
        });
        errdefer inner.deinit(alloc);

        self.* = .{
            .alloc = alloc,
            .inner = inner,
            .stream = .init(.{
            .stream = .initAlloc(alloc, .{
                .terminal = &self.inner,
            }),
            .render_state = .empty,
@@ -328,6 +335,27 @@ test "Terminal resolves ANSI fg and bg colors from render state" {
    try std.testing.expectEqualDeep(rgbToFloat4(palette[4]), colors.bg);
}

test "Terminal applies OSC 11 background color updates" {
    var term = try Terminal.init(std.testing.allocator, .{
        .cols = 80,
        .rows = 24,
    });
    defer term.deinit();

    term.write("\x1b]11;rgb:12/34/56\x1b\\");
    try term.snapshot();

    try std.testing.expectEqualDeep(
        [_]f32{
            @as(f32, 0x12) / 255.0,
            @as(f32, 0x34) / 255.0,
            @as(f32, 0x56) / 255.0,
            1.0,
        },
        term.backgroundColor(),
    );
}

test "Terminal title callback fires on OSC 2" {
    var term = try Terminal.init(std.testing.allocator, .{
        .cols = 80,
diff --git a/src/wayland.zig b/src/wayland.zig
index 5fe1b53..5443aba 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -103,6 +103,14 @@ pub const Keyboard = struct {
            40;
        self.next_repeat_time_ns = now + interval_ms * std.time.ns_per_ms;
    }

    pub fn nextRepeatDeadlineNs(self: *const Keyboard) ?i128 {
        if (!self.has_focus) return null;
        _ = self.xkb_state orelse return null;
        _ = self.last_keycode orelse return null;
        if (self.repeat_rate == 0) return null;
        return self.next_repeat_time_ns;
    }
};

pub const Globals = struct {
@@ -114,7 +122,7 @@ pub const Globals = struct {

const ClipboardOffer = struct {
    offer: *wl.DataOffer,
    mime_types: std.ArrayList([]u8),
    mime_types: std.ArrayList([:0]u8),

    fn init(offer: *wl.DataOffer) ClipboardOffer {
        return .{
@@ -176,7 +184,7 @@ pub const Clipboard = struct {
        std.posix.close(pipefds[1]);
        write_fd_closed = true;

        const roundtrip_ctx = struct {
        var roundtrip_ctx = struct {
            display: *wl.Display,

            fn roundtrip(ctx: *@This()) !void {
@@ -210,7 +218,7 @@ pub fn drainSelectionPipeThenRoundtrip(
    return try out.toOwnedSlice(alloc);
}

fn choosePreferredMime(mime_types: []const []const u8) ?[]const u8 {
fn choosePreferredMime(mime_types: []const [:0]const u8) ?[:0]const u8 {
    for (mime_types) |mime| {
        if (std.mem.eql(u8, mime, "text/plain;charset=utf-8")) return mime;
    }
@@ -407,7 +415,7 @@ fn dataOfferListener(_: *wl.DataOffer, event: wl.DataOffer.Event, clipboard: *Cl
    switch (event) {
        .offer => |mime| {
            var offer = &(clipboard.pending_offer orelse return);
            const dup = clipboard.alloc.dupe(u8, std.mem.span(mime.mime_type)) catch return;
            const dup = clipboard.alloc.dupeZ(u8, std.mem.span(mime.mime_type)) catch return;
            offer.mime_types.append(clipboard.alloc, dup) catch {
                clipboard.alloc.free(dup);
            };
@@ -443,6 +451,18 @@ fn dataDeviceListener(_: *wl.DataDevice, event: wl.DataDevice.Event, clipboard: 
    }
}

test "choosePreferredMime preserves sentinel-terminated mime type" {
    const mime_types = [_][:0]const u8{
        "text/html",
        "text/plain;charset=utf-8",
        "text/plain",
    };

    const mime = choosePreferredMime(&mime_types) orelse return error.TestUnexpectedResult;
    try std.testing.expectEqualStrings("text/plain;charset=utf-8", mime);
    try std.testing.expect(mime[mime.len] == 0);
}

fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
    switch (event) {
        .ping => |p| wm_base.pong(p.serial),
@@ -537,7 +557,8 @@ test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" {
        }
    };

    const thread = try std.Thread.spawn(.{}, Writer.run, .{.{ .fd = pipefds[1] }});
    const writer = Writer{ .fd = pipefds[1] };
    const thread = try std.Thread.spawn(.{}, Writer.run, .{writer});
    defer thread.join();

    var roundtrip_ctx = RoundtripCtx{ .called = &roundtrip_called };