vt: add style C API

Expose the terminal Style struct to the C API as GhosttyStyle, a
sized struct with foreground, background, and underline colors
(as tagged unions) plus boolean text decoration flags.

Add ghostty_style_default() to obtain the default style and
ghostty_style_is_default() to check whether a style has all
default values. Wire both through c/style.zig, main.zig, and
lib_vt.zig with the corresponding header in vt/style.h.
This commit is contained in:
Mitchell Hashimoto
2026-03-19 11:52:34 -07:00
parent f168b3c098
commit 7f36e8bd43
5 changed files with 269 additions and 0 deletions

View File

@@ -169,6 +169,8 @@ comptime {
@export(&c.mode_report_encode, .{ .name = "ghostty_mode_report_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
@export(&c.size_report_encode, .{ .name = "ghostty_size_report_encode" });
@export(&c.style_default, .{ .name = "ghostty_style_default" });
@export(&c.style_is_default, .{ .name = "ghostty_style_is_default" });
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });

View File

@@ -10,6 +10,7 @@ pub const mouse_encode = @import("mouse_encode.zig");
pub const paste = @import("paste.zig");
pub const sgr = @import("sgr.zig");
pub const size_report = @import("size_report.zig");
pub const style = @import("style.zig");
pub const terminal = @import("terminal.zig");
// The full C API, unexported.
@@ -90,6 +91,9 @@ pub const paste_is_safe = paste.is_safe;
pub const size_report_encode = size_report.encode;
pub const style_default = style.default_style;
pub const style_is_default = style.style_is_default;
pub const terminal_new = terminal.new;
pub const terminal_free = terminal.free;
pub const terminal_reset = terminal.reset;
@@ -113,6 +117,7 @@ test {
_ = paste;
_ = sgr;
_ = size_report;
_ = style;
_ = terminal;
// We want to make sure we run the tests for the C allocator interface.

133
src/terminal/c/style.zig Normal file
View File

@@ -0,0 +1,133 @@
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const style = @import("../style.zig");
const color = @import("../color.zig");
const sgr = @import("../sgr.zig");
/// C: GhosttyStyleColorTag
pub const ColorTag = enum(c_int) {
none = 0,
palette = 1,
rgb = 2,
};
/// C: GhosttyStyleColorValue
pub const ColorValue = extern union {
palette: u8,
rgb: color.RGB.C,
_padding: u32,
};
/// C: GhosttyStyleColor
pub const Color = extern struct {
tag: ColorTag,
value: ColorValue,
};
/// C: GhosttyStyle
pub const Style = extern struct {
size: usize = @sizeOf(Style),
fg_color: Color,
bg_color: Color,
underline_color: Color,
bold: bool,
italic: bool,
faint: bool,
blink: bool,
inverse: bool,
invisible: bool,
strikethrough: bool,
overline: bool,
underline: c_int,
};
fn convertColor(c: style.Style.Color) Color {
return switch (c) {
.none => .{
.tag = .none,
.value = .{ ._padding = 0 },
},
.palette => |idx| .{
.tag = .palette,
.value = .{ .palette = idx },
},
.rgb => |rgb| .{
.tag = .rgb,
.value = .{ .rgb = rgb.cval() },
},
};
}
pub fn convertStyle(s: style.Style) Style {
return .{
.fg_color = convertColor(s.fg_color),
.bg_color = convertColor(s.bg_color),
.underline_color = convertColor(s.underline_color),
.bold = s.flags.bold,
.italic = s.flags.italic,
.faint = s.flags.faint,
.blink = s.flags.blink,
.inverse = s.flags.inverse,
.invisible = s.flags.invisible,
.strikethrough = s.flags.strikethrough,
.overline = s.flags.overline,
.underline = @intFromEnum(s.flags.underline),
};
}
/// Returns the default style.
pub fn default_style(result: *Style) callconv(.c) void {
result.* = convertStyle(.{});
assert(result.size == @sizeOf(Style));
}
/// Returns true if the style is the default style.
pub fn style_is_default(s: *const Style) callconv(.c) bool {
assert(s.size == @sizeOf(Style));
return s.fg_color.tag == .none and
s.bg_color.tag == .none and
s.underline_color.tag == .none and
s.bold == false and
s.italic == false and
s.faint == false and
s.blink == false and
s.inverse == false and
s.invisible == false and
s.strikethrough == false and
s.overline == false and
s.underline == 0;
}
test "default style" {
var s: Style = undefined;
default_style(&s);
try testing.expect(style_is_default(&s));
try testing.expectEqual(ColorTag.none, s.fg_color.tag);
try testing.expectEqual(ColorTag.none, s.bg_color.tag);
try testing.expectEqual(ColorTag.none, s.underline_color.tag);
try testing.expect(!s.bold);
try testing.expect(!s.italic);
try testing.expectEqual(@as(c_int, 0), s.underline);
}
test "convert style with colors" {
const zig_style: style.Style = .{
.fg_color = .{ .palette = 42 },
.bg_color = .{ .rgb = .{ .r = 255, .g = 128, .b = 64 } },
.underline_color = .none,
.flags = .{ .bold = true, .underline = .curly },
};
const c_style = convertStyle(zig_style);
try testing.expectEqual(ColorTag.palette, c_style.fg_color.tag);
try testing.expectEqual(@as(u8, 42), c_style.fg_color.value.palette);
try testing.expectEqual(ColorTag.rgb, c_style.bg_color.tag);
try testing.expectEqual(@as(u8, 255), c_style.bg_color.value.rgb.r);
try testing.expectEqual(@as(u8, 128), c_style.bg_color.value.rgb.g);
try testing.expectEqual(@as(u8, 64), c_style.bg_color.value.rgb.b);
try testing.expectEqual(ColorTag.none, c_style.underline_color.tag);
try testing.expect(c_style.bold);
try testing.expectEqual(@as(c_int, 3), c_style.underline);
try testing.expect(!style_is_default(&c_style));
}