1 /*
2  * color.c
3  * vim: expandtab:ts=4:sts=4:sw=4
4  *
5  * Copyright (C) 2019 Aurelien Aptel <aurelien.aptel@gmail.com>
6  * Copyright (C) 2019 - 2021 Michael Vetter <jubalh@iodoru.org>
7  *
8  * This file is part of Profanity.
9  *
10  * Profanity is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * Profanity is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
22  *
23  * In addition, as a special exception, the copyright holders give permission to
24  * link the code of portions of this program with the OpenSSL library under
25  * certain conditions as described in each individual source file, and
26  * distribute linked combinations including the two.
27  *
28  * You must obey the GNU General Public License in all respects for all of the
29  * code used other than OpenSSL. If you modify file(s) with this exception, you
30  * may extend this exception to your version of the file(s), but you are not
31  * obligated to do so. If you do not wish to do so, delete this exception
32  * statement from your version. If you delete this exception statement from all
33  * source files in the program, then also delete it here.
34  *
35  */
36 
37 #include "config.h"
38 
39 #include <stdlib.h>
40 #include <string.h>
41 #include <stdint.h>
42 #include <math.h>
43 #include <glib.h>
44 
45 #ifdef HAVE_NCURSESW_NCURSES_H
46 #include <ncursesw/ncurses.h>
47 #elif HAVE_NCURSES_H
48 #include <ncurses.h>
49 #elif HAVE_CURSES_H
50 #include <curses.h>
51 #endif
52 
53 #include "config/color.h"
54 #include "config/theme.h"
55 #include "log.h"
56 
57 static struct color_pair_cache
58 {
59     struct
60     {
61         int16_t fg, bg;
62     } * pairs;
63     int size;
64     int capacity;
65 } cache = { 0 };
66 
67 /*
68  * xterm default 256 colors
69  * XXX: there are many duplicates... (eg blue3)
70  */
71 
72 const struct color_def color_names[COLOR_NAME_SIZE] = {
73     [0] = { 0, 0, 0, "black" },
74     [1] = { 0, 100, 25, "red" },
75     [2] = { 120, 100, 25, "green" },
76     [3] = { 60, 100, 25, "yellow" },
77     [4] = { 240, 100, 25, "blue" },
78     [5] = { 300, 100, 25, "magenta" },
79     [6] = { 180, 100, 25, "cyan" },
80     [7] = { 0, 0, 75, "white" },
81     [8] = { 0, 0, 50, "lightblack" },
82     [9] = { 0, 100, 50, "lightred" },
83     [10] = { 120, 100, 50, "lightgreen" },
84     [11] = { 60, 100, 50, "lightyellow" },
85     [12] = { 240, 100, 50, "lightblue" },
86     [13] = { 300, 100, 50, "lightmagenta" },
87     [14] = { 180, 100, 50, "lightcyan" },
88     [15] = { 0, 0, 100, "lightwhite" },
89     [16] = { 0, 0, 0, "grey0" },
90     [17] = { 240, 100, 18, "navyblue" },
91     [18] = { 240, 100, 26, "darkblue" },
92     [19] = { 240, 100, 34, "blue3" },
93     [20] = { 240, 100, 42, "blue3" },
94     [21] = { 240, 100, 50, "blue1" },
95     [22] = { 120, 100, 18, "darkgreen" },
96     [23] = { 180, 100, 18, "deepskyblue4" },
97     [24] = { 97, 100, 26, "deepskyblue4" },
98     [25] = { 7, 100, 34, "deepskyblue4" },
99     [26] = { 13, 100, 42, "dodgerblue3" },
100     [27] = { 17, 100, 50, "dodgerblue2" },
101     [28] = { 120, 100, 26, "green4" },
102     [29] = { 62, 100, 26, "springgreen4" },
103     [30] = { 180, 100, 26, "turquoise4" },
104     [31] = { 93, 100, 34, "deepskyblue3" },
105     [32] = { 2, 100, 42, "deepskyblue3" },
106     [33] = { 8, 100, 50, "dodgerblue1" },
107     [34] = { 120, 100, 34, "green3" },
108     [35] = { 52, 100, 34, "springgreen3" },
109     [36] = { 66, 100, 34, "darkcyan" },
110     [37] = { 180, 100, 34, "lightseagreen" },
111     [38] = { 91, 100, 42, "deepskyblue2" },
112     [39] = { 98, 100, 50, "deepskyblue1" },
113     [40] = { 120, 100, 42, "green3" },
114     [41] = { 46, 100, 42, "springgreen3" },
115     [42] = { 57, 100, 42, "springgreen2" },
116     [43] = { 68, 100, 42, "cyan3" },
117     [44] = { 180, 100, 42, "darkturquoise" },
118     [45] = { 89, 100, 50, "turquoise2" },
119     [46] = { 120, 100, 50, "green1" },
120     [47] = { 42, 100, 50, "springgreen2" },
121     [48] = { 51, 100, 50, "springgreen1" },
122     [49] = { 61, 100, 50, "mediumspringgreen" },
123     [50] = { 70, 100, 50, "cyan2" },
124     [51] = { 180, 100, 50, "cyan1" },
125     [52] = { 0, 100, 18, "darkred" },
126     [53] = { 300, 100, 18, "deeppink4" },
127     [54] = { 82, 100, 26, "purple4" },
128     [55] = { 72, 100, 34, "purple4" },
129     [56] = { 66, 100, 42, "purple3" },
130     [57] = { 62, 100, 50, "blueviolet" },
131     [58] = { 60, 100, 18, "orange4" },
132     [59] = { 0, 0, 37, "grey37" },
133     [60] = { 240, 17, 45, "mediumpurple4" },
134     [61] = { 240, 33, 52, "slateblue3" },
135     [62] = { 240, 60, 60, "slateblue3" },
136     [63] = { 240, 100, 68, "royalblue1" },
137     [64] = { 7, 100, 26, "chartreuse4" },
138     [65] = { 120, 17, 45, "darkseagreen4" },
139     [66] = { 180, 17, 45, "paleturquoise4" },
140     [67] = { 210, 33, 52, "steelblue" },
141     [68] = { 220, 60, 60, "steelblue3" },
142     [69] = { 225, 100, 68, "cornflowerblue" },
143     [70] = { 7, 100, 34, "chartreuse3" },
144     [71] = { 120, 33, 52, "darkseagreen4" },
145     [72] = { 150, 33, 52, "cadetblue" },
146     [73] = { 180, 33, 52, "cadetblue" },
147     [74] = { 200, 60, 60, "skyblue3" },
148     [75] = { 210, 100, 68, "steelblue1" },
149     [76] = { 3, 100, 42, "chartreuse3" },
150     [77] = { 120, 60, 60, "palegreen3" },
151     [78] = { 140, 60, 60, "seagreen3" },
152     [79] = { 160, 60, 60, "aquamarine3" },
153     [80] = { 180, 60, 60, "mediumturquoise" },
154     [81] = { 195, 100, 68, "steelblue1" },
155     [82] = { 7, 100, 50, "chartreuse2" },
156     [83] = { 120, 100, 68, "seagreen2" },
157     [84] = { 135, 100, 68, "seagreen1" },
158     [85] = { 150, 100, 68, "seagreen1" },
159     [86] = { 165, 100, 68, "aquamarine1" },
160     [87] = { 180, 100, 68, "darkslategray2" },
161     [88] = { 0, 100, 26, "darkred" },
162     [89] = { 17, 100, 26, "deeppink4" },
163     [90] = { 300, 100, 26, "darkmagenta" },
164     [91] = { 86, 100, 34, "darkmagenta" },
165     [92] = { 77, 100, 42, "darkviolet" },
166     [93] = { 71, 100, 50, "purple" },
167     [94] = { 2, 100, 26, "orange4" },
168     [95] = { 0, 17, 45, "lightpink4" },
169     [96] = { 300, 17, 45, "plum4" },
170     [97] = { 270, 33, 52, "mediumpurple3" },
171     [98] = { 260, 60, 60, "mediumpurple3" },
172     [99] = { 255, 100, 68, "slateblue1" },
173     [100] = { 60, 100, 26, "yellow4" },
174     [101] = { 60, 17, 45, "wheat4" },
175     [102] = { 0, 0, 52, "grey53" },
176     [103] = { 240, 20, 60, "lightslategrey" },
177     [104] = { 240, 50, 68, "mediumpurple" },
178     [105] = { 240, 100, 76, "lightslateblue" },
179     [106] = { 3, 100, 34, "yellow4" },
180     [107] = { 90, 33, 52, "darkolivegreen3" },
181     [108] = { 120, 20, 60, "darkseagreen" },
182     [109] = { 180, 20, 60, "lightskyblue3" },
183     [110] = { 210, 50, 68, "lightskyblue3" },
184     [111] = { 220, 100, 76, "skyblue2" },
185     [112] = { 2, 100, 42, "chartreuse2" },
186     [113] = { 100, 60, 60, "darkolivegreen3" },
187     [114] = { 120, 50, 68, "palegreen3" },
188     [115] = { 150, 50, 68, "darkseagreen3" },
189     [116] = { 180, 50, 68, "darkslategray3" },
190     [117] = { 200, 100, 76, "skyblue1" },
191     [118] = { 8, 100, 50, "chartreuse1" },
192     [119] = { 105, 100, 68, "lightgreen" },
193     [120] = { 120, 100, 76, "lightgreen" },
194     [121] = { 140, 100, 76, "palegreen1" },
195     [122] = { 160, 100, 76, "aquamarine1" },
196     [123] = { 180, 100, 76, "darkslategray1" },
197     [124] = { 0, 100, 34, "red3" },
198     [125] = { 27, 100, 34, "deeppink4" },
199     [126] = { 13, 100, 34, "mediumvioletred" },
200     [127] = { 300, 100, 34, "magenta3" },
201     [128] = { 88, 100, 42, "darkviolet" },
202     [129] = { 81, 100, 50, "purple" },
203     [130] = { 2, 100, 34, "darkorange3" },
204     [131] = { 0, 33, 52, "indianred" },
205     [132] = { 330, 33, 52, "hotpink3" },
206     [133] = { 300, 33, 52, "mediumorchid3" },
207     [134] = { 280, 60, 60, "mediumorchid" },
208     [135] = { 270, 100, 68, "mediumpurple2" },
209     [136] = { 6, 100, 34, "darkgoldenrod" },
210     [137] = { 30, 33, 52, "lightsalmon3" },
211     [138] = { 0, 20, 60, "rosybrown" },
212     [139] = { 300, 20, 60, "grey63" },
213     [140] = { 270, 50, 68, "mediumpurple2" },
214     [141] = { 260, 100, 76, "mediumpurple1" },
215     [142] = { 60, 100, 34, "gold3" },
216     [143] = { 60, 33, 52, "darkkhaki" },
217     [144] = { 60, 20, 60, "navajowhite3" },
218     [145] = { 0, 0, 68, "grey69" },
219     [146] = { 240, 33, 76, "lightsteelblue3" },
220     [147] = { 240, 100, 84, "lightsteelblue" },
221     [148] = { 1, 100, 42, "yellow3" },
222     [149] = { 80, 60, 60, "darkolivegreen3" },
223     [150] = { 90, 50, 68, "darkseagreen3" },
224     [151] = { 120, 33, 76, "darkseagreen2" },
225     [152] = { 180, 33, 76, "lightcyan3" },
226     [153] = { 210, 100, 84, "lightskyblue1" },
227     [154] = { 8, 100, 50, "greenyellow" },
228     [155] = { 90, 100, 68, "darkolivegreen2" },
229     [156] = { 100, 100, 76, "palegreen1" },
230     [157] = { 120, 100, 84, "darkseagreen2" },
231     [158] = { 150, 100, 84, "darkseagreen1" },
232     [159] = { 180, 100, 84, "paleturquoise1" },
233     [160] = { 0, 100, 42, "red3" },
234     [161] = { 33, 100, 42, "deeppink3" },
235     [162] = { 22, 100, 42, "deeppink3" },
236     [163] = { 11, 100, 42, "magenta3" },
237     [164] = { 300, 100, 42, "magenta3" },
238     [165] = { 90, 100, 50, "magenta2" },
239     [166] = { 6, 100, 42, "darkorange3" },
240     [167] = { 0, 60, 60, "indianred" },
241     [168] = { 340, 60, 60, "hotpink3" },
242     [169] = { 320, 60, 60, "hotpink2" },
243     [170] = { 300, 60, 60, "orchid" },
244     [171] = { 285, 100, 68, "mediumorchid1" },
245     [172] = { 7, 100, 42, "orange3" },
246     [173] = { 20, 60, 60, "lightsalmon3" },
247     [174] = { 0, 50, 68, "lightpink3" },
248     [175] = { 330, 50, 68, "pink3" },
249     [176] = { 300, 50, 68, "plum3" },
250     [177] = { 280, 100, 76, "violet" },
251     [178] = { 8, 100, 42, "gold3" },
252     [179] = { 40, 60, 60, "lightgoldenrod3" },
253     [180] = { 30, 50, 68, "tan" },
254     [181] = { 0, 33, 76, "mistyrose3" },
255     [182] = { 300, 33, 76, "thistle3" },
256     [183] = { 270, 100, 84, "plum2" },
257     [184] = { 60, 100, 42, "yellow3" },
258     [185] = { 60, 60, 60, "khaki3" },
259     [186] = { 60, 50, 68, "lightgoldenrod2" },
260     [187] = { 60, 33, 76, "lightyellow3" },
261     [188] = { 0, 0, 84, "grey84" },
262     [189] = { 240, 100, 92, "lightsteelblue1" },
263     [190] = { 9, 100, 50, "yellow2" },
264     [191] = { 75, 100, 68, "darkolivegreen1" },
265     [192] = { 80, 100, 76, "darkolivegreen1" },
266     [193] = { 90, 100, 84, "darkseagreen1" },
267     [194] = { 120, 100, 92, "honeydew2" },
268     [195] = { 180, 100, 92, "lightcyan1" },
269     [196] = { 0, 100, 50, "red1" },
270     [197] = { 37, 100, 50, "deeppink2" },
271     [198] = { 28, 100, 50, "deeppink1" },
272     [199] = { 18, 100, 50, "deeppink1" },
273     [200] = { 9, 100, 50, "magenta2" },
274     [201] = { 300, 100, 50, "magenta1" },
275     [202] = { 2, 100, 50, "orangered1" },
276     [203] = { 0, 100, 68, "indianred1" },
277     [204] = { 345, 100, 68, "indianred1" },
278     [205] = { 330, 100, 68, "hotpink" },
279     [206] = { 315, 100, 68, "hotpink" },
280     [207] = { 300, 100, 68, "mediumorchid1" },
281     [208] = { 1, 100, 50, "darkorange" },
282     [209] = { 15, 100, 68, "salmon1" },
283     [210] = { 0, 100, 76, "lightcoral" },
284     [211] = { 340, 100, 76, "palevioletred1" },
285     [212] = { 320, 100, 76, "orchid2" },
286     [213] = { 300, 100, 76, "orchid1" },
287     [214] = { 1, 100, 50, "orange1" },
288     [215] = { 30, 100, 68, "sandybrown" },
289     [216] = { 20, 100, 76, "lightsalmon1" },
290     [217] = { 0, 100, 84, "lightpink1" },
291     [218] = { 330, 100, 84, "pink1" },
292     [219] = { 300, 100, 84, "plum1" },
293     [220] = { 0, 100, 50, "gold1" },
294     [221] = { 45, 100, 68, "lightgoldenrod2" },
295     [222] = { 40, 100, 76, "lightgoldenrod2" },
296     [223] = { 30, 100, 84, "navajowhite1" },
297     [224] = { 0, 100, 92, "mistyrose1" },
298     [225] = { 300, 100, 92, "thistle1" },
299     [226] = { 60, 100, 50, "yellow1" },
300     [227] = { 60, 100, 68, "lightgoldenrod1" },
301     [228] = { 60, 100, 76, "khaki1" },
302     [229] = { 60, 100, 84, "wheat1" },
303     [230] = { 60, 100, 92, "cornsilk1" },
304     [231] = { 0, 0, 100, "grey100" },
305     [232] = { 0, 0, 3, "grey3" },
306     [233] = { 0, 0, 7, "grey7" },
307     [234] = { 0, 0, 10, "grey11" },
308     [235] = { 0, 0, 14, "grey15" },
309     [236] = { 0, 0, 18, "grey19" },
310     [237] = { 0, 0, 22, "grey23" },
311     [238] = { 0, 0, 26, "grey27" },
312     [239] = { 0, 0, 30, "grey30" },
313     [240] = { 0, 0, 34, "grey35" },
314     [241] = { 0, 0, 37, "grey39" },
315     [242] = { 0, 0, 40, "grey42" },
316     [243] = { 0, 0, 46, "grey46" },
317     [244] = { 0, 0, 50, "grey50" },
318     [245] = { 0, 0, 54, "grey54" },
319     [246] = { 0, 0, 58, "grey58" },
320     [247] = { 0, 0, 61, "grey62" },
321     [248] = { 0, 0, 65, "grey66" },
322     [249] = { 0, 0, 69, "grey70" },
323     [250] = { 0, 0, 73, "grey74" },
324     [251] = { 0, 0, 77, "grey78" },
325     [252] = { 0, 0, 81, "grey82" },
326     [253] = { 0, 0, 85, "grey85" },
327     [254] = { 0, 0, 89, "grey89" },
328     [255] = { 0, 0, 93, "grey93" },
329 };
330 
331 /* -1 is valid curses color */
332 #define COL_ERR -2
333 
334 static inline int
color_distance(const struct color_def * a,const struct color_def * b)335 color_distance(const struct color_def* a, const struct color_def* b)
336 {
337     int h = MIN((a->h - b->h) % 360, (b->h - a->h) % 360);
338     int s = (int)a->s - b->s;
339     int l = (int)a->l - b->l;
340     return h * h + s * s + l * l;
341 }
342 
343 static int
find_closest_col(int h,int s,int l)344 find_closest_col(int h, int s, int l)
345 {
346     struct color_def a = { h, s, l };
347     int min = 0;
348     int dmin = color_distance(&a, &color_names[0]);
349 
350     for (int i = 1; i < COLOR_NAME_SIZE; i++) {
351         int d = color_distance(&a, &color_names[i]);
352         if (d < dmin) {
353             dmin = d;
354             min = i;
355         }
356     }
357     return min;
358 }
359 
360 static int
find_col(const char * col_name,int n)361 find_col(const char* col_name, int n)
362 {
363     char name[32] = { 0 };
364 
365     /*
366      * make a null terminated version of col_name. we don't want to
367      * use strNcasecmp because we could end up matching blue3 with
368      * blue.
369      */
370 
371     if (n >= sizeof(name)) {
372         /* truncate */
373         log_error("Color: <%s,%d> bigger than %zu", col_name, n, sizeof(name));
374         n = sizeof(name) - 1;
375     }
376     memcpy(name, col_name, n);
377 
378     if (g_ascii_strcasecmp(name, "default") == 0) {
379         return -1;
380     }
381 
382     for (int i = 0; i < COLOR_NAME_SIZE; i++) {
383         if (g_ascii_strcasecmp(name, color_names[i].name) == 0) {
384             return i;
385         }
386     }
387 
388     return COL_ERR;
389 }
390 
391 static int
color_hash(const char * str,color_profile profile)392 color_hash(const char* str, color_profile profile)
393 {
394     GChecksum* cs = NULL;
395     guint8 buf[256] = { 0 };
396     gsize len = 256;
397     int rc = -1; /* default ncurse color */
398 
399     cs = g_checksum_new(G_CHECKSUM_SHA1);
400     if (!cs)
401         goto out;
402 
403     g_checksum_update(cs, (guint8*)str, strlen(str));
404     g_checksum_get_digest(cs, buf, &len);
405 
406     // sha1 should be 20 bytes
407     if (len != 20)
408         goto out;
409 
410     double h = ((buf[1] << 8) | buf[0]) / 65536. * 360.;
411 
412     switch (profile) {
413     case COLOR_PROFILE_REDGREEN_BLINDNESS:
414         // red/green blindness correction
415         h = fmod(fmod(h + 90., 180) - 90., 360.);
416         break;
417     case COLOR_PROFILE_BLUE_BLINDNESS:
418         // blue blindness correction
419         h = fmod(h, 180.);
420     default:
421         break;
422     }
423 
424     rc = find_closest_col((int)h, 100, 50);
425 
426 out:
427     g_checksum_free(cs);
428     return rc;
429 }
430 
431 void
color_pair_cache_reset(void)432 color_pair_cache_reset(void)
433 {
434     if (cache.pairs) {
435         free(cache.pairs);
436         memset(&cache, 0, sizeof(cache));
437     }
438 
439     /*
440      * COLOR_PAIRS is actually not a macro and is thus not a
441      * compile-time constant
442      */
443     cache.capacity = COLOR_PAIRS;
444 
445     /* when we run unit tests COLOR_PAIRS will be -1 */
446     if (cache.capacity < 0)
447         cache.capacity = 8;
448 
449     cache.pairs = g_malloc0(sizeof(*cache.pairs) * cache.capacity);
450     if (cache.pairs) {
451         /* default_default */
452         cache.pairs[0].fg = -1;
453         cache.pairs[0].bg = -1;
454         cache.size = 1;
455     } else {
456         log_error("Color: unable to allocate memory");
457     }
458 }
459 
460 static int
_color_pair_cache_get(int fg,int bg)461 _color_pair_cache_get(int fg, int bg)
462 {
463     if (COLORS < 256) {
464         if (fg > 7 || bg > 7) {
465             log_error("Color: trying to load 256 colour theme without capable terminal");
466             return -1;
467         }
468     }
469 
470     /* try to find pair in cache */
471     for (int i = 0; i < cache.size; i++) {
472         if (fg == cache.pairs[i].fg && bg == cache.pairs[i].bg) {
473             return i;
474         }
475     }
476 
477     /* otherwise cache new pair */
478 
479     if (cache.size >= cache.capacity) {
480         log_error("Color: reached ncurses color pair cache of %d (COLOR_PAIRS=%d)",
481                   cache.capacity, COLOR_PAIRS);
482         return -1;
483     }
484 
485     int i = cache.size;
486     cache.pairs[i].fg = fg;
487     cache.pairs[i].bg = bg;
488     /* (re-)define the new pair in curses */
489     init_pair(i, fg, bg);
490 
491     cache.size++;
492 
493     return i;
494 }
495 
496 /**
497  * color_pair_cache_hash_str - hash string to a color pair curses id
498  *
499  * Implements XEP-0392 ("Consistent Color Generation") as best as
500  * possible given a 256 colors terminal.
501  *
502  * hash a string into a color that will be used as fg
503  * check for 'bkgnd' in theme file or use default color as bg
504  */
505 int
color_pair_cache_hash_str(const char * str,color_profile profile)506 color_pair_cache_hash_str(const char* str, color_profile profile)
507 {
508     int fg = color_hash(str, profile);
509     int bg = -1;
510 
511     char* bkgnd = theme_get_bkgnd();
512     if (bkgnd) {
513         bg = find_col(bkgnd, strlen(bkgnd));
514         free(bkgnd);
515     }
516 
517     return _color_pair_cache_get(fg, bg);
518 }
519 
520 /**
521  * color_pair_cache_get - parse color pair "fg_bg" and returns curses id
522  *
523  * if the pair doesn't exist it will allocate it in curses with init_pair
524  * if the pair exists it returns its id
525  */
526 int
color_pair_cache_get(const char * pair_name)527 color_pair_cache_get(const char* pair_name)
528 {
529     const char* sep;
530     int fg, bg;
531 
532     sep = strchr(pair_name, '_');
533     if (!sep) {
534         log_error("Color: color pair %s missing", pair_name);
535         return -1;
536     }
537 
538     fg = find_col(pair_name, sep - pair_name);
539     bg = find_col(sep + 1, strlen(sep));
540     if (fg == COL_ERR || bg == COL_ERR) {
541         log_error("Color: bad color name %s", pair_name);
542         return -1;
543     }
544 
545     return _color_pair_cache_get(fg, bg);
546 }
547