mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 22:10:29 +00:00
vt: wip formatter api
This commit is contained in:
@@ -143,6 +143,9 @@ comptime {
|
||||
@export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" });
|
||||
@export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" });
|
||||
@export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
|
||||
@export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" });
|
||||
@export(&c.formatter_format, .{ .name = "ghostty_formatter_format" });
|
||||
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
|
||||
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
|
||||
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });
|
||||
@export(&c.terminal_reset, .{ .name = "ghostty_terminal_reset" });
|
||||
|
||||
344
src/terminal/c/formatter.zig
Normal file
344
src/terminal/c/formatter.zig
Normal file
@@ -0,0 +1,344 @@
|
||||
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: GhosttyFormatterExtra
|
||||
pub const Extra = enum(c_int) {
|
||||
none = 0,
|
||||
styles = 1,
|
||||
all = 2,
|
||||
};
|
||||
|
||||
/// C: GhosttyFormatterTerminalOptions
|
||||
pub const TerminalOptions = extern struct {
|
||||
emit: Format,
|
||||
unwrap: bool,
|
||||
trim: bool,
|
||||
extra: Extra,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const extra: formatterpkg.TerminalFormatter.Extra = switch (opts.extra) {
|
||||
.none => .none,
|
||||
.styles => .styles,
|
||||
.all => .all,
|
||||
};
|
||||
|
||||
var formatter: formatterpkg.TerminalFormatter = .init(t, .{
|
||||
.emit = opts.emit,
|
||||
.unwrap = opts.unwrap,
|
||||
.trim = opts.trim,
|
||||
});
|
||||
formatter.extra = extra;
|
||||
|
||||
ptr.* = .{
|
||||
.kind = .{ .terminal = formatter },
|
||||
.alloc = alloc,
|
||||
};
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
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 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 = .none },
|
||||
));
|
||||
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 = .none },
|
||||
));
|
||||
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 = .none },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, format(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 = .none },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, format(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(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 = .none },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
// Pass null buffer to query required size
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_space, format(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(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 = .none },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
// Buffer too small
|
||||
var buf: [2]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_space, format(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(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 = .styles },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
var buf: [65536]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, format(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 = .none },
|
||||
));
|
||||
defer free(f);
|
||||
|
||||
var buf: [65536]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, format(f, &buf, buf.len, &written));
|
||||
try testing.expect(written > 0);
|
||||
try testing.expect(std.mem.indexOf(u8, buf[0..written], "Html") != null);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub const color = @import("color.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
pub const key_event = @import("key_event.zig");
|
||||
pub const key_encode = @import("key_encode.zig");
|
||||
@@ -17,6 +18,10 @@ pub const osc_command_data = osc.commandData;
|
||||
|
||||
pub const color_rgb_get = color.rgb_get;
|
||||
|
||||
pub const formatter_terminal_new = formatter.terminal_new;
|
||||
pub const formatter_format = formatter.format;
|
||||
pub const formatter_free = formatter.free;
|
||||
|
||||
pub const sgr_new = sgr.new;
|
||||
pub const sgr_free = sgr.free;
|
||||
pub const sgr_reset = sgr.reset;
|
||||
@@ -62,6 +67,7 @@ pub const terminal_scroll_viewport = terminal.scroll_viewport;
|
||||
|
||||
test {
|
||||
_ = color;
|
||||
_ = formatter;
|
||||
_ = osc;
|
||||
_ = key_event;
|
||||
_ = key_encode;
|
||||
|
||||
@@ -3,4 +3,5 @@ pub const Result = enum(c_int) {
|
||||
success = 0,
|
||||
out_of_memory = -1,
|
||||
invalid_value = -2,
|
||||
out_of_space = -3,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("terminal_options");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const lib = @import("../lib/main.zig");
|
||||
const lib_target: lib.Target = if (build_options.c_abi) .c else .zig;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const color = @import("color.zig");
|
||||
const size = @import("size.zig");
|
||||
@@ -19,46 +22,47 @@ const Selection = @import("Selection.zig");
|
||||
const Style = @import("style.zig").Style;
|
||||
|
||||
/// Formats available.
|
||||
pub const Format = enum {
|
||||
/// Plain text.
|
||||
plain,
|
||||
pub const Format = lib.Enum(lib_target, &.{
|
||||
// Plain text.
|
||||
"plain",
|
||||
|
||||
/// Include VT sequences to preserve colors, styles, URLs, etc.
|
||||
/// This is predominantly SGR sequences but may contain others as needed.
|
||||
///
|
||||
/// Note that for reference colors, like palette indices, this will
|
||||
/// vary based on the formatter and you should see the docs. For example,
|
||||
/// PageFormatter with VT will emit SGR sequences with palette indices,
|
||||
/// not the color itself.
|
||||
///
|
||||
/// For VT, newlines will be emitted as `\r\n` so that the cursor properly
|
||||
/// moves back to the beginning prior emitting follow-up lines.
|
||||
vt,
|
||||
// Include VT sequences to preserve colors, styles, URLs, etc.
|
||||
// This is predominantly SGR sequences but may contain others as needed.
|
||||
//
|
||||
// Note that for reference colors, like palette indices, this will
|
||||
// vary based on the formatter and you should see the docs. For example,
|
||||
// PageFormatter with VT will emit SGR sequences with palette indices,
|
||||
// not the color itself.
|
||||
//
|
||||
// For VT, newlines will be emitted as `\r\n` so that the cursor properly
|
||||
// moves back to the beginning prior emitting follow-up lines.
|
||||
"vt",
|
||||
|
||||
/// HTML output.
|
||||
///
|
||||
/// This will emit inline styles for as much styling as possible,
|
||||
/// in the interest of simplicity and ease of editing. This isn't meant
|
||||
/// to build the most beautiful or efficient HTML, but rather to be
|
||||
/// stylistically correct.
|
||||
///
|
||||
/// For colors, RGB values are emitted as inline CSS (#RRGGBB) while palette
|
||||
/// indices use CSS variables (var(--vt-palette-N)). The palette colors are
|
||||
/// emitted by TerminalFormatter.Extra.palette as a <style> block if you
|
||||
/// want to also include that. But if you only format a screen or lower,
|
||||
/// the formatter doesn't have access to the current palette to render it.
|
||||
///
|
||||
/// Newlines are emitted as actual '\n' characters. Consumers should use
|
||||
/// CSS white-space: pre or pre-wrap to preserve spacing and alignment.
|
||||
html,
|
||||
// HTML output.
|
||||
//
|
||||
// This will emit inline styles for as much styling as possible,
|
||||
// in the interest of simplicity and ease of editing. This isn't meant
|
||||
// to build the most beautiful or efficient HTML, but rather to be
|
||||
// stylistically correct.
|
||||
//
|
||||
// For colors, RGB values are emitted as inline CSS (#RRGGBB) while palette
|
||||
// indices use CSS variables (var(--vt-palette-N)). The palette colors are
|
||||
// emitted by TerminalFormatter.Extra.palette as a <style> block if you
|
||||
// want to also include that. But if you only format a screen or lower,
|
||||
// the formatter doesn't have access to the current palette to render it.
|
||||
//
|
||||
// Newlines are emitted as actual '\n' characters. Consumers should use
|
||||
// CSS white-space: pre or pre-wrap to preserve spacing and alignment.
|
||||
"html",
|
||||
});
|
||||
|
||||
pub fn styled(self: Format) bool {
|
||||
return switch (self) {
|
||||
.plain => false,
|
||||
.html, .vt => true,
|
||||
};
|
||||
}
|
||||
};
|
||||
/// Returns true if the format emits styled output (not plaintext).
|
||||
pub fn formatStyled(fmt: Format) bool {
|
||||
return switch (fmt) {
|
||||
.plain => false,
|
||||
.html, .vt => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub const CodepointMap = struct {
|
||||
/// Unicode codepoint range to replace.
|
||||
@@ -1130,7 +1134,7 @@ pub const PageFormatter = struct {
|
||||
// If we're emitting styled output (not plaintext) and
|
||||
// the cell has some kind of styling or is not empty
|
||||
// then this isn't blank.
|
||||
if (self.opts.emit.styled() and
|
||||
if (formatStyled(self.opts.emit) and
|
||||
(!cell.isEmpty() or cell.hasStyling())) break :blank;
|
||||
|
||||
// Cells with no text are blank
|
||||
@@ -1186,7 +1190,7 @@ pub const PageFormatter = struct {
|
||||
style: {
|
||||
// If we aren't emitting styled output then we don't
|
||||
// have to worry about styles.
|
||||
if (!self.opts.emit.styled()) break :style;
|
||||
if (!formatStyled(self.opts.emit)) break :style;
|
||||
|
||||
// Get our cell style.
|
||||
const cell_style = self.cellStyle(cell);
|
||||
|
||||
Reference in New Issue
Block a user