Files
ghostty/src/terminal/c/formatter.zig
Mitchell Hashimoto 3c8feda118 vt: add format_alloc to C API formatter
Rename the existing format function to format_buf to clarify that it
writes into a caller-provided buffer. Add a new format_alloc variant
that allocates the output buffer internally using the provided
allocator (or the default if NULL). The caller receives the allocated
pointer and length and is responsible for freeing it.

This is useful for consumers that do not know the required buffer size
ahead of time and want to avoid the two-pass query-then-format pattern
needed with format_buf.
2026-03-14 14:59:41 -07:00

418 lines
14 KiB
Zig

const std = @import("std");
const testing = std.testing;
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const terminal_c = @import("terminal.zig");
const ZigTerminal = @import("../Terminal.zig");
const formatterpkg = @import("../formatter.zig");
const Result = @import("result.zig").Result;
/// Wrapper around formatter that tracks the allocator for C API usage.
const FormatterWrapper = struct {
kind: Kind,
alloc: std.mem.Allocator,
const Kind = union(enum) {
terminal: formatterpkg.TerminalFormatter,
};
};
/// C: GhosttyFormatter
pub const Formatter = ?*FormatterWrapper;
/// C: GhosttyFormatterFormat
pub const Format = formatterpkg.Format;
/// C: GhosttyFormatterScreenOptions
pub const ScreenOptions = extern struct {
/// C: GhosttyFormatterScreenExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
cursor: bool,
style: bool,
hyperlink: bool,
protection: bool,
kitty_keyboard: bool,
charsets: bool,
comptime {
for (std.meta.fieldNames(formatterpkg.ScreenFormatter.Extra)) |name| {
if (!@hasField(Extra, name))
@compileError("ScreenOptions.Extra missing field: " ++ name);
}
}
fn toZig(self: Extra) formatterpkg.ScreenFormatter.Extra {
return .{
.cursor = self.cursor,
.style = self.style,
.hyperlink = self.hyperlink,
.protection = self.protection,
.kitty_keyboard = self.kitty_keyboard,
.charsets = self.charsets,
};
}
};
};
/// C: GhosttyFormatterTerminalOptions
pub const TerminalOptions = extern struct {
size: usize = @sizeOf(TerminalOptions),
emit: Format,
unwrap: bool,
trim: bool,
extra: Extra,
/// C: GhosttyFormatterTerminalExtra
pub const Extra = extern struct {
size: usize = @sizeOf(Extra),
palette: bool,
modes: bool,
scrolling_region: bool,
tabstops: bool,
pwd: bool,
keyboard: bool,
screen: ScreenOptions.Extra,
comptime {
for (std.meta.fieldNames(formatterpkg.TerminalFormatter.Extra)) |name| {
if (!@hasField(Extra, name))
@compileError("TerminalOptions.Extra missing field: " ++ name);
}
}
fn toZig(self: Extra) formatterpkg.TerminalFormatter.Extra {
return .{
.palette = self.palette,
.modes = self.modes,
.scrolling_region = self.scrolling_region,
.tabstops = self.tabstops,
.pwd = self.pwd,
.keyboard = self.keyboard,
.screen = self.screen.toZig(),
};
}
};
};
pub fn terminal_new(
alloc_: ?*const CAllocator,
result: *Formatter,
terminal_: terminal_c.Terminal,
opts: TerminalOptions,
) callconv(.c) Result {
result.* = terminal_new_(
alloc_,
terminal_,
opts,
) catch |err| {
result.* = null;
return switch (err) {
error.InvalidValue => .invalid_value,
error.OutOfMemory => .out_of_memory,
};
};
return .success;
}
fn terminal_new_(
alloc_: ?*const CAllocator,
terminal_: terminal_c.Terminal,
opts: TerminalOptions,
) error{
InvalidValue,
OutOfMemory,
}!*FormatterWrapper {
const t = terminal_ orelse return error.InvalidValue;
const alloc = lib_alloc.default(alloc_);
const ptr = alloc.create(FormatterWrapper) catch
return error.OutOfMemory;
errdefer alloc.destroy(ptr);
var formatter: formatterpkg.TerminalFormatter = .init(t, .{
.emit = opts.emit,
.unwrap = opts.unwrap,
.trim = opts.trim,
});
formatter.extra = opts.extra.toZig();
ptr.* = .{
.kind = .{ .terminal = formatter },
.alloc = alloc,
};
return ptr;
}
pub fn format_buf(
formatter_: Formatter,
out_: ?[*]u8,
out_len: usize,
out_written: *usize,
) callconv(.c) Result {
const wrapper = formatter_ orelse return .invalid_value;
var writer: std.Io.Writer = .fixed(if (out_) |out|
out[0..out_len]
else
&.{});
switch (wrapper.kind) {
.terminal => |*t| t.format(&writer) catch |err| switch (err) {
error.WriteFailed => {
// On write failed we always report how much
// space we actually needed.
var discarding: std.Io.Writer.Discarding = .init(&.{});
t.format(&discarding.writer) catch unreachable;
out_written.* = @intCast(discarding.count);
return .out_of_space;
},
},
}
out_written.* = writer.end;
return .success;
}
pub fn format_alloc(
formatter_: Formatter,
alloc_: ?*const CAllocator,
out_ptr: *?[*]u8,
out_len: *usize,
) callconv(.c) Result {
const wrapper = formatter_ orelse return .invalid_value;
const alloc = lib_alloc.default(alloc_);
var aw: std.Io.Writer.Allocating = .init(alloc);
defer aw.deinit();
switch (wrapper.kind) {
.terminal => |*t| t.format(&aw.writer) catch return .out_of_memory,
}
const buf = aw.toOwnedSlice() catch return .out_of_memory;
out_ptr.* = buf.ptr;
out_len.* = buf.len;
return .success;
}
pub fn free(formatter_: Formatter) callconv(.c) void {
const wrapper = formatter_ orelse return;
const alloc = wrapper.alloc;
alloc.destroy(wrapper);
}
test "terminal_new/free" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
try testing.expect(f != null);
free(f);
}
test "terminal_new invalid_value on null terminal" {
var f: Formatter = null;
try testing.expectEqual(Result.invalid_value, terminal_new(
&lib_alloc.test_allocator,
&f,
null,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
try testing.expect(f == null);
}
test "free null" {
free(null);
}
test "format plain" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello", buf[0..written]);
}
test "format reflects terminal changes" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello", buf[0..written]);
// Write more data and re-format
terminal_c.vt_write(t, "\r\nWorld", 7);
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqualStrings("Hello\nWorld", buf[0..written]);
}
test "format null returns required size" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
// Pass null buffer to query required size
var required: usize = 0;
try testing.expectEqual(Result.out_of_space, format_buf(f, null, 0, &required));
try testing.expect(required > 0);
// Now allocate and format
var buf: [1024]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expectEqual(required, written);
}
test "format buffer too small" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Hello", 5);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .plain, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
// Buffer too small
var buf: [2]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.out_of_space, format_buf(f, &buf, buf.len, &written));
// written contains the required size
try testing.expectEqual(@as(usize, 5), written);
}
test "format null formatter" {
var written: usize = 0;
try testing.expectEqual(Result.invalid_value, format_buf(null, null, 0, &written));
}
test "format vt" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Test", 4);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .vt, .unwrap = false, .trim = true, .extra = .{ .palette = true, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = true, .hyperlink = true, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [65536]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expect(written > 0);
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Test") != null);
}
test "format html" {
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer terminal_c.free(t);
terminal_c.vt_write(t, "Html", 4);
var f: Formatter = null;
try testing.expectEqual(Result.success, terminal_new(
&lib_alloc.test_allocator,
&f,
t,
.{ .emit = .html, .unwrap = false, .trim = true, .extra = .{ .palette = false, .modes = false, .scrolling_region = false, .tabstops = false, .pwd = false, .keyboard = false, .screen = .{ .cursor = false, .style = false, .hyperlink = false, .protection = false, .kitty_keyboard = false, .charsets = false } } },
));
defer free(f);
var buf: [65536]u8 = undefined;
var written: usize = 0;
try testing.expectEqual(Result.success, format_buf(f, &buf, buf.len, &written));
try testing.expect(written > 0);
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Html") != null);
}