diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b40acaa74..f506a62b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,7 +94,8 @@ jobs: strategy: fail-fast: false matrix: - dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt, zig-vt-stream] + dir: + [c-vt, c-vt-key-encode, c-vt-paste, c-vt-sgr, zig-vt, zig-vt-stream] name: Example ${{ matrix.dir }} runs-on: namespace-profile-ghostty-sm needs: test diff --git a/example/c-vt-sgr/README.md b/example/c-vt-sgr/README.md new file mode 100644 index 000000000..c89e1aec9 --- /dev/null +++ b/example/c-vt-sgr/README.md @@ -0,0 +1,21 @@ +# Example: `ghostty-vt` SGR Parser + +This contains a simple example of how to use the `ghostty-vt` SGR parser +to parse terminal styling sequences and extract text attributes. + +This example demonstrates parsing a complex SGR sequence from Kakoune that +includes curly underline, RGB foreground/background colors, and RGB underline +color with mixed semicolon and colon separators. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-sgr/build.zig b/example/c-vt-sgr/build.zig new file mode 100644 index 000000000..ea6ea6e1e --- /dev/null +++ b/example/c-vt-sgr/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_sgr", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-sgr/build.zig.zon b/example/c-vt-sgr/build.zig.zon new file mode 100644 index 000000000..0d33b0897 --- /dev/null +++ b/example/c-vt-sgr/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_sgr, + .version = "0.0.0", + .fingerprint = 0x6e9c6d318e59c268, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-sgr/src/main.c b/example/c-vt-sgr/src/main.c new file mode 100644 index 000000000..21a529726 --- /dev/null +++ b/example/c-vt-sgr/src/main.c @@ -0,0 +1,131 @@ +#include +#include +#include + +int main() { + // Create parser + GhosttySgrParser parser; + GhosttyResult result = ghostty_sgr_new(NULL, &parser); + assert(result == GHOSTTY_SUCCESS); + + // Parse a complex SGR sequence from Kakoune + // This corresponds to the escape sequence: + // ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m + // + // Breaking down the sequence: + // - 4:3 = curly underline (colon-separated sub-parameters) + // - 38;2;51;51;51 = foreground RGB color (51, 51, 51) - dark gray + // - 48;2;170;170;170 = background RGB color (170, 170, 170) - light gray + // - 58;2;255;97;136 = underline RGB color (255, 97, 136) - pink + uint16_t params[] = {4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136}; + + // Separator array: ':' at position 0 (between 4 and 3), ';' elsewhere + char separators[] = ";;;;;;;;;;;;;;;;"; + separators[0] = ':'; + + result = ghostty_sgr_set_params(parser, params, separators, sizeof(params) / sizeof(params[0])); + assert(result == GHOSTTY_SUCCESS); + + printf("Parsing Kakoune SGR sequence:\n"); + printf("ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m\n\n"); + + // Iterate through attributes + GhosttySgrAttribute attr; + int count = 0; + while (ghostty_sgr_next(parser, &attr)) { + count++; + printf("Attribute %d: ", count); + + switch (attr.tag) { + case GHOSTTY_SGR_ATTR_UNDERLINE: + printf("Underline style = "); + switch (attr.value.underline) { + case GHOSTTY_SGR_UNDERLINE_NONE: + printf("none\n"); + break; + case GHOSTTY_SGR_UNDERLINE_SINGLE: + printf("single\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DOUBLE: + printf("double\n"); + break; + case GHOSTTY_SGR_UNDERLINE_CURLY: + printf("curly\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DOTTED: + printf("dotted\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DASHED: + printf("dashed\n"); + break; + default: + printf("unknown (%d)\n", attr.value.underline); + break; + } + break; + + case GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG: + printf("Foreground RGB = (%d, %d, %d)\n", + attr.value.direct_color_fg.r, + attr.value.direct_color_fg.g, + attr.value.direct_color_fg.b); + break; + + case GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG: + printf("Background RGB = (%d, %d, %d)\n", + attr.value.direct_color_bg.r, + attr.value.direct_color_bg.g, + attr.value.direct_color_bg.b); + break; + + case GHOSTTY_SGR_ATTR_UNDERLINE_COLOR: + printf("Underline color RGB = (%d, %d, %d)\n", + attr.value.underline_color.r, + attr.value.underline_color.g, + attr.value.underline_color.b); + break; + + case GHOSTTY_SGR_ATTR_FG_8: + printf("Foreground 8-color = %d\n", attr.value.fg_8); + break; + + case GHOSTTY_SGR_ATTR_BG_8: + printf("Background 8-color = %d\n", attr.value.bg_8); + break; + + case GHOSTTY_SGR_ATTR_FG_256: + printf("Foreground 256-color = %d\n", attr.value.fg_256); + break; + + case GHOSTTY_SGR_ATTR_BG_256: + printf("Background 256-color = %d\n", attr.value.bg_256); + break; + + case GHOSTTY_SGR_ATTR_BOLD: + printf("Bold\n"); + break; + + case GHOSTTY_SGR_ATTR_ITALIC: + printf("Italic\n"); + break; + + case GHOSTTY_SGR_ATTR_UNSET: + printf("Reset all attributes\n"); + break; + + case GHOSTTY_SGR_ATTR_UNKNOWN: + printf("Unknown attribute\n"); + break; + + default: + printf("Other attribute (tag=%d)\n", attr.tag); + break; + } + } + + printf("\nTotal attributes parsed: %d\n", count); + + // Cleanup + ghostty_sgr_free(parser); + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index e6d922009..4f8fef88e 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -30,6 +30,7 @@ * The API is organized into the following groups: * - @ref key "Key Encoding" - Encode key events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences * - @ref paste "Paste Utilities" - Validate paste data safety * - @ref allocator "Memory Management" - Memory management and custom allocators * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions @@ -40,6 +41,7 @@ * - @ref c-vt/src/main.c - OSC parser example * - @ref c-vt-key-encode/src/main.c - Key encoding example * - @ref c-vt-paste/src/main.c - Paste safety check example + * - @ref c-vt-sgr/src/main.c - SGR parser example * */ @@ -58,6 +60,11 @@ * paste data is safe before sending it to the terminal. */ +/** @example c-vt-sgr/src/main.c + * This example demonstrates how to use the SGR parser to parse terminal + * styling sequences and extract text attributes like colors and underline styles. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H @@ -68,6 +75,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/include/ghostty/vt/color.h b/include/ghostty/vt/color.h new file mode 100644 index 000000000..9e7fe6f4d --- /dev/null +++ b/include/ghostty/vt/color.h @@ -0,0 +1,77 @@ +/** + * @file color.h + * + * Color types and utilities. + */ + +#ifndef GHOSTTY_VT_COLOR_H +#define GHOSTTY_VT_COLOR_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * RGB color value. + * + * @ingroup sgr + */ +typedef struct { + uint8_t r; /**< Red component (0-255) */ + uint8_t g; /**< Green component (0-255) */ + uint8_t b; /**< Blue component (0-255) */ +} GhosttyColorRgb; + +/** + * Palette color index (0-255). + * + * @ingroup sgr + */ +typedef uint8_t GhosttyColorPaletteIndex; + +/** @addtogroup sgr + * @{ + */ + +/** Black color (0) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLACK 0 +/** Red color (1) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_RED 1 +/** Green color (2) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_GREEN 2 +/** Yellow color (3) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_YELLOW 3 +/** Blue color (4) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLUE 4 +/** Magenta color (5) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_MAGENTA 5 +/** Cyan color (6) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_CYAN 6 +/** White color (7) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_WHITE 7 +/** Bright black color (8) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8 +/** Bright red color (9) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9 +/** Bright green color (10) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10 +/** Bright yellow color (11) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11 +/** Bright blue color (12) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12 +/** Bright magenta color (13) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13 +/** Bright cyan color (14) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14 +/** Bright white color (15) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15 + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_COLOR_H */ diff --git a/include/ghostty/vt/result.h b/include/ghostty/vt/result.h index cc382eade..65938ee76 100644 --- a/include/ghostty/vt/result.h +++ b/include/ghostty/vt/result.h @@ -15,6 +15,8 @@ typedef enum { GHOSTTY_SUCCESS = 0, /** Operation failed due to failed allocation */ GHOSTTY_OUT_OF_MEMORY = -1, + /** Operation failed due to invalid value */ + GHOSTTY_INVALID_VALUE = -2, } GhosttyResult; #endif /* GHOSTTY_VT_RESULT_H */ diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h new file mode 100644 index 000000000..a296a280a --- /dev/null +++ b/include/ghostty/vt/sgr.h @@ -0,0 +1,306 @@ +/** + * @file sgr.h + * + * SGR (Select Graphic Rendition) attribute parsing and handling. + */ + +#ifndef GHOSTTY_VT_SGR_H +#define GHOSTTY_VT_SGR_H + +/** @defgroup sgr SGR Parser + * + * SGR (Select Graphic Rendition) attribute parser. + * + * SGR sequences are the syntax used to set styling attributes such as + * bold, italic, underline, and colors for text in terminal emulators. + * For example, you may be familiar with sequences like `ESC[1;31m`. The + * `1;31` is the SGR attribute list. + * + * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`) + * and returns individual text attributes like bold, italic, colors, etc. + * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed, + * and handles various color formats including 8-color, 16-color, 256-color, + * X11 named colors, and RGB in multiple formats. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_sgr_new() + * 2. Set SGR parameters with ghostty_sgr_set_params() + * 3. Iterate through attributes using ghostty_sgr_next() + * 4. Free the parser with ghostty_sgr_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create parser + * GhosttySgrParser parser; + * GhosttyResult result = ghostty_sgr_new(NULL, &parser); + * assert(result == GHOSTTY_SUCCESS); + * + * // Parse "bold, red foreground" sequence: ESC[1;31m + * uint16_t params[] = {1, 31}; + * result = ghostty_sgr_set_params(parser, params, NULL, 2); + * assert(result == GHOSTTY_SUCCESS); + * + * // 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\n", attr.value.fg_8); + * break; + * default: + * break; + * } + * } + * + * // Cleanup + * ghostty_sgr_free(parser); + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Opaque handle to an SGR parser instance. + * + * This handle represents an SGR (Select Graphic Rendition) parser that can + * be used to parse SGR sequences and extract individual text attributes. + * + * @ingroup sgr + */ +typedef struct GhosttySgrParser *GhosttySgrParser; + +/** + * SGR attribute tags. + * + * These values identify the type of an SGR attribute in a tagged union. + * Use the tag to determine which field in the attribute value union to access. + * + * @ingroup sgr + */ +typedef enum { + GHOSTTY_SGR_ATTR_UNSET = 0, + GHOSTTY_SGR_ATTR_UNKNOWN = 1, + GHOSTTY_SGR_ATTR_BOLD = 2, + GHOSTTY_SGR_ATTR_RESET_BOLD = 3, + GHOSTTY_SGR_ATTR_ITALIC = 4, + GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, + GHOSTTY_SGR_ATTR_FAINT = 6, + GHOSTTY_SGR_ATTR_UNDERLINE = 7, + GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10, + GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11, + GHOSTTY_SGR_ATTR_OVERLINE = 12, + GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13, + GHOSTTY_SGR_ATTR_BLINK = 14, + GHOSTTY_SGR_ATTR_RESET_BLINK = 15, + GHOSTTY_SGR_ATTR_INVERSE = 16, + GHOSTTY_SGR_ATTR_RESET_INVERSE = 17, + GHOSTTY_SGR_ATTR_INVISIBLE = 18, + GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19, + GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20, + GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23, + GHOSTTY_SGR_ATTR_BG_8 = 24, + GHOSTTY_SGR_ATTR_FG_8 = 25, + GHOSTTY_SGR_ATTR_RESET_FG = 26, + GHOSTTY_SGR_ATTR_RESET_BG = 27, + GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28, + GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29, + GHOSTTY_SGR_ATTR_BG_256 = 30, + GHOSTTY_SGR_ATTR_FG_256 = 31, +} GhosttySgrAttributeTag; + +/** + * Underline style types. + * + * @ingroup sgr + */ +typedef enum { + GHOSTTY_SGR_UNDERLINE_NONE = 0, + GHOSTTY_SGR_UNDERLINE_SINGLE = 1, + GHOSTTY_SGR_UNDERLINE_DOUBLE = 2, + GHOSTTY_SGR_UNDERLINE_CURLY = 3, + GHOSTTY_SGR_UNDERLINE_DOTTED = 4, + GHOSTTY_SGR_UNDERLINE_DASHED = 5, +} GhosttySgrUnderline; + +/** + * Unknown SGR attribute data. + * + * Contains the full parameter list and the partial list where parsing + * encountered an unknown or invalid sequence. + * + * @ingroup sgr + */ +typedef struct { + const uint16_t *full_ptr; + size_t full_len; + const uint16_t *partial_ptr; + size_t partial_len; +} GhosttySgrUnknown; + +/** + * SGR attribute value union. + * + * This union contains all possible attribute values. Use the tag field + * to determine which union member is active. Attributes without associated + * data (like bold, italic) don't use the union value. + * + * @ingroup sgr + */ +typedef union { + GhosttySgrUnknown unknown; + GhosttySgrUnderline underline; + GhosttyColorRgb underline_color; + GhosttyColorPaletteIndex underline_color_256; + GhosttyColorRgb direct_color_fg; + GhosttyColorRgb direct_color_bg; + GhosttyColorPaletteIndex bg_8; + GhosttyColorPaletteIndex fg_8; + GhosttyColorPaletteIndex bright_bg_8; + GhosttyColorPaletteIndex bright_fg_8; + GhosttyColorPaletteIndex bg_256; + GhosttyColorPaletteIndex fg_256; + uint64_t _padding[8]; +} GhosttySgrAttributeValue; + +/** + * SGR attribute (tagged union). + * + * A complete SGR attribute with both its type tag and associated value. + * Always check the tag field to determine which value union member is valid. + * + * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be + * identified by tag alone; the value union is not used for these and + * the memory in the value field is undefined. + * + * @ingroup sgr + */ +typedef struct { + GhosttySgrAttributeTag tag; + GhosttySgrAttributeValue value; +} GhosttySgrAttribute; + +/** + * Create a new SGR parser instance. + * + * Creates a new SGR (Select Graphic Rendition) parser using the provided + * allocator. The parser must be freed using ghostty_sgr_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GhosttyResult ghostty_sgr_new(const GhosttyAllocator *allocator, GhosttySgrParser *parser); + +/** + * Free an SGR parser instance. + * + * Releases all resources associated with the SGR parser. After this call, + * the parser handle becomes invalid and must not be used. This includes + * any attributes previously returned by ghostty_sgr_next(). + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup sgr + */ +void ghostty_sgr_free(GhosttySgrParser parser); + +/** + * Reset an SGR parser instance to the beginning of the parameter list. + * + * Resets the parser's iteration state without clearing the parameters. + * After calling this, ghostty_sgr_next() will start from the beginning + * of the parameter list again. + * + * @param parser The parser handle to reset, must not be NULL + * + * @ingroup sgr + */ +void ghostty_sgr_reset(GhosttySgrParser parser); + +/** + * Set SGR parameters for parsing. + * + * Sets the SGR parameter list to parse. Parameters are the numeric values + * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}). + * + * The separators array optionally specifies the separator type for each + * parameter position. Each byte should be either ';' for semicolon or ':' + * for colon. This is needed for certain color formats that use colon + * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator + * values are treated as semicolons. The separators array must have the same + * length as the params array, if it is not NULL. + * + * If separators is NULL, all parameters are assumed to be semicolon-separated. + * + * This function makes an internal copy of the parameter and separator data, + * so the caller can safely free or modify the input arrays after this call. + * + * After calling this function, the parser is automatically reset and ready + * to iterate from the beginning. + * + * @param parser The parser handle, must not be NULL + * @param params Array of SGR parameter values + * @param separators Optional array of separator characters (';' or ':'), or NULL + * @param len Number of parameters (and separators if provided) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GhosttyResult ghostty_sgr_set_params( + GhosttySgrParser parser, + const uint16_t *params, + const char *separators, + size_t len); + +/** + * Get the next SGR attribute. + * + * Parses and returns the next attribute from the parameter list. + * Call this function repeatedly until it returns false to process + * all attributes in the sequence. + * + * @param parser The parser handle, must not be NULL + * @param attr Pointer to store the next attribute + * @return true if an attribute was returned, false if no more attributes + * + * @ingroup sgr + */ +bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute *attr); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SGR_H */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index c66e5ab39..e1aa69659 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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()) { diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index f68333d9b..b935b264d 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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"); diff --git a/src/terminal/c/result.zig b/src/terminal/c/result.zig index a2ebc9b69..e9b5fc5e6 100644 --- a/src/terminal/c/result.zig +++ b/src/terminal/c/result.zig @@ -2,4 +2,5 @@ pub const Result = enum(c_int) { success = 0, out_of_memory = -1, + invalid_value = -2, }; diff --git a/src/terminal/c/sgr.zig b/src/terminal/c/sgr.zig new file mode 100644 index 000000000..f01a8a25b --- /dev/null +++ b/src/terminal/c/sgr.zig @@ -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)); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 0d6a053c8..c2d6e8cb4 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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"); diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index a345a7a90..b9765ca6a 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.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) {