const std = @import("std"); const testing = std.testing; const lib = @import("../lib.zig"); const CAllocator = lib.alloc.Allocator; const io_c = @import("io.zig"); const terminal_c = @import("terminal.zig"); const grid_ref = @import("grid_ref.zig"); const selection_c = @import("selection.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; const CSelection = selection_c.CSelection; /// 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, /// Optional selection to restrict output to a range. /// If null, the entire screen is formatted. selection: ?*const CSelection = null, /// 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(); // Setup the content that we're formatting if (opts.selection) |sel| formatter.content = .{ .selection = sel.toZig() orelse return error.InvalidValue, }; ptr.* = .{ .kind = .{ .terminal = formatter }, .alloc = alloc, }; return ptr; } /// Format directly to a synchronous C writer callback. pub fn format( formatter_: Formatter, writer: io_c.Writer, ) callconv(lib.calling_conv) Result { const wrapper = formatter_ orelse return .invalid_value; if (!writer.valid()) return .invalid_value; // Batch the formatter's many small writes into few callback calls. The // trailing flush upholds the API contract that success means the // callback has accepted every byte. var buffer: [io_c.WriterAdapter.recommended_buffer_len]u8 = undefined; var adapter: io_c.WriterAdapter = .initBuffered(writer, &buffer); format_: { switch (wrapper.kind) { .terminal => |*t| t.format(adapter.writer()) catch break :format_, } adapter.writer().flush() catch break :format_; return .success; } if (adapter.invalid_write) return .limit_exceeded; if (adapter.callback_failed) return .io_error; return .io_error; } 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, 80, 24, )); 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, 80, 24, )); 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 streams to writer" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, 80, 24, )); defer terminal_c.free(t); terminal_c.vt_write(t, "Hello\r\nWorld", 12); 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); const Context = struct { destination: []u8, offset: usize = 0, calls: usize = 0, fn write( userdata: ?*anyopaque, data: [*]const u8, len: usize, ) callconv(lib.calling_conv) bool { const self: *@This() = @ptrCast(@alignCast(userdata.?)); if (len > self.destination.len - self.offset) return false; @memcpy(self.destination[self.offset..][0..len], data[0..len]); self.offset += len; self.calls += 1; return true; } }; var buf: [1024]u8 = undefined; var context: Context = .{ .destination = &buf }; try testing.expectEqual(Result.success, format(f, .{ .write = &Context.write, .userdata = &context, })); try testing.expect(context.calls > 0); try testing.expectEqualStrings("Hello\nWorld", buf[0..context.offset]); } test "format writer errors" { const FailWriter = struct { fn write( _: ?*anyopaque, _: [*]const u8, _: usize, ) callconv(lib.calling_conv) bool { return false; } }; try testing.expectEqual(Result.invalid_value, format(null, .{ .write = &FailWriter.write, })); var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, 80, 24, )); 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); try testing.expectEqual(Result.invalid_value, format(f, .{})); try testing.expectEqual(Result.io_error, format(f, .{ .write = &FailWriter.write, })); } test "format reflects terminal changes" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, 80, 24, )); 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, 80, 24, )); 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, 80, 24, )); 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, 80, 24, )); 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 plain with selection" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, 80, 24, )); defer terminal_c.free(t); terminal_c.vt_write(t, "Hello World", 11); // Get grid refs for "World" (columns 6..10 on row 0) var start_ref: grid_ref.CGridRef = .{}; try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{ .tag = .active, .value = .{ .active = .{ .x = 6, .y = 0 } }, }, &start_ref)); var end_ref: grid_ref.CGridRef = .{}; try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{ .tag = .active, .value = .{ .active = .{ .x = 10, .y = 0 } }, }, &end_ref)); const sel: selection_c.CSelection = .{ .start = start_ref, .end = end_ref, }; var f: Formatter = null; try testing.expectEqual(Result.success, terminal_new( &lib.alloc.test_allocator, &f, t, .{ .emit = .plain, .unwrap = false, .trim = true, .selection = &sel, .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("World", buf[0..written]); } test "format html" { var t: terminal_c.Terminal = null; try testing.expectEqual(Result.success, terminal_c.new( &lib.alloc.test_allocator, &t, 80, 24, )); 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); }