summaryrefslogtreecommitdiff
path: root/src/palette.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/palette.c')
-rw-r--r--src/palette.c492
1 files changed, 492 insertions, 0 deletions
diff --git a/src/palette.c b/src/palette.c
new file mode 100644
index 0000000..1b27e3a
--- /dev/null
+++ b/src/palette.c
@@ -0,0 +1,492 @@
+#include "palette.h"
+#include "internal.h"
+
+#include <math.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+/* 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++;
+ }
+}