mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: C api to stream formatter output through a GhosttyWriter
Add `ghostty_formatter_format` which uses a streaming GhosttyWriter type to write. Update the example to show this.
This commit is contained in:
@@ -4,6 +4,24 @@
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
typedef struct {
|
||||
FILE *file;
|
||||
size_t written;
|
||||
} OutputWriter;
|
||||
|
||||
static bool write_output(void *userdata, const uint8_t *data, size_t len) {
|
||||
OutputWriter *output = userdata;
|
||||
size_t offset = 0;
|
||||
while (offset < len) {
|
||||
size_t written = fwrite(data + offset, 1, len - offset, output->file);
|
||||
output->written += written;
|
||||
offset += written;
|
||||
if (written == 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Create a terminal with a small grid
|
||||
GhosttyTerminal terminal;
|
||||
@@ -14,8 +32,8 @@ int main() {
|
||||
// cursor movement and styling sequences.
|
||||
const char *commands[] = {
|
||||
"Line 1: Hello World!\r\n", // Simple text on row 1
|
||||
"Line 2: \033[1mBold\033[0m and " // Bold text on row 2
|
||||
"\033[4mUnderline\033[0m\r\n",
|
||||
("Line 2: \033[1mBold\033[0m and " // Bold text on row 2
|
||||
"\033[4mUnderline\033[0m\r\n"),
|
||||
"Line 3: placeholder\r\n", // Will be overwritten below
|
||||
"\033[3;1H", // CUP: move cursor back to row 3, col 1
|
||||
"\033[2K", // EL: erase the entire line
|
||||
@@ -39,19 +57,16 @@ int main() {
|
||||
result = ghostty_formatter_terminal_new(NULL, &formatter, terminal, fmt_opts);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Format into an allocated buffer
|
||||
uint8_t *buf = NULL;
|
||||
size_t len = 0;
|
||||
result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len);
|
||||
// Stream the formatted output directly to stdout. The writer retains the
|
||||
// exact byte count and reports destination errors through its return value.
|
||||
OutputWriter output = {.file = stdout};
|
||||
GhosttyWriter writer = {.write = write_output, .userdata = &output};
|
||||
printf("Formatted output:\n");
|
||||
result = ghostty_formatter_format(formatter, writer);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Print the formatted output
|
||||
printf("Formatted output (%zu bytes):\n", len);
|
||||
fwrite(buf, 1, len, stdout);
|
||||
printf("\n");
|
||||
printf("\n(%zu bytes)\n", output.written);
|
||||
|
||||
// Clean up
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/io.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
@@ -137,6 +138,30 @@ GHOSTTY_API GhosttyResult ghostty_formatter_terminal_new(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttyFormatterTerminalOptions options);
|
||||
|
||||
/**
|
||||
* Run the formatter and stream output to a writer.
|
||||
*
|
||||
* Each call formats the current terminal state and invokes the writer
|
||||
* synchronously as output becomes available. The callback may be called more
|
||||
* than once and must not call formatter or terminal APIs using the same
|
||||
* formatter or its terminal.
|
||||
*
|
||||
* If an error occurs, the writer may already contain a partial formatted
|
||||
* output. The operation cannot be resumed from that partial output. This
|
||||
* function does not flush or make the caller's destination durable.
|
||||
*
|
||||
* @param formatter The formatter handle (must not be NULL)
|
||||
* @param writer Destination writer whose write callback must not be NULL
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_IO_ERROR if the writer rejects
|
||||
* output, GHOSTTY_LIMIT_EXCEEDED if output accounting overflows, or
|
||||
* GHOSTTY_INVALID_VALUE if an argument is invalid
|
||||
*
|
||||
* @ingroup formatter
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_formatter_format(
|
||||
GhosttyFormatter formatter,
|
||||
GhosttyWriter writer);
|
||||
|
||||
/**
|
||||
* Run the formatter and produce output into the caller-provided buffer.
|
||||
*
|
||||
|
||||
@@ -270,6 +270,7 @@ comptime {
|
||||
@export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
|
||||
if (features.formatter) {
|
||||
@export(&c.formatter_terminal_new, .{ .name = "ghostty_formatter_terminal_new" });
|
||||
@export(&c.formatter_format, .{ .name = "ghostty_formatter_format" });
|
||||
@export(&c.formatter_format_buf, .{ .name = "ghostty_formatter_format_buf" });
|
||||
@export(&c.formatter_format_alloc, .{ .name = "ghostty_formatter_format_alloc" });
|
||||
@export(&c.formatter_free, .{ .name = "ghostty_formatter_free" });
|
||||
|
||||
@@ -2,6 +2,7 @@ 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");
|
||||
@@ -160,6 +161,26 @@ fn terminal_new_(
|
||||
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;
|
||||
|
||||
var adapter: io_c.WriterAdapter = .init(writer);
|
||||
switch (wrapper.kind) {
|
||||
.terminal => |*t| t.format(adapter.writer()) catch {
|
||||
if (adapter.invalid_write) return .limit_exceeded;
|
||||
if (adapter.callback_failed) return .io_error;
|
||||
return .io_error;
|
||||
},
|
||||
}
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn format_buf(
|
||||
formatter_: Formatter,
|
||||
out_: ?[*]u8,
|
||||
@@ -281,6 +302,96 @@ test "format plain" {
|
||||
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(
|
||||
|
||||
@@ -78,6 +78,7 @@ pub const focus_encode = focus.encode;
|
||||
pub const mode_report_encode = modes.report_encode;
|
||||
|
||||
pub const formatter_terminal_new = formatter.terminal_new;
|
||||
pub const formatter_format = formatter.format;
|
||||
pub const formatter_format_buf = formatter.format_buf;
|
||||
pub const formatter_format_alloc = formatter.format_alloc;
|
||||
pub const formatter_free = formatter.free;
|
||||
|
||||
Reference in New Issue
Block a user