mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-10 11:19:45 +00:00
lib-vt: many more color utility APIs (#13206)
Embedders that render theme editors, palette pickers, or custom settings
UI need to use the same color semantics as Ghostty.
This moves the shared parsing paths into terminal/color and exposes them
through libghostty-vt. Config color and palette parsing now delegate to
the same helpers, so CLI/config behavior and the C ABI stay in lockstep.
From C:
GhosttyColorRgb rgb;
ghostty_color_parse("ForestGreen", 11, &rgb);
uint8_t index;
ghostty_color_parse_palette_entry(
"0x10=#282c34", 12, &index, &rgb);
const GhosttyColorX11Entry* names =
ghostty_color_x11_names();
The exported color API is:
ghostty_color_parse
ghostty_color_parse_x11
ghostty_color_parse_palette_entry
ghostty_color_palette_default
ghostty_color_palette_generate
ghostty_color_luminance
ghostty_color_perceived_luminance
ghostty_color_contrast
ghostty_color_x11_names
ghostty_color_x11_name_count
The X11 name table is parsed once at comptime into null-terminated
entries in rgb.txt order. The existing case-insensitive map keeps the
same behavior for RGB.parse and +list-colors, while bindings can walk a
static table without allocations.
This doesn't add any more binary size since all of this was already used
by terminal internals.
This commit is contained in:
@@ -7,6 +7,114 @@
|
||||
#ifndef GHOSTTY_VT_COLOR_H
|
||||
#define GHOSTTY_VT_COLOR_H
|
||||
|
||||
/** @defgroup color Color Utilities
|
||||
*
|
||||
* Color parsing, palette generation, color math, and X11 color name
|
||||
* utilities shared by libghostty-vt.
|
||||
*
|
||||
* These APIs expose Ghostty's color semantics directly to embedders. Use
|
||||
* them when an application needs to parse the same color strings as Ghostty
|
||||
* config and theme files, generate the same 256-color palette used by the
|
||||
* terminal, list supported X11 color names, or make UI decisions from
|
||||
* luminance and contrast values.
|
||||
*
|
||||
* ## Parsing Colors
|
||||
*
|
||||
* ghostty_color_parse() accepts the flexible syntax used by Ghostty for
|
||||
* terminal colors:
|
||||
*
|
||||
* - X11 color names, matched ASCII case-insensitively.
|
||||
* - 3- or 6-digit hex colors, with or without a leading `#`.
|
||||
* - 9- or 12-digit hex colors, with a leading `#`.
|
||||
* - XParseColor-style `rgb:<red>/<green>/<blue>` values.
|
||||
* - XParseColor-style `rgbi:<red>/<green>/<blue>` values.
|
||||
*
|
||||
* Leading and trailing spaces and tabs are ignored. Use
|
||||
* ghostty_color_parse_x11() when only X11 names should be accepted.
|
||||
*
|
||||
* @code{.c}
|
||||
* GhosttyColorRgb color;
|
||||
*
|
||||
* if (ghostty_color_parse(
|
||||
* "ForestGreen",
|
||||
* sizeof("ForestGreen") - 1,
|
||||
* &color) != GHOSTTY_SUCCESS) {
|
||||
* // Handle invalid color input.
|
||||
* }
|
||||
*
|
||||
* ghostty_color_parse("#abc", sizeof("#abc") - 1, &color);
|
||||
* ghostty_color_parse("rgb:12/34/56", sizeof("rgb:12/34/56") - 1, &color);
|
||||
* @endcode
|
||||
*
|
||||
* ## Palette Entries
|
||||
*
|
||||
* ghostty_color_parse_palette_entry() parses a single Ghostty palette
|
||||
* override in `INDEX=COLOR` form. The index may be decimal or use a `0x`,
|
||||
* `0o`, or `0b` prefix. The color side uses ghostty_color_parse().
|
||||
*
|
||||
* @code{.c}
|
||||
* GhosttyColorRgb palette[256];
|
||||
* ghostty_color_palette_default(palette);
|
||||
*
|
||||
* uint8_t index;
|
||||
* GhosttyColorRgb rgb;
|
||||
*
|
||||
* if (ghostty_color_parse_palette_entry(
|
||||
* "0x10=#282c34",
|
||||
* sizeof("0x10=#282c34") - 1,
|
||||
* &index,
|
||||
* &rgb) == GHOSTTY_SUCCESS) {
|
||||
* palette[index] = rgb;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* ## Palette Generation
|
||||
*
|
||||
* ghostty_color_palette_generate() derives the 216-color cube and grayscale
|
||||
* ramp from a base palette, background, and foreground. Set bits in
|
||||
* GhosttyColorPaletteMask preserve specific indices from the base palette.
|
||||
* The output may alias the base input.
|
||||
*
|
||||
* @code{.c}
|
||||
* GhosttyColorRgb palette[256];
|
||||
* ghostty_color_palette_default(palette);
|
||||
*
|
||||
* GhosttyColorPaletteMask skip = {0};
|
||||
* GHOSTTY_COLOR_PALETTE_MASK_SET(&skip, 16);
|
||||
*
|
||||
* GhosttyColorRgb background = {40, 44, 52};
|
||||
* GhosttyColorRgb foreground = {220, 223, 228};
|
||||
*
|
||||
* ghostty_color_palette_generate(
|
||||
* palette,
|
||||
* &skip,
|
||||
* background,
|
||||
* foreground,
|
||||
* true,
|
||||
* palette);
|
||||
* @endcode
|
||||
*
|
||||
* ## X11 Color Names
|
||||
*
|
||||
* The X11 name table is static program-lifetime memory. Entries are in
|
||||
* rgb.txt order and are terminated by an entry with `name == NULL`.
|
||||
* ghostty_color_x11_name_count() returns the number of non-terminator
|
||||
* entries.
|
||||
*
|
||||
* @code{.c}
|
||||
* const GhosttyColorX11Entry* names = ghostty_color_x11_names();
|
||||
* size_t count = ghostty_color_x11_name_count();
|
||||
*
|
||||
* for (size_t i = 0; i < count; i++) {
|
||||
* // names[i].name and names[i].color are valid here.
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
@@ -17,7 +125,7 @@ extern "C" {
|
||||
/**
|
||||
* RGB color value.
|
||||
*
|
||||
* @ingroup sgr
|
||||
* @ingroup color
|
||||
*/
|
||||
typedef struct {
|
||||
uint8_t r; /**< Red component (0-255) */
|
||||
@@ -28,49 +136,128 @@ typedef struct {
|
||||
/**
|
||||
* Palette color index (0-255).
|
||||
*
|
||||
* @ingroup sgr
|
||||
* @ingroup color
|
||||
*/
|
||||
typedef uint8_t GhosttyColorPaletteIndex;
|
||||
|
||||
/** @addtogroup sgr
|
||||
* @{
|
||||
/**
|
||||
* A 256-bit mask of palette indices.
|
||||
*
|
||||
* Index i is set iff `(bits[i >> 6] >> (i & 63)) & 1` is 1.
|
||||
* The mask is typically initialized to zero and then populated with
|
||||
* GHOSTTY_COLOR_PALETTE_MASK_SET().
|
||||
*
|
||||
* @code{.c}
|
||||
* GhosttyColorPaletteMask mask = {0};
|
||||
* GHOSTTY_COLOR_PALETTE_MASK_SET(&mask, 20);
|
||||
* if (GHOSTTY_COLOR_PALETTE_MASK_IS_SET(&mask, 20)) {
|
||||
* // Index 20 will be preserved.
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
typedef struct {
|
||||
uint64_t bits[4];
|
||||
} GhosttyColorPaletteMask;
|
||||
|
||||
/** Black color (0) @ingroup sgr */
|
||||
/**
|
||||
* An entry in Ghostty's X11 color name table.
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
typedef struct {
|
||||
/** Null-terminated color name. NULL marks the end of the table. */
|
||||
const char* name;
|
||||
/** The RGB value of the color. */
|
||||
GhosttyColorRgb color;
|
||||
} GhosttyColorX11Entry;
|
||||
|
||||
/**
|
||||
* Return the storage word for a palette mask index.
|
||||
*
|
||||
* @param index The palette index (0-255)
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
#define GHOSTTY_COLOR_PALETTE_MASK_WORD(index) ((index) >> 6)
|
||||
|
||||
/**
|
||||
* Return the storage bit for a palette mask index.
|
||||
*
|
||||
* @param index The palette index (0-255)
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
#define GHOSTTY_COLOR_PALETTE_MASK_BIT(index) (UINT64_C(1) << ((index) & 63))
|
||||
|
||||
/**
|
||||
* Set a palette mask index.
|
||||
*
|
||||
* @param mask Pointer to a GhosttyColorPaletteMask
|
||||
* @param index The palette index (0-255)
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
#define GHOSTTY_COLOR_PALETTE_MASK_SET(mask, index) \
|
||||
((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] |= GHOSTTY_COLOR_PALETTE_MASK_BIT(index))
|
||||
|
||||
/**
|
||||
* Clear a palette mask index.
|
||||
*
|
||||
* @param mask Pointer to a GhosttyColorPaletteMask
|
||||
* @param index The palette index (0-255)
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
#define GHOSTTY_COLOR_PALETTE_MASK_UNSET(mask, index) \
|
||||
((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] &= ~GHOSTTY_COLOR_PALETTE_MASK_BIT(index))
|
||||
|
||||
/**
|
||||
* Test whether a palette mask index is set.
|
||||
*
|
||||
* @param mask Pointer to a GhosttyColorPaletteMask
|
||||
* @param index The palette index (0-255)
|
||||
* @return true if the palette index is set, false otherwise
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
#define GHOSTTY_COLOR_PALETTE_MASK_IS_SET(mask, index) \
|
||||
(((mask)->bits[GHOSTTY_COLOR_PALETTE_MASK_WORD(index)] & GHOSTTY_COLOR_PALETTE_MASK_BIT(index)) != 0)
|
||||
|
||||
/** Black color (0) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BLACK 0
|
||||
/** Red color (1) @ingroup sgr */
|
||||
/** Red color (1) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_RED 1
|
||||
/** Green color (2) @ingroup sgr */
|
||||
/** Green color (2) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_GREEN 2
|
||||
/** Yellow color (3) @ingroup sgr */
|
||||
/** Yellow color (3) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_YELLOW 3
|
||||
/** Blue color (4) @ingroup sgr */
|
||||
/** Blue color (4) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BLUE 4
|
||||
/** Magenta color (5) @ingroup sgr */
|
||||
/** Magenta color (5) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_MAGENTA 5
|
||||
/** Cyan color (6) @ingroup sgr */
|
||||
/** Cyan color (6) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_CYAN 6
|
||||
/** White color (7) @ingroup sgr */
|
||||
/** White color (7) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_WHITE 7
|
||||
/** Bright black color (8) @ingroup sgr */
|
||||
/** Bright black color (8) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8
|
||||
/** Bright red color (9) @ingroup sgr */
|
||||
/** Bright red color (9) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9
|
||||
/** Bright green color (10) @ingroup sgr */
|
||||
/** Bright green color (10) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10
|
||||
/** Bright yellow color (11) @ingroup sgr */
|
||||
/** Bright yellow color (11) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11
|
||||
/** Bright blue color (12) @ingroup sgr */
|
||||
/** Bright blue color (12) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12
|
||||
/** Bright magenta color (13) @ingroup sgr */
|
||||
/** Bright magenta color (13) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13
|
||||
/** Bright cyan color (14) @ingroup sgr */
|
||||
/** Bright cyan color (14) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14
|
||||
/** Bright white color (15) @ingroup sgr */
|
||||
/** Bright white color (15) @ingroup color */
|
||||
#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15
|
||||
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* Get the RGB color components.
|
||||
*
|
||||
@@ -83,15 +270,209 @@ typedef uint8_t GhosttyColorPaletteIndex;
|
||||
* @param g Pointer to store the green component (0-255)
|
||||
* @param b Pointer to store the blue component (0-255)
|
||||
*
|
||||
* @ingroup sgr
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API void ghostty_color_rgb_get(GhosttyColorRgb color,
|
||||
uint8_t* r,
|
||||
uint8_t* g,
|
||||
uint8_t* b);
|
||||
|
||||
/**
|
||||
* Parse an X11 color name.
|
||||
*
|
||||
* The color name is resolved from Ghostty's embedded rgb.txt table.
|
||||
* Leading and trailing spaces and tabs are trimmed, and matching is
|
||||
* ASCII case-insensitive. Hex values are not accepted by this function.
|
||||
*
|
||||
* @param name The color name bytes (must not be NULL)
|
||||
* @param len The length of @p name in bytes
|
||||
* @param[out] out The parsed RGB color
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if no color
|
||||
* matches or @p name is NULL
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_color_parse_x11(
|
||||
const char* name,
|
||||
size_t len,
|
||||
GhosttyColorRgb* out);
|
||||
|
||||
/**
|
||||
* Parse a flexible Ghostty color value.
|
||||
*
|
||||
* Accepts Ghostty's terminal color syntax: X11 color names, hex colors
|
||||
* in 3-, 6-, 9-, or 12-digit form (the leading # is optional for 3- and
|
||||
* 6-digit values), and rgb:<red>/<green>/<blue> or
|
||||
* rgbi:<red>/<green>/<blue> specifications. Leading and trailing spaces
|
||||
* and tabs are trimmed.
|
||||
*
|
||||
* @param value The color value bytes (must not be NULL)
|
||||
* @param len The length of @p value in bytes
|
||||
* @param[out] out The parsed RGB color
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if parsing
|
||||
* fails or @p value is NULL
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_color_parse(
|
||||
const char* value,
|
||||
size_t len,
|
||||
GhosttyColorRgb* out);
|
||||
|
||||
/**
|
||||
* Parse a Ghostty palette entry.
|
||||
*
|
||||
* Accepts Ghostty palette config syntax: N=COLOR. N is a palette index
|
||||
* from 0 to 255 in decimal or in 0x, 0o, or 0b-prefixed form. Spaces and
|
||||
* tabs around N and COLOR are ignored. COLOR accepts the same syntax as
|
||||
* ghostty_color_parse().
|
||||
*
|
||||
* @param value The palette entry bytes (must not be NULL)
|
||||
* @param len The length of @p value in bytes
|
||||
* @param[out] out_index The parsed palette index
|
||||
* @param[out] out_rgb The parsed RGB color
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE on any
|
||||
* failure, including index overflow
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_color_parse_palette_entry(
|
||||
const char* value,
|
||||
size_t len,
|
||||
uint8_t* out_index,
|
||||
GhosttyColorRgb* out_rgb);
|
||||
|
||||
/**
|
||||
* Get Ghostty's built-in default 256-color palette.
|
||||
*
|
||||
* Writes exactly 256 entries: Ghostty's base16 defaults, the xterm
|
||||
* 6x6x6 color cube, and the grayscale ramp.
|
||||
*
|
||||
* @param[out] out The output palette, an array of exactly 256
|
||||
* GhosttyColorRgb values
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API void ghostty_color_palette_default(GhosttyColorRgb* out);
|
||||
|
||||
/**
|
||||
* Generate a 256-color palette from base colors.
|
||||
*
|
||||
* The base palette supplies indices 0-15, which are always preserved.
|
||||
* If @p base is NULL, Ghostty's default palette is used. If @p skip is
|
||||
* NULL, no extra indices are skipped. Set bits in @p skip preserve those
|
||||
* indices from @p base. The 216-color cube at indices 16-231 is generated
|
||||
* with trilinear CIELAB interpolation, and the grayscale ramp at indices
|
||||
* 232-255 is interpolated from the background to the foreground.
|
||||
*
|
||||
* For light themes, @p harmonious controls whether the generated palette
|
||||
* keeps the background-to-foreground orientation. When false, Ghostty
|
||||
* swaps the light background and dark foreground so the cube and ramp run
|
||||
* dark-to-light. The output palette may be the same pointer as @p base.
|
||||
*
|
||||
* @param base The base palette, an array of exactly 256 GhosttyColorRgb
|
||||
* values, or NULL to use Ghostty's default palette
|
||||
* @param skip The palette indices to preserve from @p base, or NULL for
|
||||
* an empty mask
|
||||
* @param bg The terminal background color
|
||||
* @param fg The terminal foreground color
|
||||
* @param harmonious Whether light themes keep background-to-foreground
|
||||
* orientation
|
||||
* @param[out] out The output palette, an array of exactly 256
|
||||
* GhosttyColorRgb values
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API void ghostty_color_palette_generate(
|
||||
const GhosttyColorRgb* base,
|
||||
const GhosttyColorPaletteMask* skip,
|
||||
GhosttyColorRgb bg,
|
||||
GhosttyColorRgb fg,
|
||||
bool harmonious,
|
||||
GhosttyColorRgb* out);
|
||||
|
||||
/**
|
||||
* Calculate W3C relative luminance for an RGB color.
|
||||
*
|
||||
* Returns a normalized value from 0.0 for black to 1.0 for white.
|
||||
* See https://www.w3.org/TR/WCAG20/#relativeluminancedef.
|
||||
*
|
||||
* @param color The RGB color
|
||||
* @return Relative luminance in the range 0.0 to 1.0
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API double ghostty_color_luminance(GhosttyColorRgb color);
|
||||
|
||||
/**
|
||||
* Calculate perceived luminance for an RGB color.
|
||||
*
|
||||
* Returns a normalized value from 0.0 for black to 1.0 for white.
|
||||
* Ghostty treats a background color as light when this exceeds 0.5.
|
||||
* This is not the metric used internally by
|
||||
* ghostty_color_palette_generate(), which uses CIELAB lightness.
|
||||
*
|
||||
* @param color The RGB color
|
||||
* @return Perceived luminance in the range 0.0 to 1.0
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API double ghostty_color_perceived_luminance(GhosttyColorRgb color);
|
||||
|
||||
/**
|
||||
* Calculate the WCAG contrast ratio between two RGB colors.
|
||||
*
|
||||
* The contrast ratio is symmetric and ranges from 1.0 for identical
|
||||
* colors to 21.0 for black and white.
|
||||
*
|
||||
* @param a The first RGB color
|
||||
* @param b The second RGB color
|
||||
* @return WCAG contrast ratio in the range 1.0 to 21.0
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API double ghostty_color_contrast(GhosttyColorRgb a, GhosttyColorRgb b);
|
||||
|
||||
/**
|
||||
* Get Ghostty's X11 color name table.
|
||||
*
|
||||
* The returned pointer references static memory valid for the program
|
||||
* lifetime and is never NULL. Entries are in rgb.txt order and are
|
||||
* terminated by an entry with name == NULL. Aliases are separate entries,
|
||||
* such as "medium spring green" and "MediumSpringGreen". Names are the
|
||||
* exact supported spellings from rgb.txt; ghostty_color_parse_x11() also
|
||||
* matches them case-insensitively.
|
||||
*
|
||||
* @code{.c}
|
||||
* for (const GhosttyColorX11Entry* e = ghostty_color_x11_names();
|
||||
* e->name != NULL;
|
||||
* e++) {
|
||||
* // e->name and e->color are valid here.
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @return Pointer to the first X11 color entry
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API const GhosttyColorX11Entry* ghostty_color_x11_names(void);
|
||||
|
||||
/**
|
||||
* Get the number of X11 color name entries.
|
||||
*
|
||||
* The returned count excludes the NULL terminator and is provided so
|
||||
* bindings can preallocate storage before reading ghostty_color_x11_names().
|
||||
*
|
||||
* @return Number of X11 color name entries
|
||||
*
|
||||
* @ingroup color
|
||||
*/
|
||||
GHOSTTY_API size_t ghostty_color_x11_name_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_COLOR_H */
|
||||
|
||||
@@ -19,8 +19,11 @@
|
||||
* 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.
|
||||
* and handles SGR color attributes including 8-color, 16-color, 256-color,
|
||||
* direct RGB, underline color, and reset forms. Color values are returned
|
||||
* using the shared @ref color types; applications that need to parse Ghostty
|
||||
* config/theme color strings, generate palettes, inspect X11 color names, or
|
||||
* calculate luminance and contrast should use the @ref color APIs directly.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
|
||||
@@ -51,7 +51,6 @@ const terminal = struct {
|
||||
const color = @import("../terminal/color.zig");
|
||||
const selection_codepoints = @import("../terminal/selection_codepoints.zig");
|
||||
const style = @import("../terminal/style.zig");
|
||||
const x11_color = @import("../terminal/x11_color.zig");
|
||||
};
|
||||
|
||||
const log = std.log.scoped(.config);
|
||||
@@ -5441,16 +5440,8 @@ pub const Color = struct {
|
||||
|
||||
pub fn parseCLI(input_: ?[]const u8) !Color {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
// Trim any whitespace before processing
|
||||
const trimmed = std.mem.trim(u8, input, " \t");
|
||||
|
||||
if (terminal.x11_color.map.get(trimmed)) |rgb| return .{
|
||||
.r = rgb.r,
|
||||
.g = rgb.g,
|
||||
.b = rgb.b,
|
||||
};
|
||||
|
||||
return fromHex(trimmed);
|
||||
const rgb: terminal.color.RGB = terminal.color.RGB.parse(input) catch return error.InvalidValue;
|
||||
return .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
@@ -5481,48 +5472,15 @@ pub const Color = struct {
|
||||
) catch error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
||||
/// is optional.
|
||||
pub fn fromHex(input: []const u8) !Color {
|
||||
// Trim the beginning '#' if it exists
|
||||
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
|
||||
if (trimmed.len != 6 and trimmed.len != 3) return error.InvalidValue;
|
||||
|
||||
// Expand short hex values to full hex values
|
||||
const rgb: []const u8 = if (trimmed.len == 3) &.{
|
||||
trimmed[0], trimmed[0],
|
||||
trimmed[1], trimmed[1],
|
||||
trimmed[2], trimmed[2],
|
||||
} else trimmed;
|
||||
|
||||
// Parse the colors two at a time.
|
||||
var result: Color = undefined;
|
||||
comptime var i: usize = 0;
|
||||
inline while (i < 6) : (i += 2) {
|
||||
const v: u8 =
|
||||
((try std.fmt.charToDigit(rgb[i], 16)) * 16) +
|
||||
try std.fmt.charToDigit(rgb[i + 1], 16);
|
||||
|
||||
@field(result, switch (i) {
|
||||
0 => "r",
|
||||
2 => "g",
|
||||
4 => "b",
|
||||
else => unreachable,
|
||||
}) = v;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
test "fromHex" {
|
||||
test "parseCLI hex" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000"));
|
||||
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
|
||||
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
|
||||
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF"));
|
||||
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFF"));
|
||||
try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.fromHex("#345"));
|
||||
try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.parseCLI("#000000"));
|
||||
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.parseCLI("#0A0B0C"));
|
||||
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.parseCLI("0A0B0C"));
|
||||
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.parseCLI("FFFFFF"));
|
||||
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.parseCLI("FFF"));
|
||||
try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.parseCLI("#345"));
|
||||
}
|
||||
|
||||
test "parseCLI from name" {
|
||||
@@ -5868,20 +5826,12 @@ pub const Palette = struct {
|
||||
input: ?[]const u8,
|
||||
) !void {
|
||||
const value = input orelse return error.ValueRequired;
|
||||
const eqlIdx = std.mem.indexOf(u8, value, "=") orelse
|
||||
return error.InvalidValue;
|
||||
|
||||
// Parse the key part (trim whitespace)
|
||||
const key = try std.fmt.parseInt(
|
||||
u8,
|
||||
std.mem.trim(u8, value[0..eqlIdx], " \t"),
|
||||
0,
|
||||
);
|
||||
|
||||
// Parse the color part (Color.parseCLI will handle whitespace)
|
||||
const rgb = try Color.parseCLI(value[eqlIdx + 1 ..]);
|
||||
self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
|
||||
self.mask.set(key);
|
||||
const entry = terminal.color.parsePaletteEntry(value) catch |err| switch (err) {
|
||||
error.Overflow => return error.Overflow,
|
||||
error.InvalidFormat => return error.InvalidValue,
|
||||
};
|
||||
self.value[entry.index] = entry.color;
|
||||
self.mask.set(entry.index);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
|
||||
@@ -208,6 +208,16 @@ comptime {
|
||||
@export(&c.row_get, .{ .name = "ghostty_row_get" });
|
||||
@export(&c.row_get_multi, .{ .name = "ghostty_row_get_multi" });
|
||||
@export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" });
|
||||
@export(&c.color_contrast, .{ .name = "ghostty_color_contrast" });
|
||||
@export(&c.color_luminance, .{ .name = "ghostty_color_luminance" });
|
||||
@export(&c.color_parse, .{ .name = "ghostty_color_parse" });
|
||||
@export(&c.color_parse_palette_entry, .{ .name = "ghostty_color_parse_palette_entry" });
|
||||
@export(&c.color_parse_x11, .{ .name = "ghostty_color_parse_x11" });
|
||||
@export(&c.color_palette_default, .{ .name = "ghostty_color_palette_default" });
|
||||
@export(&c.color_palette_generate, .{ .name = "ghostty_color_palette_generate" });
|
||||
@export(&c.color_perceived_luminance, .{ .name = "ghostty_color_perceived_luminance" });
|
||||
@export(&c.color_x11_name_count, .{ .name = "ghostty_color_x11_name_count" });
|
||||
@export(&c.color_x11_names, .{ .name = "ghostty_color_x11_names" });
|
||||
@export(&c.sgr_new, .{ .name = "ghostty_sgr_new" });
|
||||
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });
|
||||
@export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" });
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
const std = @import("std");
|
||||
const lib = @import("../lib.zig");
|
||||
const color = @import("../color.zig");
|
||||
const x11_color = @import("../x11_color.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
/// C: GhosttyColorPaletteMask
|
||||
pub const PaletteMask = extern struct {
|
||||
bits: [4]u64,
|
||||
|
||||
/// Convert to the Zig PaletteMask (std.StaticBitSet(256)).
|
||||
pub fn toZig(self: *const PaletteMask) color.PaletteMask {
|
||||
var result = color.PaletteMask.initEmpty();
|
||||
for (0..256) |i| {
|
||||
if (((self.bits[i >> 6] >> @as(u6, @intCast(i & 63))) & 1) != 0) {
|
||||
result.set(i);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyColorX11Entry
|
||||
pub const X11Entry = extern struct {
|
||||
/// Null-terminated color name; null marks the end of the table.
|
||||
name: ?[*:0]const u8,
|
||||
color: color.RGB.C,
|
||||
};
|
||||
|
||||
/// Comptime-built static table, terminated by a null-name entry.
|
||||
const x11_entries: [x11_color.entries.len + 1]X11Entry = entries: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
var result: [x11_color.entries.len + 1]X11Entry = undefined;
|
||||
for (x11_color.entries, 0..) |entry, i| result[i] = .{
|
||||
.name = entry.name.ptr,
|
||||
.color = entry.color.cval(),
|
||||
};
|
||||
result[x11_color.entries.len] = .{ .name = null, .color = .{ .r = 0, .g = 0, .b = 0 } };
|
||||
break :entries result;
|
||||
};
|
||||
|
||||
pub fn rgb_get(
|
||||
c: color.RGB.C,
|
||||
@@ -11,3 +49,524 @@ pub fn rgb_get(
|
||||
g.* = c.g;
|
||||
b.* = c.b;
|
||||
}
|
||||
|
||||
pub fn parse_x11(
|
||||
name_: ?[*]const u8,
|
||||
len: usize,
|
||||
out: *color.RGB.C,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const name = (name_ orelse return .invalid_value)[0..len];
|
||||
const trimmed = std.mem.trim(u8, name, " \t");
|
||||
const rgb = x11_color.map.get(trimmed) orelse return .invalid_value;
|
||||
out.* = rgb.cval();
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn parse(
|
||||
value_: ?[*]const u8,
|
||||
len: usize,
|
||||
out: *color.RGB.C,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const value = (value_ orelse return .invalid_value)[0..len];
|
||||
const rgb = color.RGB.parse(value) catch return .invalid_value;
|
||||
out.* = rgb.cval();
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn palette_default(out: *color.PaletteC) callconv(lib.calling_conv) void {
|
||||
out.* = color.paletteCval(&color.default);
|
||||
}
|
||||
|
||||
/// Generate a 256-color palette. The output may alias the base input.
|
||||
pub fn palette_generate(
|
||||
base_: ?*const color.PaletteC,
|
||||
skip_: ?*const PaletteMask,
|
||||
bg: color.RGB.C,
|
||||
fg: color.RGB.C,
|
||||
harmonious: bool,
|
||||
out: *color.PaletteC,
|
||||
) callconv(lib.calling_conv) void {
|
||||
const base: color.Palette = if (base_) |base|
|
||||
color.paletteZval(base)
|
||||
else
|
||||
color.default;
|
||||
const skip: color.PaletteMask = if (skip_) |skip|
|
||||
skip.toZig()
|
||||
else
|
||||
.initEmpty();
|
||||
const result = color.generate256Color(
|
||||
base,
|
||||
skip,
|
||||
.fromC(bg),
|
||||
.fromC(fg),
|
||||
harmonious,
|
||||
);
|
||||
out.* = color.paletteCval(&result);
|
||||
}
|
||||
|
||||
pub fn x11_names() callconv(lib.calling_conv) [*]const X11Entry {
|
||||
return x11_entries[0..].ptr;
|
||||
}
|
||||
|
||||
pub fn x11_name_count() callconv(lib.calling_conv) usize {
|
||||
return x11_color.entries.len;
|
||||
}
|
||||
|
||||
pub fn parse_palette_entry(
|
||||
value_: ?[*]const u8,
|
||||
len: usize,
|
||||
out_index: *u8,
|
||||
out_rgb: *color.RGB.C,
|
||||
) callconv(lib.calling_conv) Result {
|
||||
const value = (value_ orelse return .invalid_value)[0..len];
|
||||
const entry = color.parsePaletteEntry(value) catch return .invalid_value;
|
||||
out_index.* = entry.index;
|
||||
out_rgb.* = entry.color.cval();
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn luminance(c: color.RGB.C) callconv(lib.calling_conv) f64 {
|
||||
return color.RGB.fromC(c).luminance();
|
||||
}
|
||||
|
||||
pub fn perceived_luminance(c: color.RGB.C) callconv(lib.calling_conv) f64 {
|
||||
return color.RGB.fromC(c).perceivedLuminance();
|
||||
}
|
||||
|
||||
pub fn contrast(a: color.RGB.C, b: color.RGB.C) callconv(lib.calling_conv) f64 {
|
||||
return color.RGB.fromC(a).contrast(.fromC(b));
|
||||
}
|
||||
|
||||
fn expectRgb(expected: color.RGB, actual: color.RGB.C) !void {
|
||||
try std.testing.expectEqual(expected, color.RGB.fromC(actual));
|
||||
}
|
||||
|
||||
fn expectPaletteC(expected: *const color.PaletteC, actual: *const color.PaletteC) !void {
|
||||
for (expected.*, actual.*) |expected_rgb, actual_rgb| {
|
||||
try expectRgb(color.RGB.fromC(expected_rgb), actual_rgb);
|
||||
}
|
||||
}
|
||||
|
||||
fn generatePalette(
|
||||
base_: ?*const color.PaletteC,
|
||||
skip_: ?*const PaletteMask,
|
||||
bg: color.RGB,
|
||||
fg: color.RGB,
|
||||
harmonious: bool,
|
||||
) color.PaletteC {
|
||||
var out: color.PaletteC = undefined;
|
||||
palette_generate(base_, skip_, bg.cval(), fg.cval(), harmonious, &out);
|
||||
return out;
|
||||
}
|
||||
|
||||
fn setMaskBit(mask: *PaletteMask, idx: usize) void {
|
||||
mask.bits[idx >> 6] |= @as(u64, 1) << @as(u6, @intCast(idx & 63));
|
||||
}
|
||||
|
||||
test "color: parse_x11 valid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_]struct {
|
||||
input: []const u8,
|
||||
expected: color.RGB,
|
||||
}{
|
||||
.{ .input = "white", .expected = .{ .r = 255, .g = 255, .b = 255 } },
|
||||
.{ .input = "medium spring green", .expected = .{ .r = 0, .g = 250, .b = 154 } },
|
||||
.{ .input = "ForestGreen", .expected = .{ .r = 34, .g = 139, .b = 34 } },
|
||||
.{ .input = "FoReStGReen", .expected = .{ .r = 34, .g = 139, .b = 34 } },
|
||||
.{ .input = " Forest Green ", .expected = .{ .r = 34, .g = 139, .b = 34 } },
|
||||
.{ .input = "\tblack\t", .expected = .{ .r = 0, .g = 0, .b = 0 } },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.success, parse_x11(case.input.ptr, case.input.len, &out));
|
||||
try expectRgb(case.expected, out);
|
||||
}
|
||||
}
|
||||
|
||||
test "color: parse_x11 invalid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_][]const u8{
|
||||
"nosuchcolor",
|
||||
"",
|
||||
" ",
|
||||
"#ffffff",
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse_x11(case.ptr, case.len, &out));
|
||||
}
|
||||
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse_x11(null, 0, &out));
|
||||
}
|
||||
|
||||
test "color: parse valid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_]struct {
|
||||
input: []const u8,
|
||||
expected: color.RGB,
|
||||
}{
|
||||
.{ .input = "black", .expected = .{ .r = 0, .g = 0, .b = 0 } },
|
||||
.{ .input = "#AABBCC", .expected = .{ .r = 170, .g = 187, .b = 204 } },
|
||||
.{ .input = "0A0B0C", .expected = .{ .r = 10, .g = 11, .b = 12 } },
|
||||
.{ .input = "FFF", .expected = .{ .r = 255, .g = 255, .b = 255 } },
|
||||
.{ .input = "#345", .expected = .{ .r = 51, .g = 68, .b = 85 } },
|
||||
.{ .input = "rgb:1/2/3", .expected = .{ .r = 17, .g = 34, .b = 51 } },
|
||||
.{ .input = " black ", .expected = .{ .r = 0, .g = 0, .b = 0 } },
|
||||
.{ .input = " #AABBCC ", .expected = .{ .r = 170, .g = 187, .b = 204 } },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.success, parse(case.input.ptr, case.input.len, &out));
|
||||
try expectRgb(case.expected, out);
|
||||
}
|
||||
}
|
||||
|
||||
test "color: parse invalid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_][]const u8{
|
||||
"",
|
||||
"notacolor",
|
||||
"#12345",
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse(case.ptr, case.len, &out));
|
||||
}
|
||||
|
||||
var out: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse(null, 0, &out));
|
||||
}
|
||||
|
||||
test "color: parse_palette_entry valid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_]struct {
|
||||
input: []const u8,
|
||||
index: u8,
|
||||
expected: color.RGB,
|
||||
}{
|
||||
.{ .input = "0=#AABBCC", .index = 0, .expected = .{ .r = 170, .g = 187, .b = 204 } },
|
||||
.{ .input = "0xF=#ABCDEF", .index = 15, .expected = .{ .r = 171, .g = 205, .b = 239 } },
|
||||
.{ .input = "0b1=#014589", .index = 1, .expected = .{ .r = 1, .g = 69, .b = 137 } },
|
||||
.{ .input = "0o7=#234567", .index = 7, .expected = .{ .r = 35, .g = 69, .b = 103 } },
|
||||
.{ .input = " 1= #DDEEFF ", .index = 1, .expected = .{ .r = 221, .g = 238, .b = 255 } },
|
||||
.{ .input = "1=black", .index = 1, .expected = .{ .r = 0, .g = 0, .b = 0 } },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var index: u8 = undefined;
|
||||
var rgb: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.success, parse_palette_entry(
|
||||
case.input.ptr,
|
||||
case.input.len,
|
||||
&index,
|
||||
&rgb,
|
||||
));
|
||||
try testing.expectEqual(case.index, index);
|
||||
try expectRgb(case.expected, rgb);
|
||||
}
|
||||
}
|
||||
|
||||
test "color: parse_palette_entry invalid" {
|
||||
const testing = std.testing;
|
||||
|
||||
const cases = [_][]const u8{
|
||||
"256=#AABBCC",
|
||||
"a",
|
||||
"",
|
||||
"1=notacolor",
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
var index: u8 = undefined;
|
||||
var rgb: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse_palette_entry(
|
||||
case.ptr,
|
||||
case.len,
|
||||
&index,
|
||||
&rgb,
|
||||
));
|
||||
}
|
||||
|
||||
var index: u8 = undefined;
|
||||
var rgb: color.RGB.C = undefined;
|
||||
try testing.expectEqual(.invalid_value, parse_palette_entry(null, 0, &index, &rgb));
|
||||
}
|
||||
|
||||
test "color: palette_default" {
|
||||
var out: color.PaletteC = undefined;
|
||||
palette_default(&out);
|
||||
|
||||
const expected = color.paletteCval(&color.default);
|
||||
try expectPaletteC(&expected, &out);
|
||||
|
||||
try expectRgb(.{ .r = 0x1D, .g = 0x1F, .b = 0x21 }, out[0]);
|
||||
try expectRgb(.{ .r = 0, .g = 0, .b = 0 }, out[16]);
|
||||
try expectRgb(.{ .r = 255, .g = 255, .b = 255 }, out[231]);
|
||||
try expectRgb(.{ .r = 8, .g = 8, .b = 8 }, out[232]);
|
||||
try expectRgb(.{ .r = 238, .g = 238, .b = 238 }, out[255]);
|
||||
}
|
||||
|
||||
test "color: palette_generate base16 preserved" {
|
||||
const testing = std.testing;
|
||||
|
||||
const base = color.paletteCval(&color.default);
|
||||
const palette = generatePalette(
|
||||
&base,
|
||||
null,
|
||||
.{ .r = 0, .g = 0, .b = 0 },
|
||||
.{ .r = 255, .g = 255, .b = 255 },
|
||||
false,
|
||||
);
|
||||
|
||||
for (0..16) |i| {
|
||||
try testing.expectEqual(color.RGB.fromC(base[i]), color.RGB.fromC(palette[i]));
|
||||
}
|
||||
}
|
||||
|
||||
test "color: palette_generate cube corners" {
|
||||
const bg = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const palette = generatePalette(null, null, bg, fg, false);
|
||||
|
||||
try expectRgb(bg, palette[16]);
|
||||
try expectRgb(fg, palette[231]);
|
||||
}
|
||||
|
||||
test "color: palette_generate light theme harmonious=false" {
|
||||
const white = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const black = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
|
||||
const normal = generatePalette(null, null, white, black, false);
|
||||
try expectRgb(black, normal[16]);
|
||||
try expectRgb(white, normal[231]);
|
||||
|
||||
const harmonious = generatePalette(null, null, white, black, true);
|
||||
try expectRgb(white, harmonious[16]);
|
||||
try expectRgb(black, harmonious[231]);
|
||||
}
|
||||
|
||||
test "color: palette_generate grayscale monotonic" {
|
||||
const testing = std.testing;
|
||||
|
||||
const palette = generatePalette(
|
||||
null,
|
||||
null,
|
||||
.{ .r = 0, .g = 0, .b = 0 },
|
||||
.{ .r = 255, .g = 255, .b = 255 },
|
||||
false,
|
||||
);
|
||||
|
||||
var prev_lum: f64 = 0.0;
|
||||
for (232..256) |i| {
|
||||
const lum = color.RGB.fromC(palette[i]).luminance();
|
||||
try testing.expect(lum >= prev_lum);
|
||||
prev_lum = lum;
|
||||
}
|
||||
}
|
||||
|
||||
test "color: palette_generate skip mask" {
|
||||
const testing = std.testing;
|
||||
|
||||
const base = color.paletteCval(&color.default);
|
||||
var skip: PaletteMask = .{ .bits = .{ 0, 0, 0, 0 } };
|
||||
setMaskBit(&skip, 20);
|
||||
setMaskBit(&skip, 100);
|
||||
setMaskBit(&skip, 240);
|
||||
|
||||
const palette = generatePalette(
|
||||
&base,
|
||||
&skip,
|
||||
.{ .r = 0, .g = 0, .b = 0 },
|
||||
.{ .r = 255, .g = 255, .b = 255 },
|
||||
false,
|
||||
);
|
||||
|
||||
try testing.expectEqual(color.RGB.fromC(base[20]), color.RGB.fromC(palette[20]));
|
||||
try testing.expectEqual(color.RGB.fromC(base[100]), color.RGB.fromC(palette[100]));
|
||||
try testing.expectEqual(color.RGB.fromC(base[240]), color.RGB.fromC(palette[240]));
|
||||
try testing.expect(!color.RGB.fromC(palette[21]).eql(color.RGB.fromC(base[21])));
|
||||
}
|
||||
|
||||
test "color: palette_generate dark harmonious no-op" {
|
||||
const testing = std.testing;
|
||||
|
||||
const bg = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const normal = generatePalette(null, null, bg, fg, false);
|
||||
const harmonious = generatePalette(null, null, bg, fg, true);
|
||||
|
||||
for (16..256) |i| {
|
||||
try testing.expectEqual(color.RGB.fromC(normal[i]), color.RGB.fromC(harmonious[i]));
|
||||
}
|
||||
}
|
||||
|
||||
test "color: palette_generate light harmonious ramp" {
|
||||
const testing = std.testing;
|
||||
|
||||
const bg = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const fg = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
|
||||
{
|
||||
const palette = generatePalette(null, null, bg, fg, false);
|
||||
var prev_lum: f64 = 0.0;
|
||||
for (232..256) |i| {
|
||||
const lum = color.RGB.fromC(palette[i]).luminance();
|
||||
try testing.expect(lum >= prev_lum);
|
||||
prev_lum = lum;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const palette = generatePalette(null, null, bg, fg, true);
|
||||
var prev_lum: f64 = 1.0;
|
||||
for (232..256) |i| {
|
||||
const lum = color.RGB.fromC(palette[i]).luminance();
|
||||
try testing.expect(lum <= prev_lum);
|
||||
prev_lum = lum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "color: palette_generate NULL base uses default" {
|
||||
const base = color.paletteCval(&color.default);
|
||||
const bg = color.RGB{ .r = 5, .g = 10, .b = 15 };
|
||||
const fg = color.RGB{ .r = 245, .g = 250, .b = 255 };
|
||||
|
||||
const null_base = generatePalette(null, null, bg, fg, false);
|
||||
const explicit = generatePalette(&base, null, bg, fg, false);
|
||||
try expectPaletteC(&explicit, &null_base);
|
||||
}
|
||||
|
||||
test "color: palette_generate NULL skip" {
|
||||
const base = color.paletteCval(&color.default);
|
||||
const bg = color.RGB{ .r = 5, .g = 10, .b = 15 };
|
||||
const fg = color.RGB{ .r = 245, .g = 250, .b = 255 };
|
||||
const skip: PaletteMask = .{ .bits = .{ 0, 0, 0, 0 } };
|
||||
|
||||
const null_skip = generatePalette(&base, null, bg, fg, false);
|
||||
const explicit = generatePalette(&base, &skip, bg, fg, false);
|
||||
try expectPaletteC(&explicit, &null_skip);
|
||||
}
|
||||
|
||||
test "color: palette_generate matches generate256Color" {
|
||||
const base_c = color.paletteCval(&color.default);
|
||||
const bg = color.RGB{ .r = 16, .g = 32, .b = 48 };
|
||||
const fg = color.RGB{ .r = 240, .g = 224, .b = 208 };
|
||||
var skip_c: PaletteMask = .{ .bits = .{ 0, 0, 0, 0 } };
|
||||
setMaskBit(&skip_c, 20);
|
||||
setMaskBit(&skip_c, 100);
|
||||
setMaskBit(&skip_c, 240);
|
||||
|
||||
const actual = generatePalette(&base_c, &skip_c, bg, fg, true);
|
||||
const expected_z = color.generate256Color(
|
||||
color.paletteZval(&base_c),
|
||||
skip_c.toZig(),
|
||||
bg,
|
||||
fg,
|
||||
true,
|
||||
);
|
||||
const expected = color.paletteCval(&expected_z);
|
||||
try expectPaletteC(&expected, &actual);
|
||||
}
|
||||
|
||||
test "color: palette_generate in-place" {
|
||||
var base = color.paletteCval(&color.default);
|
||||
const bg = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const fg = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const expected = generatePalette(&base, null, bg, fg, false);
|
||||
|
||||
palette_generate(&base, null, bg.cval(), fg.cval(), false, &base);
|
||||
try expectPaletteC(&expected, &base);
|
||||
}
|
||||
|
||||
test "color: luminance" {
|
||||
const testing = std.testing;
|
||||
|
||||
const black = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const white = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
try testing.expectApproxEqAbs(@as(f64, 0.0), luminance(black.cval()), 0.000001);
|
||||
try testing.expectApproxEqAbs(@as(f64, 1.0), luminance(white.cval()), 0.000001);
|
||||
|
||||
const samples = [_]color.RGB{
|
||||
.{ .r = 40, .g = 44, .b = 52 },
|
||||
.{ .r = 171, .g = 205, .b = 239 },
|
||||
.{ .r = 12, .g = 34, .b = 56 },
|
||||
};
|
||||
for (samples) |sample| {
|
||||
try testing.expectApproxEqAbs(sample.luminance(), luminance(sample.cval()), 0.000001);
|
||||
}
|
||||
}
|
||||
|
||||
test "color: perceived_luminance" {
|
||||
const testing = std.testing;
|
||||
|
||||
const black = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const white = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const dark = color.RGB{ .r = 0x28, .g = 0x2C, .b = 0x34 };
|
||||
try testing.expectApproxEqAbs(@as(f64, 0.0), perceived_luminance(black.cval()), 0.000001);
|
||||
try testing.expectApproxEqAbs(@as(f64, 1.0), perceived_luminance(white.cval()), 0.000001);
|
||||
try testing.expect(perceived_luminance(dark.cval()) < 0.5);
|
||||
try testing.expect(perceived_luminance(white.cval()) > 0.5);
|
||||
|
||||
const samples = [_]color.RGB{
|
||||
dark,
|
||||
.{ .r = 171, .g = 205, .b = 239 },
|
||||
.{ .r = 12, .g = 34, .b = 56 },
|
||||
};
|
||||
for (samples) |sample| {
|
||||
try testing.expectApproxEqAbs(
|
||||
sample.perceivedLuminance(),
|
||||
perceived_luminance(sample.cval()),
|
||||
0.000001,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "color: contrast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const black = color.RGB{ .r = 0, .g = 0, .b = 0 };
|
||||
const white = color.RGB{ .r = 255, .g = 255, .b = 255 };
|
||||
const gray = color.RGB{ .r = 128, .g = 128, .b = 128 };
|
||||
|
||||
try testing.expectApproxEqAbs(@as(f64, 21.0), contrast(white.cval(), black.cval()), 0.000001);
|
||||
try testing.expectApproxEqAbs(
|
||||
contrast(black.cval(), white.cval()),
|
||||
contrast(white.cval(), black.cval()),
|
||||
0.000001,
|
||||
);
|
||||
try testing.expectApproxEqAbs(@as(f64, 1.0), contrast(gray.cval(), gray.cval()), 0.000001);
|
||||
}
|
||||
|
||||
test "color: x11_names" {
|
||||
const testing = std.testing;
|
||||
|
||||
const names = x11_names();
|
||||
const count = x11_name_count();
|
||||
try testing.expectEqual(x11_color.entries.len, count);
|
||||
try testing.expect(count > 700);
|
||||
|
||||
var walked: usize = 0;
|
||||
while (names[walked].name) |name| : (walked += 1) {
|
||||
const name_slice = std.mem.span(name);
|
||||
const expected = x11_color.map.get(name_slice).?;
|
||||
try testing.expectEqual(expected, color.RGB.fromC(names[walked].color));
|
||||
}
|
||||
|
||||
try testing.expectEqual(count, walked);
|
||||
try testing.expectEqual(@as(?[*:0]const u8, null), names[count].name);
|
||||
}
|
||||
|
||||
test "color: x11_names static" {
|
||||
try std.testing.expectEqual(@intFromPtr(x11_names()), @intFromPtr(x11_names()));
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@ pub const osc_command_type = osc.commandType;
|
||||
pub const osc_command_data = osc.commandData;
|
||||
|
||||
pub const color_rgb_get = color.rgb_get;
|
||||
pub const color_contrast = color.contrast;
|
||||
pub const color_luminance = color.luminance;
|
||||
pub const color_parse = color.parse;
|
||||
pub const color_parse_palette_entry = color.parse_palette_entry;
|
||||
pub const color_parse_x11 = color.parse_x11;
|
||||
pub const color_palette_default = color.palette_default;
|
||||
pub const color_palette_generate = color.palette_generate;
|
||||
pub const color_perceived_luminance = color.perceived_luminance;
|
||||
pub const color_x11_name_count = color.x11_name_count;
|
||||
pub const color_x11_names = color.x11_names;
|
||||
|
||||
pub const color_scheme_report_encode = color_scheme.report_encode;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const std = @import("std");
|
||||
const lib = @import("../lib.zig");
|
||||
const color = @import("../color.zig");
|
||||
const color_c = @import("color.zig");
|
||||
const mouse_event = @import("mouse_event.zig");
|
||||
const point = @import("../point.zig");
|
||||
const size_report = @import("size_report.zig");
|
||||
@@ -38,7 +39,9 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: {
|
||||
break :structs .initComptime(.{
|
||||
.{ "GhosttyBuffer", StructInfo.init(lib.Buffer) },
|
||||
.{ "GhosttyCodepoints", StructInfo.init(Codepoints) },
|
||||
.{ "GhosttyColorPaletteMask", StructInfo.init(color_c.PaletteMask) },
|
||||
.{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
|
||||
.{ "GhosttyColorX11Entry", StructInfo.init(color_c.X11Entry) },
|
||||
.{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
|
||||
.{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
|
||||
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
|
||||
|
||||
@@ -47,6 +47,81 @@ pub const default: Palette = default: {
|
||||
/// Palette is the 256 color palette.
|
||||
pub const Palette = [256]RGB;
|
||||
|
||||
/// A parsed palette entry from Ghostty's config "N=COLOR" syntax.
|
||||
pub const PaletteEntry = struct {
|
||||
index: u8,
|
||||
color: RGB,
|
||||
};
|
||||
|
||||
/// Parse a palette entry in Ghostty config syntax: "N=COLOR" where N is
|
||||
/// a palette index 0-255 (decimal, or 0x/0o/0b-prefixed per Zig's
|
||||
/// parseInt base-0 rules) and COLOR is anything RGB.parse accepts.
|
||||
/// Whitespace (spaces/tabs) around N and COLOR is ignored.
|
||||
pub fn parsePaletteEntry(value: []const u8) error{ InvalidFormat, Overflow }!PaletteEntry {
|
||||
const eql_idx = std.mem.indexOfScalar(u8, value, '=') orelse
|
||||
return error.InvalidFormat;
|
||||
const index = std.fmt.parseInt(
|
||||
u8,
|
||||
std.mem.trim(u8, value[0..eql_idx], " \t"),
|
||||
0,
|
||||
) catch |err| switch (err) {
|
||||
error.Overflow => return error.Overflow,
|
||||
error.InvalidCharacter => return error.InvalidFormat,
|
||||
};
|
||||
const rgb = try RGB.parse(value[eql_idx + 1 ..]);
|
||||
return .{ .index = index, .color = rgb };
|
||||
}
|
||||
|
||||
test "parsePaletteEntry" {
|
||||
const testing = std.testing;
|
||||
|
||||
{
|
||||
const entry = try parsePaletteEntry("0=#AABBCC");
|
||||
try testing.expectEqual(@as(u8, 0), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 170, .g = 187, .b = 204 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry("0b1=#014589");
|
||||
try testing.expectEqual(@as(u8, 1), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 1, .g = 69, .b = 137 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry("0o7=#234567");
|
||||
try testing.expectEqual(@as(u8, 7), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 35, .g = 69, .b = 103 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry("0xF=#ABCDEF");
|
||||
try testing.expectEqual(@as(u8, 15), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 171, .g = 205, .b = 239 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry("0 = #AABBCC");
|
||||
try testing.expectEqual(@as(u8, 0), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 170, .g = 187, .b = 204 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry(" 1= #DDEEFF ");
|
||||
try testing.expectEqual(@as(u8, 1), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 221, .g = 238, .b = 255 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry(" 2 = #123456 ");
|
||||
try testing.expectEqual(@as(u8, 2), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 18, .g = 52, .b = 86 }, entry.color);
|
||||
}
|
||||
{
|
||||
const entry = try parsePaletteEntry("1=black");
|
||||
try testing.expectEqual(@as(u8, 1), entry.index);
|
||||
try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, entry.color);
|
||||
}
|
||||
|
||||
try testing.expectError(error.InvalidFormat, parsePaletteEntry(" "));
|
||||
try testing.expectError(error.InvalidFormat, parsePaletteEntry("a"));
|
||||
try testing.expectError(error.Overflow, parsePaletteEntry("256=#AABBCC"));
|
||||
try testing.expectError(error.InvalidFormat, parsePaletteEntry("1=notacolor"));
|
||||
}
|
||||
|
||||
/// C-compatible palette type using the extern RGB struct.
|
||||
pub const PaletteC = [256]RGB.C;
|
||||
|
||||
@@ -541,6 +616,8 @@ pub const RGB = packed struct(u24) {
|
||||
|
||||
/// Parse a color specification.
|
||||
///
|
||||
/// Leading and trailing spaces and tabs are ignored.
|
||||
///
|
||||
/// Any of the following forms are accepted:
|
||||
///
|
||||
/// 1. rgb:<red>/<green>/<blue>
|
||||
@@ -554,38 +631,42 @@ pub const RGB = packed struct(u24) {
|
||||
/// where <red>, <green>, and <blue> are floating point values between
|
||||
/// 0.0 and 1.0 (inclusive).
|
||||
///
|
||||
/// 3. #rgb, #rrggbb, #rrrgggbbb #rrrrggggbbbb
|
||||
/// 3. #rgb, #rrggbb, rgb, rrggbb, #rrrgggbbb, #rrrrggggbbbb
|
||||
///
|
||||
/// where `r`, `g`, and `b` are a single hexadecimal digit.
|
||||
/// These specify a color with 4, 8, 12, and 16 bits of precision
|
||||
/// per color channel.
|
||||
/// where `r`, `g`, and `b` are hexadecimal digits. The forms with
|
||||
/// a leading # specify a color with 4, 8, 12, and 16 bits of
|
||||
/// precision per color channel. The forms without a leading # are
|
||||
/// accepted for compatibility with Ghostty config/theme color values.
|
||||
///
|
||||
/// 4. X11 color names
|
||||
pub fn parse(value: []const u8) error{InvalidFormat}!RGB {
|
||||
if (value.len == 0) {
|
||||
const input = std.mem.trim(u8, value, " \t");
|
||||
if (input.len == 0) {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
if (value[0] == '#') {
|
||||
switch (value.len) {
|
||||
if (input[0] == '#') {
|
||||
switch (input.len) {
|
||||
4 => return RGB{
|
||||
.r = try RGB.fromHex(value[1..2]),
|
||||
.g = try RGB.fromHex(value[2..3]),
|
||||
.b = try RGB.fromHex(value[3..4]),
|
||||
.r = try RGB.fromHex(input[1..2]),
|
||||
.g = try RGB.fromHex(input[2..3]),
|
||||
.b = try RGB.fromHex(input[3..4]),
|
||||
},
|
||||
7 => return RGB{
|
||||
.r = try RGB.fromHex(value[1..3]),
|
||||
.g = try RGB.fromHex(value[3..5]),
|
||||
.b = try RGB.fromHex(value[5..7]),
|
||||
.r = try RGB.fromHex(input[1..3]),
|
||||
.g = try RGB.fromHex(input[3..5]),
|
||||
.b = try RGB.fromHex(input[5..7]),
|
||||
},
|
||||
10 => return RGB{
|
||||
.r = try RGB.fromHex(value[1..4]),
|
||||
.g = try RGB.fromHex(value[4..7]),
|
||||
.b = try RGB.fromHex(value[7..10]),
|
||||
.r = try RGB.fromHex(input[1..4]),
|
||||
.g = try RGB.fromHex(input[4..7]),
|
||||
.b = try RGB.fromHex(input[7..10]),
|
||||
},
|
||||
13 => return RGB{
|
||||
.r = try RGB.fromHex(value[1..5]),
|
||||
.g = try RGB.fromHex(value[5..9]),
|
||||
.b = try RGB.fromHex(value[9..13]),
|
||||
.r = try RGB.fromHex(input[1..5]),
|
||||
.g = try RGB.fromHex(input[5..9]),
|
||||
.b = try RGB.fromHex(input[9..13]),
|
||||
},
|
||||
|
||||
else => {
|
||||
@@ -595,24 +676,36 @@ pub const RGB = packed struct(u24) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for X11 named colors. We allow whitespace around the edges
|
||||
// of the color because Kitty allows whitespace. This is not part of
|
||||
// any spec I could find.
|
||||
if (x11_color.map.get(std.mem.trim(u8, value, " "))) |rgb| return rgb;
|
||||
// Check for X11 named colors. We allow whitespace around the edges.
|
||||
if (x11_color.map.get(input)) |rgb| return rgb;
|
||||
|
||||
if (value.len < "rgb:a/a/a".len or !std.mem.eql(u8, value[0..3], "rgb")) {
|
||||
switch (input.len) {
|
||||
3 => return RGB{
|
||||
.r = try RGB.fromHex(input[0..1]),
|
||||
.g = try RGB.fromHex(input[1..2]),
|
||||
.b = try RGB.fromHex(input[2..3]),
|
||||
},
|
||||
6 => return RGB{
|
||||
.r = try RGB.fromHex(input[0..2]),
|
||||
.g = try RGB.fromHex(input[2..4]),
|
||||
.b = try RGB.fromHex(input[4..6]),
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
if (input.len < "rgb:a/a/a".len or !std.mem.eql(u8, input[0..3], "rgb")) {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
var i: usize = 3;
|
||||
|
||||
const use_intensity = if (value[i] == 'i') blk: {
|
||||
const use_intensity = if (input[i] == 'i') blk: {
|
||||
i += 1;
|
||||
break :blk true;
|
||||
} else false;
|
||||
|
||||
if (value[i] != ':') {
|
||||
if (input[i] != ':') {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
@@ -620,8 +713,8 @@ pub const RGB = packed struct(u24) {
|
||||
i += 1;
|
||||
|
||||
const r = r: {
|
||||
const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end|
|
||||
value[i..end]
|
||||
const slice = if (std.mem.indexOfScalarPos(u8, input, i, '/')) |end|
|
||||
input[i..end]
|
||||
else {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
@@ -636,8 +729,8 @@ pub const RGB = packed struct(u24) {
|
||||
};
|
||||
|
||||
const g = g: {
|
||||
const slice = if (std.mem.indexOfScalarPos(u8, value, i, '/')) |end|
|
||||
value[i..end]
|
||||
const slice = if (std.mem.indexOfScalarPos(u8, input, i, '/')) |end|
|
||||
input[i..end]
|
||||
else {
|
||||
@branchHint(.cold);
|
||||
return error.InvalidFormat;
|
||||
@@ -652,9 +745,9 @@ pub const RGB = packed struct(u24) {
|
||||
};
|
||||
|
||||
const b = if (use_intensity)
|
||||
try RGB.fromIntensity(value[i..])
|
||||
try RGB.fromIntensity(input[i..])
|
||||
else
|
||||
try RGB.fromHex(value[i..]);
|
||||
try RGB.fromHex(input[i..]);
|
||||
|
||||
return RGB{
|
||||
.r = r,
|
||||
@@ -780,6 +873,11 @@ test "RGB.parse" {
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#fffffffff"));
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("#ffffffffffff"));
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 16 }, try RGB.parse("#ff0010"));
|
||||
try testing.expectEqual(RGB{ .r = 10, .g = 11, .b = 12 }, try RGB.parse("0A0B0C"));
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("FFFFFF"));
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 255, .b = 255 }, try RGB.parse("FFF"));
|
||||
try testing.expectEqual(RGB{ .r = 51, .g = 68, .b = 85 }, try RGB.parse("#345"));
|
||||
try testing.expectEqual(RGB{ .r = 170, .g = 187, .b = 204 }, try RGB.parse(" #AABBCC "));
|
||||
|
||||
try testing.expectEqual(RGB{ .r = 0, .g = 0, .b = 0 }, try RGB.parse("black"));
|
||||
try testing.expectEqual(RGB{ .r = 255, .g = 0, .b = 0 }, try RGB.parse("red"));
|
||||
@@ -790,8 +888,11 @@ test "RGB.parse" {
|
||||
try testing.expectEqual(RGB{ .r = 124, .g = 252, .b = 0 }, try RGB.parse("LawnGreen"));
|
||||
try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, try RGB.parse("medium spring green"));
|
||||
try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse(" Forest Green "));
|
||||
try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, try RGB.parse("\tForestGreen\t"));
|
||||
|
||||
// Invalid format
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse(""));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse(" "));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("rgb;"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("rgb:"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse(":a/a/a"));
|
||||
@@ -807,6 +908,9 @@ test "RGB.parse" {
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("#ffff"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("#fffff"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("#gggggg"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("#12345"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("12345"));
|
||||
try testing.expectError(error.InvalidFormat, RGB.parse("nosuchcolor"));
|
||||
}
|
||||
|
||||
test "DynamicPalette: init" {
|
||||
|
||||
@@ -2,6 +2,20 @@ const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const RGB = @import("color.zig").RGB;
|
||||
|
||||
/// A single X11 color entry.
|
||||
pub const Entry = struct {
|
||||
/// Color name. Null-terminated so it can be exposed through the C API
|
||||
/// without runtime allocation.
|
||||
name: [:0]const u8,
|
||||
color: RGB,
|
||||
};
|
||||
|
||||
/// All X11 colors in rgb.txt file order.
|
||||
///
|
||||
/// This is kept separate from `map` because the C API needs stable file order
|
||||
/// and null-terminated names. `ColorMap` keys do not provide that contract.
|
||||
pub const entries: []const Entry = entriesArray();
|
||||
|
||||
/// The map of all available X11 colors.
|
||||
pub const map = colorMap();
|
||||
|
||||
@@ -10,15 +24,10 @@ pub const ColorMap = std.StaticStringMapWithEql(
|
||||
std.static_string_map.eqlAsciiIgnoreCase,
|
||||
);
|
||||
|
||||
fn colorMap() ColorMap {
|
||||
@setEvalBranchQuota(500_000);
|
||||
|
||||
const KV = struct { []const u8, RGB };
|
||||
|
||||
// The length of our data is the number of lines in the rgb file.
|
||||
fn entriesArray() []const Entry {
|
||||
@setEvalBranchQuota(1_000_000);
|
||||
const len = std.mem.count(u8, data, "\n");
|
||||
var kvs: [len]KV = undefined;
|
||||
|
||||
var result: [len]Entry = undefined;
|
||||
// Parse the line. This is not very robust parsing, because we expect
|
||||
// a very exact format for rgb.txt. However, this is all done at comptime
|
||||
// so if our data is bad, we should hopefully get an error here or one
|
||||
@@ -37,11 +46,29 @@ fn colorMap() ColorMap {
|
||||
const g = try std.fmt.parseInt(u8, std.mem.trim(u8, line[4..7], " "), 10);
|
||||
const b = try std.fmt.parseInt(u8, std.mem.trim(u8, line[8..11], " "), 10);
|
||||
const name = std.mem.trim(u8, line[12..], " \t");
|
||||
kvs[i] = .{ name, .{ .r = r, .g = g, .b = b } };
|
||||
var name_z: [name.len:0]u8 = undefined;
|
||||
@memcpy(name_z[0..name.len], name);
|
||||
name_z[name.len] = 0;
|
||||
const final_name = name_z;
|
||||
result[i] = .{
|
||||
.name = final_name[0..name.len :0],
|
||||
.color = .{ .r = r, .g = g, .b = b },
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
assert(i == len);
|
||||
|
||||
const final = result;
|
||||
return &final;
|
||||
}
|
||||
|
||||
fn colorMap() ColorMap {
|
||||
@setEvalBranchQuota(1_000_000);
|
||||
|
||||
const KV = struct { []const u8, RGB };
|
||||
var kvs: [entries.len]KV = undefined;
|
||||
for (entries, 0..) |entry, i| kvs[i] = .{ entry.name, entry.color };
|
||||
|
||||
return .initComptime(kvs);
|
||||
}
|
||||
|
||||
@@ -67,3 +94,14 @@ test {
|
||||
try testing.expectEqual(RGB{ .r = 0, .g = 250, .b = 154 }, map.get("mediumspringgreen"));
|
||||
try testing.expectEqual(RGB{ .r = 34, .g = 139, .b = 34 }, map.get("forestgreen"));
|
||||
}
|
||||
|
||||
test "entries" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expect(entries.len > 700);
|
||||
for (entries) |entry| {
|
||||
try testing.expectEqual(entry.color, map.get(entry.name).?);
|
||||
try testing.expectEqual(@as(u8, 0), entry.name.ptr[entry.name.len]);
|
||||
}
|
||||
try testing.expectEqualStrings("snow", entries[0].name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user