mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-29 03:47:54 +00:00
lib-vt: C API for SGR parser (#9352)
This exposes the SGR parser to the C and Wasm APIs. An example is shown
in c-vt-sgr.
Compressed example:
```c
#include <assert.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
// Create parser
GhosttySgrParser parser;
assert(ghostty_sgr_new(NULL, &parser) == GHOSTTY_SUCCESS);
// Parse: ESC[1;31m (bold + red foreground)
uint16_t params[] = {1, 31};
assert(ghostty_sgr_set_params(parser, params, NULL, 2) == GHOSTTY_SUCCESS);
printf("Parsing: ESC[1;31m\n\n");
// Iterate through attributes
GhosttySgrAttribute attr;
while (ghostty_sgr_next(parser, &attr)) {
switch (attr.tag) {
case GHOSTTY_SGR_ATTR_BOLD:
printf("✓ Bold enabled\n");
break;
case GHOSTTY_SGR_ATTR_FG_8:
printf("✓ Foreground color: %d (red)\n", attr.value.fg_8);
break;
default:
break;
}
}
ghostty_sgr_free(parser);
return 0;
}
```
**AI disclosure:** Amp wrote most of the C headers, but I verified it
all. https://ampcode.com/threads/T-d9f145cb-e6ef-48a8-ad63-e5fc85c0d43e
This commit is contained in:
committed by
GitHub
parent
27b0978cd5
commit
a82ad89ef3
@@ -98,13 +98,6 @@ comptime {
|
||||
// we want to reference the C API so that it gets exported.
|
||||
if (@import("root") == lib) {
|
||||
const c = terminal.c_api;
|
||||
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
|
||||
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
|
||||
@export(&c.osc_next, .{ .name = "ghostty_osc_next" });
|
||||
@export(&c.osc_reset, .{ .name = "ghostty_osc_reset" });
|
||||
@export(&c.osc_end, .{ .name = "ghostty_osc_end" });
|
||||
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
|
||||
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
|
||||
@export(&c.key_event_new, .{ .name = "ghostty_key_event_new" });
|
||||
@export(&c.key_event_free, .{ .name = "ghostty_key_event_free" });
|
||||
@export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" });
|
||||
@@ -125,7 +118,19 @@ comptime {
|
||||
@export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" });
|
||||
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
|
||||
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
|
||||
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
|
||||
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
|
||||
@export(&c.osc_next, .{ .name = "ghostty_osc_next" });
|
||||
@export(&c.osc_reset, .{ .name = "ghostty_osc_reset" });
|
||||
@export(&c.osc_end, .{ .name = "ghostty_osc_end" });
|
||||
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
|
||||
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
|
||||
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
|
||||
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });
|
||||
@export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" });
|
||||
@export(&c.sgr_set_params, .{ .name = "ghostty_sgr_set_params" });
|
||||
@export(&c.sgr_next, .{ .name = "ghostty_sgr_next" });
|
||||
|
||||
// On Wasm we need to export our allocator convenience functions.
|
||||
if (builtin.target.cpu.arch.isWasm()) {
|
||||
|
||||
@@ -2,6 +2,7 @@ pub const osc = @import("osc.zig");
|
||||
pub const key_event = @import("key_event.zig");
|
||||
pub const key_encode = @import("key_encode.zig");
|
||||
pub const paste = @import("paste.zig");
|
||||
pub const sgr = @import("sgr.zig");
|
||||
|
||||
// The full C API, unexported.
|
||||
pub const osc_new = osc.new;
|
||||
@@ -12,6 +13,12 @@ pub const osc_end = osc.end;
|
||||
pub const osc_command_type = osc.commandType;
|
||||
pub const osc_command_data = osc.commandData;
|
||||
|
||||
pub const sgr_new = sgr.new;
|
||||
pub const sgr_free = sgr.free;
|
||||
pub const sgr_reset = sgr.reset;
|
||||
pub const sgr_set_params = sgr.setParams;
|
||||
pub const sgr_next = sgr.next;
|
||||
|
||||
pub const key_event_new = key_event.new;
|
||||
pub const key_event_free = key_event.free;
|
||||
pub const key_event_set_action = key_event.set_action;
|
||||
@@ -41,6 +48,7 @@ test {
|
||||
_ = key_event;
|
||||
_ = key_encode;
|
||||
_ = paste;
|
||||
_ = sgr;
|
||||
|
||||
// We want to make sure we run the tests for the C allocator interface.
|
||||
_ = @import("../../lib/allocator.zig");
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
pub const Result = enum(c_int) {
|
||||
success = 0,
|
||||
out_of_memory = -1,
|
||||
invalid_value = -2,
|
||||
};
|
||||
|
||||
142
src/terminal/c/sgr.zig
Normal file
142
src/terminal/c/sgr.zig
Normal file
@@ -0,0 +1,142 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const sgr = @import("../sgr.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const log = std.log.scoped(.sgr);
|
||||
|
||||
/// Wrapper around parser that tracks the allocator for C API usage.
|
||||
const ParserWrapper = struct {
|
||||
parser: sgr.Parser,
|
||||
alloc: Allocator,
|
||||
};
|
||||
|
||||
/// C: GhosttySgrParser
|
||||
pub const Parser = ?*ParserWrapper;
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *Parser,
|
||||
) callconv(.c) Result {
|
||||
const alloc = lib_alloc.default(alloc_);
|
||||
const ptr = alloc.create(ParserWrapper) catch
|
||||
return .out_of_memory;
|
||||
ptr.* = .{
|
||||
.parser = .empty,
|
||||
.alloc = alloc,
|
||||
};
|
||||
result.* = ptr;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn free(parser_: Parser) callconv(.c) void {
|
||||
const wrapper = parser_ orelse return;
|
||||
const alloc = wrapper.alloc;
|
||||
const parser: *sgr.Parser = &wrapper.parser;
|
||||
if (parser.params.len > 0) alloc.free(parser.params);
|
||||
alloc.destroy(wrapper);
|
||||
}
|
||||
|
||||
pub fn reset(parser_: Parser) callconv(.c) void {
|
||||
const wrapper = parser_ orelse return;
|
||||
const parser: *sgr.Parser = &wrapper.parser;
|
||||
parser.idx = 0;
|
||||
}
|
||||
|
||||
pub fn setParams(
|
||||
parser_: Parser,
|
||||
params: [*]const u16,
|
||||
seps_: ?[*]const u8,
|
||||
len: usize,
|
||||
) callconv(.c) Result {
|
||||
const wrapper = parser_ orelse return .invalid_value;
|
||||
const alloc = wrapper.alloc;
|
||||
const parser: *sgr.Parser = &wrapper.parser;
|
||||
|
||||
// Copy our new parameters
|
||||
const params_slice = alloc.dupe(u16, params[0..len]) catch
|
||||
return .out_of_memory;
|
||||
if (parser.params.len > 0) alloc.free(parser.params);
|
||||
parser.params = params_slice;
|
||||
|
||||
// If we have separators, set that state too.
|
||||
parser.params_sep = .initEmpty();
|
||||
if (seps_) |seps| {
|
||||
if (len > @TypeOf(parser.params_sep).bit_length) {
|
||||
log.warn("ghostty_sgr_set_params: separators length {} exceeds max supported length {}", .{
|
||||
len,
|
||||
@TypeOf(parser.params_sep).bit_length,
|
||||
});
|
||||
return .invalid_value;
|
||||
}
|
||||
|
||||
for (seps[0..len], 0..) |sep, i| {
|
||||
if (sep == ':') parser.params_sep.set(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our parsing state
|
||||
parser.idx = 0;
|
||||
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
parser_: Parser,
|
||||
result: *sgr.Attribute.C,
|
||||
) callconv(.c) bool {
|
||||
const wrapper = parser_ orelse return false;
|
||||
const parser: *sgr.Parser = &wrapper.parser;
|
||||
if (parser.next()) |attr| {
|
||||
result.* = attr.cval();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
test "alloc" {
|
||||
var p: Parser = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&p,
|
||||
));
|
||||
free(p);
|
||||
}
|
||||
|
||||
test "simple params, no seps" {
|
||||
var p: Parser = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&p,
|
||||
));
|
||||
defer free(p);
|
||||
|
||||
try testing.expectEqual(Result.success, setParams(
|
||||
p,
|
||||
&.{1},
|
||||
null,
|
||||
1,
|
||||
));
|
||||
|
||||
// Set it twice on purpose to make sure we don't leak.
|
||||
try testing.expectEqual(Result.success, setParams(
|
||||
p,
|
||||
&.{1},
|
||||
null,
|
||||
1,
|
||||
));
|
||||
|
||||
// Verify we get bold
|
||||
var attr: sgr.Attribute.C = undefined;
|
||||
try testing.expect(next(p, &attr));
|
||||
try testing.expectEqual(.bold, attr.tag);
|
||||
|
||||
// Nothing else
|
||||
try testing.expect(!next(p, &attr));
|
||||
}
|
||||
@@ -5,7 +5,6 @@ const stream = @import("stream.zig");
|
||||
const ansi = @import("ansi.zig");
|
||||
const csi = @import("csi.zig");
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
const stream_readonly = @import("stream_readonly.zig");
|
||||
const style = @import("style.zig");
|
||||
pub const apc = @import("apc.zig");
|
||||
@@ -19,6 +18,7 @@ pub const modes = @import("modes.zig");
|
||||
pub const page = @import("page.zig");
|
||||
pub const parse_table = @import("parse_table.zig");
|
||||
pub const search = @import("search.zig");
|
||||
pub const sgr = @import("sgr.zig");
|
||||
pub const size = @import("size.zig");
|
||||
pub const tmux = if (options.tmux_control_mode) @import("tmux.zig") else struct {};
|
||||
pub const x11_color = @import("x11_color.zig");
|
||||
|
||||
@@ -151,7 +151,7 @@ pub const Attribute = union(Tag) {
|
||||
dotted = 4,
|
||||
dashed = 5,
|
||||
|
||||
pub const C = u8;
|
||||
pub const C = c_int;
|
||||
|
||||
pub fn cval(self: Underline) Underline.C {
|
||||
return @intFromEnum(self);
|
||||
@@ -176,10 +176,13 @@ pub const Attribute = union(Tag) {
|
||||
|
||||
/// Parser parses the attributes from a list of SGR parameters.
|
||||
pub const Parser = struct {
|
||||
params: []const u16,
|
||||
params: []const u16 = &.{},
|
||||
params_sep: SepList = .initEmpty(),
|
||||
idx: usize = 0,
|
||||
|
||||
/// Empty state parser.
|
||||
pub const empty: Parser = .{};
|
||||
|
||||
/// Next returns the next attribute or null if there are no more attributes.
|
||||
pub fn next(self: *Parser) ?Attribute {
|
||||
if (self.idx >= self.params.len) {
|
||||
|
||||
Reference in New Issue
Block a user