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

@@ -98,6 +98,7 @@ extern "C" {
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/style.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/mouse.h>

128
include/ghostty/vt/style.h Normal file
View File

@@ -0,0 +1,128 @@
/**
* @file style.h
*
* Terminal cell style types.
*/
#ifndef GHOSTTY_VT_STYLE_H
#define GHOSTTY_VT_STYLE_H
#include <ghostty/vt/color.h>
#include <ghostty/vt/types.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup style Style
*
* Terminal cell style attributes.
*
* A style describes the visual attributes of a terminal cell, including
* foreground, background, and underline colors, as well as flags for
* bold, italic, underline, and other text decorations.
*
* @{
*/
/**
* Style color tags.
*
* These values identify the type of color in a style color.
* Use the tag to determine which field in the color value union to access.
*
* @ingroup style
*/
typedef enum {
GHOSTTY_STYLE_COLOR_NONE = 0,
GHOSTTY_STYLE_COLOR_PALETTE = 1,
GHOSTTY_STYLE_COLOR_RGB = 2,
} GhosttyStyleColorTag;
/**
* Style color value union.
*
* Use the tag to determine which field is active.
*
* @ingroup style
*/
typedef union {
GhosttyColorPaletteIndex palette;
GhosttyColorRgb rgb;
uint32_t _padding;
} GhosttyStyleColorValue;
/**
* Style color (tagged union).
*
* A color used in a style attribute. Can be unset (none), a palette
* index, or a direct RGB value.
*
* @ingroup style
*/
typedef struct {
GhosttyStyleColorTag tag;
GhosttyStyleColorValue value;
} GhosttyStyleColor;
/**
* Terminal cell style.
*
* Describes the complete visual style for a terminal cell, including
* foreground, background, and underline colors, as well as text
* decoration flags. The underline field uses the same values as
* GhosttySgrUnderline.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup style
*/
typedef struct {
size_t size;
GhosttyStyleColor fg_color;
GhosttyStyleColor bg_color;
GhosttyStyleColor underline_color;
bool bold;
bool italic;
bool faint;
bool blink;
bool inverse;
bool invisible;
bool strikethrough;
bool overline;
int underline; /**< One of GHOSTTY_SGR_UNDERLINE_* values */
} GhosttyStyle;
/**
* Get the default style.
*
* Initializes the style to the default values (no colors, no flags).
*
* @param style Pointer to the style to initialize
*
* @ingroup style
*/
void ghostty_style_default(GhosttyStyle* style);
/**
* Check if a style is the default style.
*
* Returns true if all colors are unset and all flags are off.
*
* @param style Pointer to the style to check
* @return true if the style is the default style
*
* @ingroup style
*/
bool ghostty_style_is_default(const GhosttyStyle* style);
#ifdef __cplusplus
}
#endif
/** @} */
#endif /* GHOSTTY_VT_STYLE_H */

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));
}