mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-29 02:21:43 +00:00
Terminal construction previously accepted GhosttyTerminalOptions with dimensions and one scrollback byte limit. Remove the options struct from the ABI and make ghostty_terminal_new accept columns and rows directly. Add byte and line limit options to ghostty_terminal_set and forward them to the runtime Terminal setters. NULL removes a limit, while zero bytes disables scrollback. Update type metadata, tests, and all API examples.
484 lines
16 KiB
Zig
484 lines
16 KiB
Zig
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 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;
|
|
}
|
|
|
|
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 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);
|
|
}
|