chiark / gitweb /
@@@ tty cleanup
[mLib] / ui / ttycolour.c
1 /* -*-c-*-
2  *
3  * Configurable terminal colour support
4  *
5  * (c) 2024 Straylight/Edgeware
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of the mLib utilities library.
11  *
12  * mLib is free software: you can redistribute it and/or modify it under
13  * the terms of the GNU Library General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or (at
15  * your option) any later version.
16  *
17  * mLib is distributed in the hope that it will be useful, but WITHOUT
18  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
20  * License for more details.
21  *
22  * You should have received a copy of the GNU Library General Public
23  * License along with mLib.  If not, write to the Free Software
24  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
25  * USA.
26  */
27
28 /*----- Header files ------------------------------------------------------*/
29
30 #include <ctype.h>
31 #include <string.h>
32
33 #include "macros.h"
34 #include "report.h"
35 #include "tty.h"
36 #include "ttycolour.h"
37
38 /*----- Main code ---------------------------------------------------------*/
39
40 /* --- @env_setting_p@ --- *
41  *
42  * Arguments:   @const char *var@ = environment variable name
43  *
44  * Returns:     Nonzero if the variable is set to a non-empty value,
45  *              otherwise zero.
46  *
47  * Use:         This is the recommended way to check the %|NO_COLOR|%,
48  *              `%|CLICOLOR|% and %|CLICOLOR_FORCE|% variables.
49  */
50
51 static int env_setting_p(const char *var)
52   { const char *p; p = getenv(var); return (p && *p); }
53
54 /* --- @ttycolour_enablep@ --- *
55  *
56  * Arguments:   @unsigned f@ = flags
57  *
58  * Returns:     Nonzero if colours should be applied to output, otherwise
59  *              zero.
60  *
61  * Use:         This function determines whether it's generally a good idea
62  *              to produce output in pretty colours.  Set @TCEF_TTY@ if the
63  *              output stream is -- or should be considered to be --
64  *              interactive (e.g., according to @isatty@); set @TCEF_DFLT@ if
65  *              the application prefers to produce coloured output if
66  *              possible.
67  *
68  *              The detailed behaviour is as follows.  (Since the purpose of
69  *              this function is to abide by common conventions and to be
70  *              convenient for users, these details may change in future.)
71  *
72  *                * If the %|NO_COLOR|% environment variable is non-empty,
73  *                  colour is disabled (%%\url{https://no-color.org/}%%).
74  *
75  *                * If the %|TERM|% variable is set to %|dumb|%, then colour
76  *                  is disabled (Emacs).
77  *
78  *                * If the %|FORCE_COLOR|% environment variable is non-empty,
79  *                  then colour is enabled, unless the value is 0, in which
80  *                  case colour is disabled (apparently from the Node
81  *                  community, %%\url{%%https://force-color.org/}%% and
82  *                  %%\url{https://nodejs.org/api/tty.html#writestreamgetcolordepthenv}%%).
83  *
84  *                * If the %|CLICOLOR_FORCE|% environment variable is
85  *                  non-empty, then colour is enabled (apparently from
86  *                  Mac OS, (%%\url{http://bixense.com/clicolors/}%%).
87  *
88  *                * If the @TCEF_TTY@ flag is clear, then colour is disabled.
89  *
90  *                * If the @TCEF_DFLT@ flag is set, then colour is enabled.
91  *
92  *                * If the %|CLICOLOR|% environment variable is non-empty,
93  *                  then colour is enabled (again, apparently from Mac OS,
94  *                  (%%\url{http://bixense.com/clicolors/}%%).
95  *
96  *                * Otherwise, colour is disabled.
97  */
98
99 int ttycolour_enablep(unsigned f)
100 {
101   const char *t;
102
103   if (env_setting_p("NO_COLOR")) return (0);
104   else if (t = getenv("TERM"), !t || STRCMP(t, ==, "dumb")) return (0);
105   else if (t = getenv("FORCE_COLOR"), t && *t) return (*t == '0' ? 0 : 1);
106   else if (env_setting_p("CLICOLOR_FORCE")) return (1);
107   else if (!(f&TCEF_TTY)) return (0);
108   else if ((f&TCEF_DFLT) || env_setting_p("CLICOLOR")) return (1);
109   else return (0);
110 }
111
112
113
114 int ttycolour_config(struct tty_attr *attr, const char *user, unsigned f,
115                      struct tty *tty, const struct ttycolour_style *tab)
116 {
117   const char *p, *q;
118   const struct tty_attrlist *aa;
119   size_t n;
120   unsigned i, arg, a, fg, bg, st, spc, clr;
121   int rc = 0;
122
123 #define ST_BASE 0u
124 #define ST_EXT 1u
125 #define ST_CLR 2u
126 #define ST_RED 3u
127 #define ST_GREEN 4u
128 #define ST_BLUE 5u
129 #define ST_MASK 0x7fu
130 #define ST_FG 0x00u
131 #define ST_BG 0x80u
132
133 #define SETAFIELD(f, v) (a = (a&~TTAF_##f##MASK) | ((v) << TTAF_##f##SHIFT))
134
135   for (i = 0; tab[i].tok; i++) {
136     aa = tab[i].dflt;
137     if (aa) {
138       for (;; aa++)
139         if ((tty->acaps&aa->cap_mask) == aa->cap_eq)
140           { attr[i] = aa->attr; goto next_token; }
141         else if (!aa->cap_mask)
142           break;
143     }
144     attr[i].f = attr[i]._res0 = attr[i].fg = attr[i].bg = 0;
145   next_token:;
146   }
147
148   if (f&TCIF_GETENV) p = getenv(user);
149   else p = user;
150
151   if (p)
152     while (*p) {
153       n = strcspn(p, "=:"); q = p + n;
154       if (!*q || *q == ':') {
155         if (f&TCIF_REPORT) moan("missing colour token in `%s'", user);
156         rc = -1; p = *q ? q + 1 : q; continue;
157       }
158       for (i = 0; tab[i].tok; i++)
159         if (STRNCMP(p, ==, tab[i].tok, n) && !tab[i].tok[n])
160           goto found_tok;
161       if (f&TCIF_REPORT)
162         moan("unknown colour token `%.*s' in `%s'", (int)n, p, user);
163       rc = -1; q = p + strcspn(p, ":"); p = *q ? q + 1 : q; continue;
164
165     found_tok:
166       a = fg = bg = 0; st = ST_BASE; arg = 0; clr = 0; q++;
167       for (;;) {
168
169         if (ISDIGIT(*q))
170           { arg = 10*arg + (*q++ - '0'); continue; }
171
172         switch (st&ST_MASK) {
173           case ST_BASE:
174             switch (arg) {
175               case 0: a = fg = bg = 0; break;
176               case 1: SETAFIELD(WT, TTWT_BOLD); break;
177               case 2: SETAFIELD(WT, TTWT_DIM); break;
178               case 3: a |= TTAF_ITAL; break;
179               case 4: SETAFIELD(LN, TTLN_ULINE); break;
180               case 7: a |= TTAF_INVV; break;
181               case 21: SETAFIELD(LN, TTLN_UULINE); break;
182               case 22: a &= ~TTAF_WTMASK; break;
183               case 23: a &= ~TTAF_ITAL; break;
184               case 24: a &= ~TTAF_LNMASK; break;
185               case 27: a &= ~TTAF_INVV; break;
186
187               case 30: case 31: case 32: case 33:
188               case 34: case 35: case 36: case 37:
189                 SETAFIELD(FGSPC, TTCSPC_1BPC);
190                 fg = arg - 30;
191                 break;
192               case 90: case 91: case 92: case 93:
193               case 94: case 95: case 96: case 97:
194                 SETAFIELD(FGSPC, TTCSPC_1BPCBR);
195                 fg = (arg - 90) | TT1BPC_BRI;
196                 break;
197               case 38: st = ST_EXT | ST_FG; break;
198               case 39: SETAFIELD(FGSPC, TTCSPC_NONE); break;
199
200               case 40: case 41: case 42: case 43:
201               case 44: case 45: case 46: case 47:
202                 SETAFIELD(BGSPC, TTCSPC_1BPC);
203                 bg = arg - 40;
204                 break;
205               case 100: case 101: case 102: case 103:
206               case 104: case 105: case 106: case 107:
207                 SETAFIELD(BGSPC, TTCSPC_1BPCBR);
208                 fg = (arg - 100) | TT1BPC_BRI;
209                 break;
210               case 48: st = ST_EXT | ST_BG; break;
211               case 49: SETAFIELD(BGSPC, TTCSPC_NONE); break;
212
213               default:
214                 if (f&TCIF_REPORT)
215                   moan("unknown colour code %u in `%.*s' string in `%s'",
216                        arg, (int)n, p, user);
217                 rc = -1; break;
218             }
219             break;
220
221           case ST_EXT:
222             switch (arg) {
223               case 2: st = (st&~ST_MASK) | ST_RED; break;
224               case 5: st = (st&~ST_MASK) | ST_CLR; break;
225               default:
226                 if (f&TCIF_REPORT)
227                   moan("unknown extended colour space %u "
228                        "in `%.*s' string in `%s'",
229                        arg, (int)n, p, user);
230                 st = ST_BASE; rc = -1; break;
231             }
232             break;
233
234           case ST_CLR:
235             if (arg < 8)
236               { spc = TTCSPC_1BPC; clr = arg; }
237             else if (arg < 16)
238               { spc = TTCSPC_1BPCBR; clr = (arg - 8) | TT1BPC_BRI; }
239             else if (arg < 232)
240               { spc = TTCSPC_6LPC; clr = arg - 16; }
241             else if (arg < 256)
242               { spc = TTCSPC_24LGS; clr = arg - 232; }
243             else {
244               if (f&TCIF_REPORT)
245                 moan("indexed colour %u out of range "
246                      "in `%.*s' string in `%s'",
247                      arg, (int)n, p, user);
248               st = ST_BASE; rc = -1; break;
249             }
250             if (st&ST_BG) { SETAFIELD(BGSPC, spc); bg = clr; }
251             else { SETAFIELD(FGSPC, spc); fg = clr; }
252             st = ST_BASE; break;
253
254           case ST_RED:
255             if (arg < 256)
256               { clr = U32(arg) << 16; st = ST_GREEN; }
257             else {
258               if (f&TCIF_REPORT)
259                 moan("red channel %u out of range "
260                      "in `%.*s' string in `%s'",
261                      arg, (int)n, p, user);
262               st = ST_BASE; rc = -1;
263             }
264             break;
265
266           case ST_GREEN:
267             if (arg < 256)
268               { clr |= U32(arg) << 8; st = ST_BLUE; }
269             else {
270               if (f&TCIF_REPORT)
271                 moan("green channel %u out of range "
272                      "in `%.*s' string in `%s'",
273                      arg, (int)n, p, user);
274               st = ST_BASE; rc = -1; break;
275             }
276             break;
277
278           case ST_BLUE:
279             if (arg < 256) {
280               clr |= U32(arg) << 0;
281               if (st&ST_BG) { SETAFIELD(BGSPC, TTCSPC_8BPC); bg = clr; }
282               else { SETAFIELD(FGSPC, TTCSPC_8BPC); fg = clr; }
283             } else {
284               if (f&TCIF_REPORT)
285                 moan("blue channel %u out of range "
286                      "in `%.*s' string in `%s'",
287                      arg, (int)n, p, user);
288               rc = -1; break;
289             }
290             st = ST_BASE; break;
291
292           default:
293             assert(0);
294         }
295         arg = 0;
296         if (!*q || *q == ':') break;
297         else if (*q != ';') {
298           if (f&TCIF_REPORT)
299             moan("expected `;' but found `%c' in `%.*s' string in `%s'",
300                  *q, (int)n, p, user);
301           rc = -1; q += strcspn(q, ":;"); if (!*q || *q == ':') break;
302         }
303         q++;
304       }
305       attr[i].f = a; attr[i]._res0 = 0; attr[i].fg = fg; attr[i].bg = bg;
306       p = *q ? q + 1 : q;
307     }
308
309   return (rc);
310 }
311
312 /*----- That's all, folks -------------------------------------------------*/