/* * color.c * vim: expandtab:ts=4:sts=4:sw=4 * * Copyright (C) 2019 Aurelien Aptel * Copyright (C) 2019 - 2023 Michael Vetter * * This file is part of Profani-tty. * * Profani-tty is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Profani-tty is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Profani-tty. If not, see . * * In addition, as a special exception, the copyright holders give permission to * link the code of portions of this program with the OpenSSL library under * certain conditions as described in each individual source file, and * distribute linked combinations including the two. * * You must obey the GNU General Public License in all respects for all of the * code used other than OpenSSL. If you modify file(s) with this exception, you * may extend this exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this exception * statement from your version. If you delete this exception statement from all * source files in the program, then also delete it here. * */ #include "config.h" #include #include #include #include #include #ifdef HAVE_NCURSESW_NCURSES_H #include #elif HAVE_NCURSES_H #include #elif HAVE_CURSES_H #include #endif #include "config/color.h" #include "config/theme.h" #include "log.h" static struct color_pair_cache { struct { int16_t fg, bg; }* pairs; int size; int capacity; } cache = { 0 }; /* * xterm default 256 colors * XXX: there are many duplicates... (eg blue3) */ const struct color_def color_names[COLOR_NAME_SIZE] = { [0] = { 0, 0, 0, "black" }, [1] = { 0, 100, 25, "red" }, [2] = { 120, 100, 25, "green" }, [3] = { 60, 100, 25, "yellow" }, [4] = { 240, 100, 25, "blue" }, [5] = { 300, 100, 25, "magenta" }, [6] = { 180, 100, 25, "cyan" }, [7] = { 0, 0, 75, "white" }, [8] = { 0, 0, 50, "lightblack" }, [9] = { 0, 100, 50, "lightred" }, [10] = { 120, 100, 50, "lightgreen" }, [11] = { 60, 100, 50, "lightyellow" }, [12] = { 240, 100, 50, "lightblue" }, [13] = { 300, 100, 50, "lightmagenta" }, [14] = { 180, 100, 50, "lightcyan" }, [15] = { 0, 0, 100, "lightwhite" }, [16] = { 0, 0, 0, "grey0" }, [17] = { 240, 100, 18, "navyblue" }, [18] = { 240, 100, 26, "darkblue" }, [19] = { 240, 100, 34, "blue3" }, [20] = { 240, 100, 42, "blue3" }, [21] = { 240, 100, 50, "blue1" }, [22] = { 120, 100, 18, "darkgreen" }, [23] = { 180, 100, 18, "deepskyblue4" }, [24] = { 97, 100, 26, "deepskyblue4" }, [25] = { 7, 100, 34, "deepskyblue4" }, [26] = { 13, 100, 42, "dodgerblue3" }, [27] = { 17, 100, 50, "dodgerblue2" }, [28] = { 120, 100, 26, "green4" }, [29] = { 62, 100, 26, "springgreen4" }, [30] = { 180, 100, 26, "turquoise4" }, [31] = { 93, 100, 34, "deepskyblue3" }, [32] = { 2, 100, 42, "deepskyblue3" }, [33] = { 8, 100, 50, "dodgerblue1" }, [34] = { 120, 100, 34, "green3" }, [35] = { 52, 100, 34, "springgreen3" }, [36] = { 66, 100, 34, "darkcyan" }, [37] = { 180, 100, 34, "lightseagreen" }, [38] = { 91, 100, 42, "deepskyblue2" }, [39] = { 98, 100, 50, "deepskyblue1" }, [40] = { 120, 100, 42, "green3" }, [41] = { 46, 100, 42, "springgreen3" }, [42] = { 57, 100, 42, "springgreen2" }, [43] = { 68, 100, 42, "cyan3" }, [44] = { 180, 100, 42, "darkturquoise" }, [45] = { 89, 100, 50, "turquoise2" }, [46] = { 120, 100, 50, "green1" }, [47] = { 42, 100, 50, "springgreen2" }, [48] = { 51, 100, 50, "springgreen1" }, [49] = { 61, 100, 50, "mediumspringgreen" }, [50] = { 70, 100, 50, "cyan2" }, [51] = { 180, 100, 50, "cyan1" }, [52] = { 0, 100, 18, "darkred" }, [53] = { 300, 100, 18, "deeppink4" }, [54] = { 82, 100, 26, "purple4" }, [55] = { 72, 100, 34, "purple4" }, [56] = { 66, 100, 42, "purple3" }, [57] = { 62, 100, 50, "blueviolet" }, [58] = { 60, 100, 18, "orange4" }, [59] = { 0, 0, 37, "grey37" }, [60] = { 240, 17, 45, "mediumpurple4" }, [61] = { 240, 33, 52, "slateblue3" }, [62] = { 240, 60, 60, "slateblue3" }, [63] = { 240, 100, 68, "royalblue1" }, [64] = { 7, 100, 26, "chartreuse4" }, [65] = { 120, 17, 45, "darkseagreen4" }, [66] = { 180, 17, 45, "paleturquoise4" }, [67] = { 210, 33, 52, "steelblue" }, [68] = { 220, 60, 60, "steelblue3" }, [69] = { 225, 100, 68, "cornflowerblue" }, [70] = { 7, 100, 34, "chartreuse3" }, [71] = { 120, 33, 52, "darkseagreen4" }, [72] = { 150, 33, 52, "cadetblue" }, [73] = { 180, 33, 52, "cadetblue" }, [74] = { 200, 60, 60, "skyblue3" }, [75] = { 210, 100, 68, "steelblue1" }, [76] = { 3, 100, 42, "chartreuse3" }, [77] = { 120, 60, 60, "palegreen3" }, [78] = { 140, 60, 60, "seagreen3" }, [79] = { 160, 60, 60, "aquamarine3" }, [80] = { 180, 60, 60, "mediumturquoise" }, [81] = { 195, 100, 68, "steelblue1" }, [82] = { 7, 100, 50, "chartreuse2" }, [83] = { 120, 100, 68, "seagreen2" }, [84] = { 135, 100, 68, "seagreen1" }, [85] = { 150, 100, 68, "seagreen1" }, [86] = { 165, 100, 68, "aquamarine1" }, [87] = { 180, 100, 68, "darkslategray2" }, [88] = { 0, 100, 26, "darkred" }, [89] = { 17, 100, 26, "deeppink4" }, [90] = { 300, 100, 26, "darkmagenta" }, [91] = { 86, 100, 34, "darkmagenta" }, [92] = { 77, 100, 42, "darkviolet" }, [93] = { 71, 100, 50, "purple" }, [94] = { 2, 100, 26, "orange4" }, [95] = { 0, 17, 45, "lightpink4" }, [96] = { 300, 17, 45, "plum4" }, [97] = { 270, 33, 52, "mediumpurple3" }, [98] = { 260, 60, 60, "mediumpurple3" }, [99] = { 255, 100, 68, "slateblue1" }, [100] = { 60, 100, 26, "yellow4" }, [101] = { 60, 17, 45, "wheat4" }, [102] = { 0, 0, 52, "grey53" }, [103] = { 240, 20, 60, "lightslategrey" }, [104] = { 240, 50, 68, "mediumpurple" }, [105] = { 240, 100, 76, "lightslateblue" }, [106] = { 3, 100, 34, "yellow4" }, [107] = { 90, 33, 52, "darkolivegreen3" }, [108] = { 120, 20, 60, "darkseagreen" }, [109] = { 180, 20, 60, "lightskyblue3" }, [110] = { 210, 50, 68, "lightskyblue3" }, [111] = { 220, 100, 76, "skyblue2" }, [112] = { 2, 100, 42, "chartreuse2" }, [113] = { 100, 60, 60, "darkolivegreen3" }, [114] = { 120, 50, 68, "palegreen3" }, [115] = { 150, 50, 68, "darkseagreen3" }, [116] = { 180, 50, 68, "darkslategray3" }, [117] = { 200, 100, 76, "skyblue1" }, [118] = { 8, 100, 50, "chartreuse1" }, [119] = { 105, 100, 68, "lightgreen" }, [120] = { 120, 100, 76, "lightgreen" }, [121] = { 140, 100, 76, "palegreen1" }, [122] = { 160, 100, 76, "aquamarine1" }, [123] = { 180, 100, 76, "darkslategray1" }, [124] = { 0, 100, 34, "red3" }, [125] = { 27, 100, 34, "deeppink4" }, [126] = { 13, 100, 34, "mediumvioletred" }, [127] = { 300, 100, 34, "magenta3" }, [128] = { 88, 100, 42, "darkviolet" }, [129] = { 81, 100, 50, "purple" }, [130] = { 2, 100, 34, "darkorange3" }, [131] = { 0, 33, 52, "indianred" }, [132] = { 330, 33, 52, "hotpink3" }, [133] = { 300, 33, 52, "mediumorchid3" }, [134] = { 280, 60, 60, "mediumorchid" }, [135] = { 270, 100, 68, "mediumpurple2" }, [136] = { 6, 100, 34, "darkgoldenrod" }, [137] = { 30, 33, 52, "lightsalmon3" }, [138] = { 0, 20, 60, "rosybrown" }, [139] = { 300, 20, 60, "grey63" }, [140] = { 270, 50, 68, "mediumpurple2" }, [141] = { 260, 100, 76, "mediumpurple1" }, [142] = { 60, 100, 34, "gold3" }, [143] = { 60, 33, 52, "darkkhaki" }, [144] = { 60, 20, 60, "navajowhite3" }, [145] = { 0, 0, 68, "grey69" }, [146] = { 240, 33, 76, "lightsteelblue3" }, [147] = { 240, 100, 84, "lightsteelblue" }, [148] = { 1, 100, 42, "yellow3" }, [149] = { 80, 60, 60, "darkolivegreen3" }, [150] = { 90, 50, 68, "darkseagreen3" }, [151] = { 120, 33, 76, "darkseagreen2" }, [152] = { 180, 33, 76, "lightcyan3" }, [153] = { 210, 100, 84, "lightskyblue1" }, [154] = { 8, 100, 50, "greenyellow" }, [155] = { 90, 100, 68, "darkolivegreen2" }, [156] = { 100, 100, 76, "palegreen1" }, [157] = { 120, 100, 84, "darkseagreen2" }, [158] = { 150, 100, 84, "darkseagreen1" }, [159] = { 180, 100, 84, "paleturquoise1" }, [160] = { 0, 100, 42, "red3" }, [161] = { 33, 100, 42, "deeppink3" }, [162] = { 22, 100, 42, "deeppink3" }, [163] = { 11, 100, 42, "magenta3" }, [164] = { 300, 100, 42, "magenta3" }, [165] = { 90, 100, 50, "magenta2" }, [166] = { 6, 100, 42, "darkorange3" }, [167] = { 0, 60, 60, "indianred" }, [168] = { 340, 60, 60, "hotpink3" }, [169] = { 320, 60, 60, "hotpink2" }, [170] = { 300, 60, 60, "orchid" }, [171] = { 285, 100, 68, "mediumorchid1" }, [172] = { 7, 100, 42, "orange3" }, [173] = { 20, 60, 60, "lightsalmon3" }, [174] = { 0, 50, 68, "lightpink3" }, [175] = { 330, 50, 68, "pink3" }, [176] = { 300, 50, 68, "plum3" }, [177] = { 280, 100, 76, "violet" }, [178] = { 8, 100, 42, "gold3" }, [179] = { 40, 60, 60, "lightgoldenrod3" }, [180] = { 30, 50, 68, "tan" }, [181] = { 0, 33, 76, "mistyrose3" }, [182] = { 300, 33, 76, "thistle3" }, [183] = { 270, 100, 84, "plum2" }, [184] = { 60, 100, 42, "yellow3" }, [185] = { 60, 60, 60, "khaki3" }, [186] = { 60, 50, 68, "lightgoldenrod2" }, [187] = { 60, 33, 76, "lightyellow3" }, [188] = { 0, 0, 84, "grey84" }, [189] = { 240, 100, 92, "lightsteelblue1" }, [190] = { 9, 100, 50, "yellow2" }, [191] = { 75, 100, 68, "darkolivegreen1" }, [192] = { 80, 100, 76, "darkolivegreen1" }, [193] = { 90, 100, 84, "darkseagreen1" }, [194] = { 120, 100, 92, "honeydew2" }, [195] = { 180, 100, 92, "lightcyan1" }, [196] = { 0, 100, 50, "red1" }, [197] = { 37, 100, 50, "deeppink2" }, [198] = { 28, 100, 50, "deeppink1" }, [199] = { 18, 100, 50, "deeppink1" }, [200] = { 9, 100, 50, "magenta2" }, [201] = { 300, 100, 50, "magenta1" }, [202] = { 2, 100, 50, "orangered1" }, [203] = { 0, 100, 68, "indianred1" }, [204] = { 345, 100, 68, "indianred1" }, [205] = { 330, 100, 68, "hotpink" }, [206] = { 315, 100, 68, "hotpink" }, [207] = { 300, 100, 68, "mediumorchid1" }, [208] = { 1, 100, 50, "darkorange" }, [209] = { 15, 100, 68, "salmon1" }, [210] = { 0, 100, 76, "lightcoral" }, [211] = { 340, 100, 76, "palevioletred1" }, [212] = { 320, 100, 76, "orchid2" }, [213] = { 300, 100, 76, "orchid1" }, [214] = { 1, 100, 50, "orange1" }, [215] = { 30, 100, 68, "sandybrown" }, [216] = { 20, 100, 76, "lightsalmon1" }, [217] = { 0, 100, 84, "lightpink1" }, [218] = { 330, 100, 84, "pink1" }, [219] = { 300, 100, 84, "plum1" }, [220] = { 0, 100, 50, "gold1" }, [221] = { 45, 100, 68, "lightgoldenrod2" }, [222] = { 40, 100, 76, "lightgoldenrod2" }, [223] = { 30, 100, 84, "navajowhite1" }, [224] = { 0, 100, 92, "mistyrose1" }, [225] = { 300, 100, 92, "thistle1" }, [226] = { 60, 100, 50, "yellow1" }, [227] = { 60, 100, 68, "lightgoldenrod1" }, [228] = { 60, 100, 76, "khaki1" }, [229] = { 60, 100, 84, "wheat1" }, [230] = { 60, 100, 92, "cornsilk1" }, [231] = { 0, 0, 100, "grey100" }, [232] = { 0, 0, 3, "grey3" }, [233] = { 0, 0, 7, "grey7" }, [234] = { 0, 0, 10, "grey11" }, [235] = { 0, 0, 14, "grey15" }, [236] = { 0, 0, 18, "grey19" }, [237] = { 0, 0, 22, "grey23" }, [238] = { 0, 0, 26, "grey27" }, [239] = { 0, 0, 30, "grey30" }, [240] = { 0, 0, 34, "grey35" }, [241] = { 0, 0, 37, "grey39" }, [242] = { 0, 0, 40, "grey42" }, [243] = { 0, 0, 46, "grey46" }, [244] = { 0, 0, 50, "grey50" }, [245] = { 0, 0, 54, "grey54" }, [246] = { 0, 0, 58, "grey58" }, [247] = { 0, 0, 61, "grey62" }, [248] = { 0, 0, 65, "grey66" }, [249] = { 0, 0, 69, "grey70" }, [250] = { 0, 0, 73, "grey74" }, [251] = { 0, 0, 77, "grey78" }, [252] = { 0, 0, 81, "grey82" }, [253] = { 0, 0, 85, "grey85" }, [254] = { 0, 0, 89, "grey89" }, [255] = { 0, 0, 93, "grey93" }, }; /* -1 is valid curses color */ #define COL_ERR -2 static inline int color_distance(const struct color_def* a, const struct color_def* b) { int h = MIN((a->h - b->h) % 360, (b->h - a->h) % 360); int s = (int)a->s - b->s; int l = (int)a->l - b->l; return h * h + s * s + l * l; } static int find_closest_col(int h, int s, int l) { struct color_def a = { h, s, l }; int min = 0; int dmin = color_distance(&a, &color_names[0]); for (int i = 1; i < COLOR_NAME_SIZE; i++) { int d = color_distance(&a, &color_names[i]); if (d < dmin) { dmin = d; min = i; } } return min; } static int find_col(const char* col_name, int n) { char name[32] = { 0 }; /* * make a null terminated version of col_name. we don't want to * use strNcasecmp because we could end up matching blue3 with * blue. */ if (n >= sizeof(name)) { /* truncate */ log_error("Color: <%s,%d> bigger than %zu", col_name, n, sizeof(name)); n = sizeof(name) - 1; } memcpy(name, col_name, n); if (g_ascii_strcasecmp(name, "default") == 0) { return -1; } for (int i = 0; i < COLOR_NAME_SIZE; i++) { if (g_ascii_strcasecmp(name, color_names[i].name) == 0) { return i; } } return COL_ERR; } static int color_hash(const char* str, color_profile profile) { GChecksum* cs = NULL; guint8 buf[256] = { 0 }; gsize len = 256; int rc = -1; /* default ncurse color */ cs = g_checksum_new(G_CHECKSUM_SHA1); if (!cs) goto out; g_checksum_update(cs, (guint8*)str, strlen(str)); g_checksum_get_digest(cs, buf, &len); // sha1 should be 20 bytes if (len != 20) goto out; double h = ((buf[1] << 8) | buf[0]) / 65536. * 360.; switch (profile) { case COLOR_PROFILE_REDGREEN_BLINDNESS: // red/green blindness correction h = fmod(fmod(h + 90., 180) - 90., 360.); break; case COLOR_PROFILE_BLUE_BLINDNESS: // blue blindness correction h = fmod(h, 180.); default: break; } rc = find_closest_col((int)h, 100, 50); out: g_checksum_free(cs); return rc; } void color_pair_cache_reset(void) { if (cache.pairs) { free(cache.pairs); memset(&cache, 0, sizeof(cache)); } /* * COLOR_PAIRS is actually not a macro and is thus not a * compile-time constant */ cache.capacity = COLOR_PAIRS; /* when we run unit tests COLOR_PAIRS will be -1 */ if (cache.capacity < 0) cache.capacity = 8; cache.pairs = g_malloc0(sizeof(*cache.pairs) * cache.capacity); if (cache.pairs) { /* default_default */ cache.pairs[0].fg = -1; cache.pairs[0].bg = -1; cache.size = 1; } else { log_error("Color: unable to allocate memory"); } } static int _color_pair_cache_get(int fg, int bg) { if (COLORS < 256) { if (fg > 7 || bg > 7) { log_error("Color: trying to load 256 colour theme without capable terminal"); return -1; } } /* try to find pair in cache */ for (int i = 0; i < cache.size; i++) { if (fg == cache.pairs[i].fg && bg == cache.pairs[i].bg) { return i; } } /* otherwise cache new pair */ if (cache.size >= cache.capacity) { log_error("Color: reached ncurses color pair cache of %d (COLOR_PAIRS=%d)", cache.capacity, COLOR_PAIRS); return -1; } int i = cache.size; cache.pairs[i].fg = fg; cache.pairs[i].bg = bg; /* (re-)define the new pair in curses */ init_pair(i, fg, bg); cache.size++; return i; } /** * color_pair_cache_hash_str - hash string to a color pair curses id * * Implements XEP-0392 ("Consistent Color Generation") as best as * possible given a 256 colors terminal. * * hash a string into a color that will be used as fg * check for 'bkgnd' in theme file or use default color as bg */ int color_pair_cache_hash_str(const char* str, color_profile profile) { int fg = color_hash(str, profile); int bg = -1; char* bkgnd = theme_get_bkgnd(); if (bkgnd) { bg = find_col(bkgnd, strlen(bkgnd)); free(bkgnd); } return _color_pair_cache_get(fg, bg); } /** * color_pair_cache_get - parse color pair "fg_bg" and returns curses id * * if the pair doesn't exist it will allocate it in curses with init_pair * if the pair exists it returns its id */ int color_pair_cache_get(const char* pair_name) { const char* sep; int fg, bg; sep = strchr(pair_name, '_'); if (!sep) { log_error("Color: color pair %s missing", pair_name); return -1; } fg = find_col(pair_name, sep - pair_name); bg = find_col(sep + 1, strlen(sep)); if (fg == COL_ERR || bg == COL_ERR) { log_error("Color: bad color name %s", pair_name); return -1; } return _color_pair_cache_get(fg, bg); }