diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c/formatter.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig | 417 |
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); +} |
