diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
| commit | dcfe11a7588fcd42471159453810abc9b7720a57 (patch) | |
| tree | c32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/terminal/c/sys.zig | |
| parent | 2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff) | |
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround.
expose the new selection formatter and hyperlink uri apis for follow-up rendering work.
add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c/sys.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/sys.zig | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/sys.zig b/vendor/libghostty-vt/src/terminal/c/sys.zig new file mode 100644 index 00000000..c4b2b17f --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/c/sys.zig @@ -0,0 +1,430 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const lib = @import("../lib.zig"); +const CAllocator = lib.alloc.Allocator; +const terminal_sys = @import("../sys.zig"); +const Result = @import("result.zig").Result; + +/// C: GhosttySysImage +pub const Image = extern struct { + width: u32, + height: u32, + data: ?[*]u8, + data_len: usize, +}; + +/// C: GhosttySysDecodePngFn +pub const DecodePngFn = *const fn ( + ?*anyopaque, + *const CAllocator, + [*]const u8, + usize, + *Image, +) callconv(lib.calling_conv) bool; + +/// C: GhosttySysLogLevel +pub const LogLevel = enum(c_int) { + @"error" = 0, + warning = 1, + info = 2, + debug = 3, + + pub fn fromStd(level: std.log.Level) LogLevel { + return switch (level) { + .err => .@"error", + .warn => .warning, + .info => .info, + .debug => .debug, + }; + } +}; + +/// C: GhosttySysLogFn +pub const LogFn = *const fn ( + ?*anyopaque, + LogLevel, + [*]const u8, + usize, + [*]const u8, + usize, +) callconv(lib.calling_conv) void; + +/// C: GhosttySysOption +pub const Option = enum(c_int) { + userdata = 0, + decode_png = 1, + log = 2, + + pub fn InType(comptime self: Option) type { + return switch (self) { + .userdata => ?*const anyopaque, + .decode_png => ?DecodePngFn, + .log => ?LogFn, + }; + } +}; + +/// Global state for the sys interface so we can call through to the C +/// callbacks from Zig. +const Global = struct { + userdata: ?*anyopaque = null, + decode_png: ?DecodePngFn = null, + log: ?LogFn = null, +}; + +/// Global state for the C sys interface. +var global: Global = .{}; + +/// Zig-compatible wrapper that calls through to the stored C callback. +/// The C callback allocates the pixel data through the provided allocator, +/// so we can take ownership directly. +fn decodePngWrapper( + alloc: std.mem.Allocator, + data: []const u8, +) terminal_sys.DecodeError!terminal_sys.Image { + const func = global.decode_png orelse return error.InvalidData; + + const c_alloc = CAllocator.fromZig(&alloc); + var out: Image = undefined; + if (!func(global.userdata, &c_alloc, data.ptr, data.len, &out)) return error.InvalidData; + + const result_data = out.data orelse return error.InvalidData; + + return .{ + .width = out.width, + .height = out.height, + .data = result_data[0..out.data_len], + }; +} + +pub fn set( + option: Option, + value: ?*const anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Option, @intFromEnum(option)) catch { + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| setTyped( + comptime_option, + @ptrCast(@alignCast(value)), + ), + }; +} + +fn setTyped( + comptime option: Option, + value: option.InType(), +) Result { + switch (option) { + .userdata => global.userdata = @constCast(value), + .decode_png => { + global.decode_png = value; + terminal_sys.decode_png = if (value != null) &decodePngWrapper else null; + }, + .log => global.log = value, + } + return .success; +} + +/// Dispatch a log message to the installed C callback, if any. +fn emitLog(level: LogLevel, scope: []const u8, message: []const u8) void { + const func = global.log orelse return; + func( + global.userdata, + level, + scope.ptr, + scope.len, + message.ptr, + message.len, + ); +} + +/// Emits logs in chunks. Almost all logs will be less than the chunk size +/// but this allows emitting larger logs without heap allocation. +const LogEmitter = struct { + c_level: LogLevel, + scope_text: []const u8, + buf: [2048]u8 = undefined, + pos: usize = 0, + + fn write(self: *@This(), bytes: []const u8) error{}!usize { + var remaining = bytes; + while (remaining.len > 0) { + const space = self.buf.len - self.pos; + if (space == 0) { + self.flush(); + continue; + } + + const n = @min(remaining.len, space); + @memcpy(self.buf[self.pos..][0..n], remaining[0..n]); + self.pos += n; + remaining = remaining[n..]; + } + + return bytes.len; + } + + fn flush(self: *@This()) void { + if (self.pos == 0) return; + emitLog( + self.c_level, + self.scope_text, + self.buf[0..self.pos], + ); + self.pos = 0; + } +}; + +/// Custom std.log sink for C ABI builds. +/// +/// When a log callback is installed via ghostty_sys_set(), messages are +/// dispatched through it. When no callback is installed, messages are +/// silently discarded. Large messages that exceed the stack buffer are +/// delivered across multiple callback invocations. +pub fn logFn( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + if (global.log == null) return; + + const scope_text: []const u8 = if (scope == .default) "" else @tagName(scope); + const c_level = LogLevel.fromStd(level); + + var ctx: LogEmitter = .{ + .c_level = c_level, + .scope_text = scope_text, + }; + const writer: std.io.GenericWriter( + *LogEmitter, + error{}, + LogEmitter.write, + ) = .{ .context = &ctx }; + + nosuspend writer.print(format, args) catch {}; + ctx.flush(); +} + +/// Built-in log callback that writes to stderr. +/// +/// Formats each message as "[level](scope): message\n". Can be passed +/// directly to ghostty_sys_set(GHOSTTY_SYS_OPT_LOG, &ghostty_sys_log_stderr). +/// +/// Uses std.debug.lockStderrWriter for thread-safe, mutex-protected output. +/// On freestanding/wasm targets this is a no-op (no stderr available). +pub fn logStderr( + _: ?*anyopaque, + level: LogLevel, + scope_ptr: [*]const u8, + scope_len: usize, + message_ptr: [*]const u8, + message_len: usize, +) callconv(lib.calling_conv) void { + if (comptime builtin.target.cpu.arch.isWasm()) return; + + const scope = scope_ptr[0..scope_len]; + const message = message_ptr[0..message_len]; + + const level_text = switch (level) { + .@"error" => "error", + .warning => "warning", + .info => "info", + .debug => "debug", + }; + + var buffer: [64]u8 = undefined; + const writer = std.debug.lockStderrWriter(&buffer); + defer std.debug.unlockStderrWriter(); + nosuspend { + if (scope.len > 0) { + writer.print("[{s}]({s}): {s}\n", .{ level_text, scope, message }) catch {}; + } else { + writer.print("[{s}]: {s}\n", .{ level_text, message }) catch {}; + } + } +} + +test "set decode_png with null clears" { + // Start from a known state. + global.decode_png = null; + terminal_sys.decode_png = null; + + try std.testing.expectEqual(Result.success, set(.decode_png, null)); + try std.testing.expect(terminal_sys.decode_png == null); +} + +test "set decode_png installs wrapper" { + const S = struct { + fn decode(_: ?*anyopaque, _: *const CAllocator, _: [*]const u8, _: usize, out: *Image) callconv(lib.calling_conv) bool { + out.* = .{ .width = 1, .height = 1, .data = null, .data_len = 0 }; + return true; + } + }; + + try std.testing.expectEqual(Result.success, set( + .decode_png, + @ptrCast(&S.decode), + )); + try std.testing.expect(terminal_sys.decode_png != null); + + // Clear it again. + try std.testing.expectEqual(Result.success, set(.decode_png, null)); + try std.testing.expect(terminal_sys.decode_png == null); +} + +test "set log with null clears" { + global.log = null; + + try std.testing.expectEqual(Result.success, set(.log, null)); + try std.testing.expect(global.log == null); +} + +test "set log installs callback" { + const S = struct { + var called: bool = false; + fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, _: [*]const u8, _: usize) callconv(lib.calling_conv) void { + called = true; + } + }; + + try std.testing.expectEqual(Result.success, set(.log, @ptrCast(&S.logCb))); + try std.testing.expect(global.log != null); + + emitLog(.info, "test", "hello"); + try std.testing.expect(S.called); + + // Clear it again. + S.called = false; + try std.testing.expectEqual(Result.success, set(.log, null)); + try std.testing.expect(global.log == null); + + emitLog(.info, "test", "should not arrive"); + try std.testing.expect(!S.called); +} + +test "logFn small message single chunk" { + const S = struct { + var call_count: usize = 0; + var total_len: usize = 0; + + fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void { + _ = msg; + call_count += 1; + total_len += msg_len; + } + }; + + S.call_count = 0; + S.total_len = 0; + global.log = @ptrCast(&S.logCb); + defer { + global.log = null; + } + + logFn(.info, .default, "hello", .{}); + + try std.testing.expectEqual(@as(usize, 1), S.call_count); + try std.testing.expectEqual(@as(usize, 5), S.total_len); +} + +test "logFn message exceeding chunk size is split" { + const S = struct { + var call_count: usize = 0; + var total_len: usize = 0; + + fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void { + _ = msg; + call_count += 1; + total_len += msg_len; + // Each chunk must not exceed the buffer size. + std.debug.assert(msg_len <= 2048); + } + }; + + S.call_count = 0; + S.total_len = 0; + global.log = @ptrCast(&S.logCb); + defer { + global.log = null; + } + + // Format a message larger than the 2048-byte buffer. + // 'A' repeated 3000 times via a fill format. + const fill: [3000]u8 = .{0x41} ** 3000; + logFn(.info, .default, "{s}", .{@as([]const u8, &fill)}); + + try std.testing.expect(S.call_count >= 2); + try std.testing.expectEqual(@as(usize, 3000), S.total_len); +} + +test "logFn message exactly at chunk boundary" { + const S = struct { + var call_count: usize = 0; + var total_len: usize = 0; + + fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void { + _ = msg; + call_count += 1; + total_len += msg_len; + std.debug.assert(msg_len <= 2048); + } + }; + + S.call_count = 0; + S.total_len = 0; + global.log = @ptrCast(&S.logCb); + defer { + global.log = null; + } + + // Exactly 2048 bytes — should emit one full chunk, no remainder. + const fill: [2048]u8 = .{0x42} ** 2048; + logFn(.info, .default, "{s}", .{@as([]const u8, &fill)}); + + try std.testing.expectEqual(@as(usize, 1), S.call_count); + try std.testing.expectEqual(@as(usize, 2048), S.total_len); +} + +test "logFn message exactly double chunk size" { + const S = struct { + var call_count: usize = 0; + var total_len: usize = 0; + + fn logCb(_: ?*anyopaque, _: LogLevel, _: [*]const u8, _: usize, msg: [*]const u8, msg_len: usize) callconv(lib.calling_conv) void { + _ = msg; + call_count += 1; + total_len += msg_len; + std.debug.assert(msg_len <= 2048); + } + }; + + S.call_count = 0; + S.total_len = 0; + global.log = @ptrCast(&S.logCb); + defer { + global.log = null; + } + + // Exactly 4096 bytes — should emit exactly two full chunks. + const fill: [4096]u8 = .{0x43} ** 4096; + logFn(.info, .default, "{s}", .{@as([]const u8, &fill)}); + + try std.testing.expectEqual(@as(usize, 2), S.call_count); + try std.testing.expectEqual(@as(usize, 4096), S.total_len); +} + +test "set userdata" { + var data: u32 = 42; + try std.testing.expectEqual(Result.success, set(.userdata, @ptrCast(&data))); + try std.testing.expect(global.userdata == @as(?*anyopaque, @ptrCast(&data))); + + // Clear it. + try std.testing.expectEqual(Result.success, set(.userdata, null)); + try std.testing.expect(global.userdata == null); +} |
