#include "palette.h" #include "internal.h" #include #include #include #include /* Linear light for each byte value of an sRGB channel. The entries are the * transfer curve, evaluated once and stored, so every host reads the same ones. */ static const float srgb_linear[256] = { 0x0p+0f, 0x1.3e4568p-12f, 0x1.3e4568p-11f, 0x1.dd681cp-11f, 0x1.3e4568p-10f, 0x1.8dd6c2p-10f, 0x1.dd681cp-10f, 0x1.167cbap-9f, 0x1.3e4568p-9f, 0x1.660e16p-9f, 0x1.8dd6c2p-9f, 0x1.b6a31ap-9f, 0x1.e1e31ap-9f, 0x1.07c38cp-8f, 0x1.1fcc2cp-8f, 0x1.390ffap-8f, 0x1.53936ep-8f, 0x1.6f5adep-8f, 0x1.8c6a92p-8f, 0x1.aac6c2p-8f, 0x1.ca7382p-8f, 0x1.eb74ep-8f, 0x1.06e76ap-7f, 0x1.18c2a4p-7f, 0x1.2b4e06p-7f, 0x1.3e8b7cp-7f, 0x1.527cd6p-7f, 0x1.6723eep-7f, 0x1.7c8292p-7f, 0x1.929a86p-7f, 0x1.a96d8ep-7f, 0x1.c0fd62p-7f, 0x1.d94bbep-7f, 0x1.f25a44p-7f, 0x1.06155p-6f, 0x1.135f3ep-6f, 0x1.210bb6p-6f, 0x1.2f1b8ap-6f, 0x1.3d8f84p-6f, 0x1.4c6866p-6f, 0x1.5ba6fap-6f, 0x1.6b4c02p-6f, 0x1.7b584p-6f, 0x1.8bcc72p-6f, 0x1.9ca956p-6f, 0x1.adefaap-6f, 0x1.bfa02p-6f, 0x1.d1bb72p-6f, 0x1.e44258p-6f, 0x1.f73582p-6f, 0x1.054ad2p-5f, 0x1.0f31b8p-5f, 0x1.194fccp-5f, 0x1.23a55ep-5f, 0x1.2e32c6p-5f, 0x1.38f85cp-5f, 0x1.43f678p-5f, 0x1.4f2d64p-5f, 0x1.5a9d76p-5f, 0x1.6647p-5f, 0x1.722a56p-5f, 0x1.7e47c6p-5f, 0x1.8a9fa2p-5f, 0x1.973238p-5f, 0x1.a3ffdep-5f, 0x1.b108d4p-5f, 0x1.be4d6ep-5f, 0x1.cbcdfcp-5f, 0x1.d98ac8p-5f, 0x1.e7841ep-5f, 0x1.f5ba52p-5f, 0x1.0216cep-4f, 0x1.096f2ap-4f, 0x1.10e66p-4f, 0x1.187c94p-4f, 0x1.2031ecp-4f, 0x1.28068ap-4f, 0x1.2ffa94p-4f, 0x1.380e2cp-4f, 0x1.404176p-4f, 0x1.489496p-4f, 0x1.5107aep-4f, 0x1.599aep-4f, 0x1.624e5p-4f, 0x1.6b2224p-4f, 0x1.741676p-4f, 0x1.7d2b6ap-4f, 0x1.866124p-4f, 0x1.8fb7c4p-4f, 0x1.992f6cp-4f, 0x1.a2c83ep-4f, 0x1.ac8258p-4f, 0x1.b65ddep-4f, 0x1.c05aeep-4f, 0x1.ca79aap-4f, 0x1.d4ba32p-4f, 0x1.df1ca4p-4f, 0x1.e9a122p-4f, 0x1.f447dp-4f, 0x1.ff10c2p-4f, 0x1.04fe0ep-3f, 0x1.0a85p-3f, 0x1.101d46p-3f, 0x1.15c6fp-3f, 0x1.1b820ap-3f, 0x1.214ea8p-3f, 0x1.272cd6p-3f, 0x1.2d1ca4p-3f, 0x1.331e2p-3f, 0x1.39315ap-3f, 0x1.3f566p-3f, 0x1.458d46p-3f, 0x1.4bd612p-3f, 0x1.5230d8p-3f, 0x1.589dap-3f, 0x1.5f1c82p-3f, 0x1.65ad88p-3f, 0x1.6c50c2p-3f, 0x1.73063cp-3f, 0x1.79ce06p-3f, 0x1.80a82cp-3f, 0x1.8794bep-3f, 0x1.8e93c8p-3f, 0x1.95a55cp-3f, 0x1.9cc984p-3f, 0x1.a4004ep-3f, 0x1.ab49ccp-3f, 0x1.b2a606p-3f, 0x1.ba1516p-3f, 0x1.c196f8p-3f, 0x1.c92bcp-3f, 0x1.d0d37ep-3f, 0x1.d88e4p-3f, 0x1.e05c18p-3f, 0x1.e83d08p-3f, 0x1.f0312p-3f, 0x1.f8387p-3f, 0x1.002984p-2f, 0x1.044078p-2f, 0x1.08611ap-2f, 0x1.0c8b74p-2f, 0x1.10bf8ap-2f, 0x1.14fd64p-2f, 0x1.194506p-2f, 0x1.1d967ap-2f, 0x1.21f1c2p-2f, 0x1.2656e8p-2f, 0x1.2ac5fp-2f, 0x1.2f3ee2p-2f, 0x1.33c1c4p-2f, 0x1.384e9cp-2f, 0x1.3ce56ep-2f, 0x1.418644p-2f, 0x1.463124p-2f, 0x1.4ae61p-2f, 0x1.4fa512p-2f, 0x1.546e3p-2f, 0x1.59416ep-2f, 0x1.5e1ed2p-2f, 0x1.630666p-2f, 0x1.67f83p-2f, 0x1.6cf43p-2f, 0x1.71fa6ep-2f, 0x1.770af2p-2f, 0x1.7c25c2p-2f, 0x1.814ae2p-2f, 0x1.867a5ap-2f, 0x1.8bb42ep-2f, 0x1.90f866p-2f, 0x1.964706p-2f, 0x1.9ba016p-2f, 0x1.a1039ap-2f, 0x1.a67198p-2f, 0x1.abea16p-2f, 0x1.b16d1ap-2f, 0x1.b6faa8p-2f, 0x1.bc92cap-2f, 0x1.c23582p-2f, 0x1.c7e2d6p-2f, 0x1.cd9acep-2f, 0x1.d35d6cp-2f, 0x1.d92abap-2f, 0x1.df02bap-2f, 0x1.e4e574p-2f, 0x1.ead2ecp-2f, 0x1.f0cb28p-2f, 0x1.f6ce2ep-2f, 0x1.fcdc04p-2f, 0x1.017a5ap-1f, 0x1.048c1cp-1f, 0x1.07a34ep-1f, 0x1.0abff2p-1f, 0x1.0de20cp-1f, 0x1.11099ep-1f, 0x1.1436acp-1f, 0x1.176936p-1f, 0x1.1aa14p-1f, 0x1.1ddecep-1f, 0x1.2121e2p-1f, 0x1.246a7cp-1f, 0x1.27b8a2p-1f, 0x1.2b0c56p-1f, 0x1.2e659ap-1f, 0x1.31c472p-1f, 0x1.3528dep-1f, 0x1.3892e2p-1f, 0x1.3c0282p-1f, 0x1.3f77bep-1f, 0x1.42f29cp-1f, 0x1.46731cp-1f, 0x1.49f94p-1f, 0x1.4d850cp-1f, 0x1.511682p-1f, 0x1.54ada6p-1f, 0x1.584a7ap-1f, 0x1.5bedp-1f, 0x1.5f953cp-1f, 0x1.63432ep-1f, 0x1.66f6d8p-1f, 0x1.6ab04p-1f, 0x1.6e6f66p-1f, 0x1.72344cp-1f, 0x1.75fef8p-1f, 0x1.79cf6ap-1f, 0x1.7da5a4p-1f, 0x1.8181a8p-1f, 0x1.85637cp-1f, 0x1.894b2p-1f, 0x1.8d3896p-1f, 0x1.912be2p-1f, 0x1.952504p-1f, 0x1.992402p-1f, 0x1.9d28dcp-1f, 0x1.a13396p-1f, 0x1.a5443p-1f, 0x1.a95aaep-1f, 0x1.ad7714p-1f, 0x1.b1996p-1f, 0x1.b5c19ap-1f, 0x1.b9efcp-1f, 0x1.be23d8p-1f, 0x1.c25ddcp-1f, 0x1.c69ddep-1f, 0x1.cae3d2p-1f, 0x1.cf2fc6p-1f, 0x1.d381acp-1f, 0x1.d7d99ap-1f, 0x1.dc377ep-1f, 0x1.e09b7p-1f, 0x1.e5055ep-1f, 0x1.e9755cp-1f, 0x1.edeb5cp-1f, 0x1.f26772p-1f, 0x1.f6e98cp-1f, 0x1.fb71cp-1f, 0x1p+0f, }; static inline float linf_to_srgbf(float c) { return c > 0.0031308f ? 1.055f * powf(c, 1.0f / 2.4f) - 0.055f : 12.92f * c; } static const struct term_theme builtin_theme = { .fg = 0xFFFFFFFFu, .bg = 0xFF1E1E2Eu, .cursor = 0xFF89B4FAu, .ansi = { 0xFF585B70, 0xFFCC6666, 0xFFB5BD68, 0xFFF0C674, 0xFF81A2BE, 0xFFB294BB, 0xFF8ABEB7, 0xFFC5C8C6, 0xFF666666, 0xFFD54E53, 0xFFB9CA4A, 0xFFE7C547, 0xFF7AA6DA, 0xFFC397D8, 0xFF70C0B1, 0xFFEAEAEA, }, }; void term_theme_default(struct term_theme *theme) { *theme = builtin_theme; } static const struct term_theme high_contrast_theme = { .fg = 0xFFFFFFFFu, .bg = 0xFF000000u, .cursor = 0xFF00FFFFu, .ansi = { 0xFF000000, 0xFFFF0000, 0xFF00FF00, 0xFFFFFF00, 0xFF0000FF, 0xFFFF00FF, 0xFF00FFFF, 0xFFFFFFFF, 0xFF808080, 0xFFFF5555, 0xFF55FF55, 0xFFFFFF55, 0xFF5555FF, 0xFFFF55FF, 0xFF55FFFF, 0xFFFFFFFF, }, }; void term_theme_high_contrast(struct term_theme *theme) { *theme = high_contrast_theme; } /* Finds the slot for key and the default it resets to. Keys are * foreground, background, and color0 to color15. */ static bool palette_slot(struct term_theme *p, const struct term_theme *def, const char *key, uint32_t **slot, const uint32_t **fallback) { if (!strcmp(key, "foreground")) { *slot = &p->fg; *fallback = &def->fg; return true; } if (!strcmp(key, "background")) { *slot = &p->bg; *fallback = &def->bg; return true; } if (strncmp(key, "color", 5)) return false; const char *digits = key + 5; if (!*digits || (*digits == '0' && digits[1])) return false; int i = 0; for (const char *d = digits; *d; d++) { if (*d < '0' || *d > '9') return false; i = i * 10 + (*d - '0'); if (i > 15) return false; } *slot = &p->ansi[i]; *fallback = &def->ansi[i]; return true; } bool term_theme_set_key(struct term_theme *p, const char *key, const char *val) { struct term_theme def; term_theme_default(&def); uint32_t *slot; const uint32_t *fallback; if (!palette_slot(p, &def, key, &slot, &fallback)) return false; if (!val[0]) { *slot = *fallback; return true; } uint32_t v; if (!term_color_parse(val, &v)) return false; *slot = v; return true; } static bool parse_hex_channel(const char *s, size_t len, uint8_t *out) { if (len == 0 || len > 4) return false; unsigned v = 0; for (size_t i = 0; i < len; i++) { int d = term_hex_nib(s[i]); if (d < 0) return false; v = (v << 4) | (unsigned)d; } static const unsigned max_for_len[5] = {0, 15, 255, 4095, 65535}; unsigned max = max_for_len[len]; *out = (uint8_t)((unsigned long)v * 255 / max); return true; } static bool parse_fraction(const char *s, size_t len, uint8_t *out) { if (len == 0) return false; bool seen_digit = false; bool seen_dot = false; unsigned integer = 0; unsigned frac = 0; unsigned frac_digits = 0; size_t i = 0; if (s[0] == '.') { seen_dot = true; i = 1; } for (; i < len; i++) { char c = s[i]; if (c >= '0' && c <= '9') { seen_digit = true; if (!seen_dot) { integer = integer * 10 + (unsigned)(c - '0'); if (integer > 1) return false; } else { if (frac_digits < 9) { frac = frac * 10 + (unsigned)(c - '0'); frac_digits++; } } } else if (c == '.' && !seen_dot) { seen_dot = true; } else { return false; } } if (!seen_digit) return false; double value = (double)integer; if (frac_digits > 0) { double f = (double)frac; for (unsigned k = 0; k < frac_digits; k++) f /= 10.0; value += f; } if (value > 1.0) value = 1.0; *out = (uint8_t)(value * 255.0 + 0.5); return true; } static const char *skip_leading_ws(const char *s) { while (*s == ' ' || *s == '\t') s++; return s; } static size_t rtrimmed_len(const char *s) { size_t n = strlen(s); while (n > 0 && (s[n - 1] == ' ' || s[n - 1] == '\t')) n--; return n; } bool term_color_parse(const char *input, uint32_t *out) { if (!input || !out) return false; const char *s = skip_leading_ws(input); size_t len = rtrimmed_len(s); if (len == 0) return false; if (s[0] == '#') { const char *body = s + 1; size_t blen = len - 1; size_t cw = blen / 3; if (blen != 3 && blen != 6 && blen != 9 && blen != 12) return false; uint8_t r, g, b; if (!parse_hex_channel(body, cw, &r) || !parse_hex_channel(body + cw, cw, &g) || !parse_hex_channel(body + 2 * cw, cw, &b)) return false; *out = term_rgb(r, g, b); return true; } if (len >= 4 && (s[0] == 'r' || s[0] == 'R') && (s[1] == 'g' || s[1] == 'G') && (s[2] == 'b' || s[2] == 'B')) { size_t i = 3; bool intensity = false; if ((s[i] == 'i' || s[i] == 'I')) { intensity = true; i++; } if (i >= len || s[i] != ':') return false; i++; const char *channels[3]; size_t chlens[3]; int ci = 0; size_t start = i; for (;; i++) { if (i == len || s[i] == '/') { if (ci >= 3) return false; channels[ci] = s + start; chlens[ci] = i - start; ci++; if (i == len) break; start = i + 1; } } if (ci != 3) return false; uint8_t r, g, b; if (intensity) { if (!parse_fraction(channels[0], chlens[0], &r) || !parse_fraction(channels[1], chlens[1], &g) || !parse_fraction(channels[2], chlens[2], &b)) return false; } else { if (!parse_hex_channel(channels[0], chlens[0], &r) || !parse_hex_channel(channels[1], chlens[1], &g) || !parse_hex_channel(channels[2], chlens[2], &b)) return false; } *out = term_rgb(r, g, b); return true; } if (len == 6 || len == 3) { uint8_t r, g, b; size_t cw = len / 3; if (!parse_hex_channel(s, cw, &r) || !parse_hex_channel(s + cw, cw, &g) || !parse_hex_channel(s + 2 * cw, cw, &b)) return false; *out = term_rgb(r, g, b); return true; } return false; } struct lab { float l, a, b; }; static struct lab lab_from_rgb(uint32_t argb) { float r = srgb_linear[(argb >> 16) & 0xFF]; float g = srgb_linear[(argb >> 8) & 0xFF]; float b = srgb_linear[argb & 0xFF]; float x = (r * 0.4124564f + g * 0.3575761f + b * 0.1804375f) / 0.95047f; float y = r * 0.2126729f + g * 0.7151522f + b * 0.0721750f; float z = (r * 0.0193339f + g * 0.1191920f + b * 0.9503041f) / 1.08883f; x = x > 0.008856f ? cbrtf(x) : 7.787f * x + 16.0f / 116.0f; y = y > 0.008856f ? cbrtf(y) : 7.787f * y + 16.0f / 116.0f; z = z > 0.008856f ? cbrtf(z) : 7.787f * z + 16.0f / 116.0f; struct lab lab; lab.l = 116.0f * y - 16.0f; lab.a = 500.0f * (x - y); lab.b = 200.0f * (y - z); return lab; } static uint32_t lab_to_rgb(struct lab lab) { float y = (lab.l + 16.0f) / 116.0f; float x = lab.a / 500.0f + y; float z = y - lab.b / 200.0f; float x3 = x * x * x; float y3 = y * y * y; float z3 = z * z * z; float xf = (x3 > 0.008856f ? x3 : (x - 16.0f / 116.0f) / 7.787f) * 0.95047f; float yf = (y3 > 0.008856f ? y3 : (y - 16.0f / 116.0f) / 7.787f); float zf = (z3 > 0.008856f ? z3 : (z - 16.0f / 116.0f) / 7.787f) * 1.08883f; float r = xf * 3.2404542f - yf * 1.5371385f - zf * 0.4985314f; float g = -xf * 0.9692660f + yf * 1.8760108f + zf * 0.0415560f; float b = xf * 0.0556434f - yf * 0.2040259f + zf * 1.0572252f; r = linf_to_srgbf(r); g = linf_to_srgbf(g); b = linf_to_srgbf(b); if (r < 0.0f) r = 0.0f; if (r > 1.0f) r = 1.0f; if (g < 0.0f) g = 0.0f; if (g > 1.0f) g = 1.0f; if (b < 0.0f) b = 0.0f; if (b > 1.0f) b = 1.0f; uint8_t rb = (uint8_t)(r * 255.0f + 0.5f); uint8_t gb = (uint8_t)(g * 255.0f + 0.5f); uint8_t bb = (uint8_t)(b * 255.0f + 0.5f); return term_rgb(rb, gb, bb); } static struct lab lab_lerp(float t, struct lab a, struct lab b) { struct lab r; r.l = a.l + t * (b.l - a.l); r.a = a.a + t * (b.a - a.a); r.b = a.b + t * (b.b - a.b); return r; } /* * Derive the 256-color palette from a theme. Entries 0-15 are the theme's * ANSI colors. The 6x6x6 cube and the gray ramp interpolate in Lab space * between the background, the ANSI colors, and the foreground, so they match * the theme instead of the fixed xterm values. */ static void derive_palette(uint32_t *p, const struct term_theme *theme) { struct lab base8[8]; base8[0] = lab_from_rgb(theme->bg); for (int i = 1; i < 7; i++) base8[i] = lab_from_rgb(theme->ansi[i]); base8[7] = lab_from_rgb(theme->fg); for (int i = 0; i < 16; i++) p[i] = theme->ansi[i]; p[TERM_PALETTE_FG] = theme->fg; p[TERM_PALETTE_BG] = theme->bg; p[TERM_PALETTE_CURSOR] = theme->cursor; bool is_light_theme = base8[7].l < base8[0].l; if (is_light_theme) { struct lab tmp = base8[0]; base8[0] = base8[7]; base8[7] = tmp; } int idx = 16; for (int ri = 0; ri < 6; ri++) { float tr = (float)ri / 5.0f; struct lab c0 = lab_lerp(tr, base8[0], base8[1]); struct lab c1 = lab_lerp(tr, base8[2], base8[3]); struct lab c2 = lab_lerp(tr, base8[4], base8[5]); struct lab c3 = lab_lerp(tr, base8[6], base8[7]); for (int gi = 0; gi < 6; gi++) { float tg = (float)gi / 5.0f; struct lab c4 = lab_lerp(tg, c0, c1); struct lab c5 = lab_lerp(tg, c2, c3); for (int bi = 0; bi < 6; bi++) p[idx++] = lab_to_rgb(lab_lerp((float)bi / 5.0f, c4, c5)); } } for (int i = 0; i < 24; i++) { float t = (float)(i + 1) / 25.0f; p[idx++] = lab_to_rgb(lab_lerp(t, base8[0], base8[7])); } } static bool theme_is_zero(const struct term_theme *theme) { static const struct term_theme zero; return !memcmp(theme, &zero, sizeof(zero)); } void term_palette_init(struct palette *p, const struct term_theme *theme) { p->theme = theme && !theme_is_zero(theme) ? *theme : builtin_theme; if (!p->theme.cursor) p->theme.cursor = builtin_theme.cursor; derive_palette(p->base, &p->theme); memcpy(p->colors, p->base, sizeof(p->colors)); p->version++; } static bool valid_slot(int slot) { return slot >= 0 && slot < TERM_PALETTE_SIZE; } void term_palette_set(struct palette *p, int slot, uint32_t argb) { if (!valid_slot(slot) || p->colors[slot] == argb) return; p->colors[slot] = argb; p->version++; } void term_palette_reset(struct palette *p, int slot) { if (valid_slot(slot)) term_palette_set(p, slot, p->base[slot]); } void term_palette_reset_all(struct palette *p) { if (memcmp(p->colors, p->base, sizeof(p->colors))) { memcpy(p->colors, p->base, sizeof(p->colors)); p->version++; } }