From 967a4b01a79c920f00de5aeb32615dece2fab0df Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Wed, 19 Aug 2026 13:36:46 +0000 Subject: [PATCH] c-api: fix runtime lifecycle edge cases Amp-Thread-ID: https://ampcode.com/threads/T-01a016b4-7f3b-723c-9a8b-daa5588744b0 Co-authored-by: Tim Culverhouse --- build.zig | 4 +-- include/vaxis.h | 17 +++++------ src/c_api.zig | 27 +++++++++++++++++ src/widgets/terminal/Terminal.zig | 49 ++++++++++++++++++++++++++++--- 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/build.zig b/build.zig index 747e340..2e687dd 100644 --- a/build.zig +++ b/build.zig @@ -202,7 +202,7 @@ pub fn build(b: *std.Build) void { }); c_test_mod.addCSourceFile(.{ .file = b.path("examples/c/parse.c"), - .flags = &.{"-std=c99"}, + .flags = &.{ "-std=c99", "-pedantic-errors" }, }); c_test_mod.addIncludePath(b.path("include")); c_test_mod.linkLibrary(static_lib); @@ -220,7 +220,7 @@ pub fn build(b: *std.Build) void { }); c_runtime_mod.addCSourceFile(.{ .file = b.path("examples/c/runtime.c"), - .flags = &.{"-std=c99"}, + .flags = &.{ "-std=c99", "-pedantic-errors" }, }); c_runtime_mod.addIncludePath(b.path("include")); c_runtime_mod.linkLibrary(static_lib); diff --git a/include/vaxis.h b/include/vaxis.h index 817ecdf..4b54304 100644 --- a/include/vaxis.h +++ b/include/vaxis.h @@ -34,7 +34,7 @@ extern "C" { #define VAXIS_ENUM_TYPED : int #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L #define VAXIS_ENUM_TYPED : int -#elif defined(__clang__) +#elif defined(__clang__) && !defined(__STRICT_ANSI__) #if __has_extension(c_fixed_enum) #define VAXIS_ENUM_TYPED : int #else @@ -192,6 +192,8 @@ typedef struct { uint8_t b; } vaxis_rgb; +/* An opaque terminal input parser. Not thread-safe: use one parser per input + * stream. Parsed events are owned by the parser. */ typedef struct vaxis_parser vaxis_parser; typedef struct vaxis_event vaxis_event; @@ -311,8 +313,8 @@ typedef struct { /* Embedded terminal/PTY. argv and options are copied. The handle is confined * to its creating thread except for its internal reader worker. Event text is - * borrowed until the next terminal call. PTYs are currently supported only - * on Linux; elsewhere new returns VAXIS_ERR_UNSUPPORTED. */ + * borrowed until the next vaxis_terminal_try_event call. PTYs are currently + * supported only on Linux; elsewhere new returns VAXIS_ERR_UNSUPPORTED. */ vaxis_result vaxis_terminal_new(const vaxis_string *argv, size_t argc, const vaxis_terminal_options *options, vaxis_terminal **terminal); @@ -394,7 +396,10 @@ vaxis_result vaxis_runtime_enter_alt_screen(vaxis_runtime *runtime); vaxis_result vaxis_runtime_exit_alt_screen(vaxis_runtime *runtime); vaxis_result vaxis_runtime_query_terminal(vaxis_runtime *runtime, uint64_t timeout_ns); +/* For a custom event loop, call send, parse and handle all query responses, + * then call finish to enable the detected terminal features. */ vaxis_result vaxis_runtime_query_terminal_send(vaxis_runtime *runtime); +vaxis_result vaxis_runtime_query_terminal_finish(vaxis_runtime *runtime); /* Apply a parsed event to runtime state. Capability events are intercepted in * the same way as vaxis.Loop; other events are accepted without mutation. */ vaxis_result vaxis_runtime_handle_event(vaxis_runtime *runtime, @@ -421,14 +426,8 @@ vaxis_result vaxis_runtime_transmit_image_base64(vaxis_runtime *runtime, void vaxis_runtime_free_transmitted_image(vaxis_runtime *runtime, uint32_t image_id); -/* An opaque terminal input parser. Not thread-safe: use one parser per - * input stream. */ -typedef struct vaxis_parser vaxis_parser; - /* An opaque parsed event, owned by the parser that produced it and valid * until the next parse call. Read it through the accessors below. */ -typedef struct vaxis_event vaxis_event; - /* Create a parser. Returns NULL on allocation failure. */ vaxis_parser *vaxis_parser_new(void); vaxis_parser *vaxis_parser_new_with_allocator( diff --git a/src/c_api.zig b/src/c_api.zig index ca6bbf0..8b778de 100644 --- a/src/c_api.zig +++ b/src/c_api.zig @@ -647,6 +647,11 @@ fn terminalCreate(custom: ?*const CAllocator, cargv: [*]const CString, argc: usi return t; } +fn validTerminalSize(size: CWinsize, scrollback_size: u16) bool { + return size.rows > 0 and size.cols > 0 and + scrollback_size <= std.math.maxInt(u16) - size.rows; +} + pub fn terminal_new(cargv: ?[*]const CString, argc: usize, opts: ?*const CTerminalOptions, out: ?*?*CTerminal) callconv(.c) Result { return terminal_new_with_allocator(null, cargv, argc, opts, out); } @@ -656,6 +661,7 @@ pub fn terminal_new_with_allocator(custom: ?*const CAllocator, cargv: ?[*]const if (builtin.os.tag != .linux) return .err_unsupported; if (argc == 0) return .err_invalid; const options = opts orelse return .err_invalid; + if (!validTerminalSize(options.size, options.scrollback_size)) return .err_range; const handle = terminalCreate(custom, cargv orelse return .err_invalid, argc, options.*) catch |err| return switch (err) { error.InvalidArgument, error.Invalid => .err_invalid, error.OutOfMemory => .err_oom, @@ -683,6 +689,7 @@ pub fn terminal_resize(terminal: ?*CTerminal, size: CWinsize) callconv(.c) Resul return .err_unsupported; } const t = terminal orelse return .err_invalid; + if (!validTerminalSize(size, t.terminal.?.scrollback_size)) return .err_range; t.terminal.?.resize(zigWinsize(size)) catch return .err_io; return .ok; } @@ -868,6 +875,12 @@ pub fn runtime_query_terminal_send(runtime: ?*CRuntime) callconv(.c) Result { r.vx.?.queryTerminalSend(r.tty.tty.?.writer()) catch return .err_io; return .ok; } +pub fn runtime_query_terminal_finish(runtime: ?*CRuntime) callconv(.c) Result { + const r = runtime orelse return .err_invalid; + r.vx.?.queries_done.store(true, .unordered); + r.vx.?.enableDetectedFeatures(r.tty.tty.?.writer()) catch return .err_io; + return .ok; +} fn applyRuntimeEvent(vx: *Vaxis, e: *const CEvent) void { switch (e.type) { .key_press => { @@ -894,6 +907,10 @@ fn applyRuntimeEvent(vx: *Vaxis, e: *const CEvent) void { std.Io.futexWake(vx.io, std.atomic.Value(u32), &vx.query_futex, 10); vx.queries_done.store(true, .unordered); }, + .winsize => { + vx.state.in_band_resize = true; + if (comptime builtin.os.tag != .windows) Tty.resetSignalHandler(); + }, else => {}, } } @@ -1419,6 +1436,16 @@ test "c api: runtime capability events update Vaxis state" { event.type = .cap_unicode; applyRuntimeEvent(&vx, &event); try testing.expectEqual(vaxis.gwidth.Method.unicode, vx.caps.unicode); + event.type = .winsize; + applyRuntimeEvent(&vx, &event); + try testing.expect(vx.state.in_band_resize); +} + +test "c api: terminal dimensions are validated" { + try testing.expect(!validTerminalSize(.{ .rows = 0, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, 0)); + try testing.expect(!validTerminalSize(.{ .rows = 24, .cols = 0, .x_pixel = 0, .y_pixel = 0 }, 0)); + try testing.expect(!validTerminalSize(.{ .rows = 65_000, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, 1_000)); + try testing.expect(validTerminalSize(.{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 }, 500)); } test "c api: plain keypress with text" { diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index 38da161..d495ba4 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -11,7 +11,6 @@ const vaxis = @import("../../main.zig"); const Winsize = vaxis.Winsize; const Screen = @import("Screen.zig"); const Key = vaxis.Key; -const Queue = vaxis.Queue(Event, 16); const key = @import("key.zig"); pub const Event = union(enum) { @@ -22,6 +21,15 @@ pub const Event = union(enum) { pwd_change: []const u8, }; +const QueuedEvent = union(enum) { + exited, + redraw, + bell, + title_change: []u8, + pwd_change: []u8, +}; +const Queue = vaxis.Queue(QueuedEvent, 16); + const posix = std.posix; const log = std.log.scoped(.terminal); @@ -83,6 +91,7 @@ working_directory: std.ArrayList(u8) = .empty, last_printed: []const u8 = "", event_queue: Queue, +event_text: ?[]u8 = null, /// initialize a Terminal. This sets the size of the underlying pty and allocates the sizes of the /// screen @@ -151,6 +160,10 @@ pub fn deinit(self: *Terminal) void { thread.await(self.io); self.thread = null; } + if (self.event_text) |text| self.allocator.free(text); + while (self.event_queue.tryPop() catch null) |event| { + self.freeQueuedEvent(event); + } self.pty.deinit(self.io); self.front_screen.deinit(self.allocator); self.back_screen_pri.deinit(self.allocator); @@ -237,7 +250,31 @@ pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !v } pub fn tryEvent(self: *Terminal) !?Event { - return try self.event_queue.tryPop(); + if (self.event_text) |text| { + self.allocator.free(text); + self.event_text = null; + } + const event = try self.event_queue.tryPop() orelse return null; + return switch (event) { + .exited => .exited, + .redraw => .redraw, + .bell => .bell, + .title_change => |text| blk: { + self.event_text = text; + break :blk .{ .title_change = text }; + }, + .pwd_change => |text| blk: { + self.event_text = text; + break :blk .{ .pwd_change = text }; + }, + }; +} + +fn freeQueuedEvent(self: *Terminal, event: QueuedEvent) void { + switch (event) { + .title_change, .pwd_change => |text| self.allocator.free(text), + else => {}, + } } pub fn update(self: *Terminal, event: InputEvent) !void { @@ -682,7 +719,9 @@ fn _run(self: *Terminal) !void { 0 => { self.title.clearRetainingCapacity(); try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]); - try self.event_queue.push(.{ .title_change = self.title.items }); + const text = try self.allocator.dupe(u8, self.title.items); + errdefer self.allocator.free(text); + try self.event_queue.push(.{ .title_change = text }); }, 7 => { // OSC 7 ; file:// @@ -702,7 +741,9 @@ fn _run(self: *Terminal) !void { } else enc[i]; try self.working_directory.append(self.allocator, b); } - try self.event_queue.push(.{ .pwd_change = self.working_directory.items }); + const text = try self.allocator.dupe(u8, self.working_directory.items); + errdefer self.allocator.free(text); + try self.event_queue.push(.{ .pwd_change = text }); }, else => log.info("unhandled osc: {s}", .{osc}), } -- 2.51.2