aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/c/formatter.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c/formatter.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c/formatter.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/c/formatter.zig417
1 files changed, 417 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/formatter.zig b/vendor/libghostty-vt/src/terminal/c/formatter.zig
new file mode 100644
index 00000000..11717bc2
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/formatter.zig
@@ -0,0 +1,417 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.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(lib.calling_conv) 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: *ZigTerminal = (terminal_ orelse return error.InvalidValue).terminal;
+
+ 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(lib.calling_conv) 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(lib.calling_conv) 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(lib.calling_conv) 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);
+}