mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-30 00:35:23 +00:00
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.
418 lines
14 KiB
Zig
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);
|
|
}
|