3 * Configurable terminal colour support
5 * (c) 2024 Straylight/Edgeware
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the mLib utilities library.
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.
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.
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,
28 /*----- Header files ------------------------------------------------------*/
36 #include "ttycolour.h"
38 /*----- Main code ---------------------------------------------------------*/
40 /* --- @env_setting_p@ --- *
42 * Arguments: @const char *var@ = environment variable name
44 * Returns: Nonzero if the variable is set to a non-empty value,
47 * Use: This is the recommended way to check the %|NO_COLOR|%,
48 * `%|CLICOLOR|% and %|CLICOLOR_FORCE|% variables.
51 static int env_setting_p(const char *var)
52 { const char *p; p = getenv(var); return (p && *p); }
54 /* --- @ttycolour_enablep@ --- *
56 * Arguments: @unsigned f@ = flags
58 * Returns: Nonzero if colours should be applied to output, otherwise
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
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.)
72 * * If the %|NO_COLOR|% environment variable is non-empty,
73 * colour is disabled (%%\url{https://no-color.org/}%%).
75 * * If the %|TERM|% variable is set to %|dumb|%, then colour
76 * is disabled (Emacs).
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}%%).
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/}%%).
88 * * If the @TCEF_TTY@ flag is clear, then colour is disabled.
90 * * If the @TCEF_DFLT@ flag is set, then colour is enabled.
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/}%%).
96 * * Otherwise, colour is disabled.
99 int ttycolour_enablep(unsigned f)
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);
114 int ttycolour_config(struct tty_attr *attr, const char *user, unsigned f,
115 struct tty *tty, const struct ttycolour_style *tab)
118 const struct tty_attrlist *aa;
120 unsigned i, arg, a, fg, bg, st, spc, clr;
129 #define ST_MASK 0x7fu
133 #define SETAFIELD(f, v) (a = (a&~TTAF_##f##MASK) | ((v) << TTAF_##f##SHIFT))
135 for (i = 0; tab[i].tok; i++) {
139 if ((tty->acaps&aa->cap_mask) == aa->cap_eq)
140 { attr[i] = aa->attr; goto next_token; }
141 else if (!aa->cap_mask)
144 attr[i].f = attr[i]._res0 = attr[i].fg = attr[i].bg = 0;
148 if (f&TCIF_GETENV) p = getenv(user);
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;
158 for (i = 0; tab[i].tok; i++)
159 if (STRNCMP(p, ==, tab[i].tok, n) && !tab[i].tok[n])
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;
166 a = fg = bg = 0; st = ST_BASE; arg = 0; clr = 0; q++;
170 { arg = 10*arg + (*q++ - '0'); continue; }
172 switch (st&ST_MASK) {
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;
187 case 30: case 31: case 32: case 33:
188 case 34: case 35: case 36: case 37:
189 SETAFIELD(FGSPC, TTCSPC_1BPC);
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;
197 case 38: st = ST_EXT | ST_FG; break;
198 case 39: SETAFIELD(FGSPC, TTCSPC_NONE); break;
200 case 40: case 41: case 42: case 43:
201 case 44: case 45: case 46: case 47:
202 SETAFIELD(BGSPC, TTCSPC_1BPC);
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;
210 case 48: st = ST_EXT | ST_BG; break;
211 case 49: SETAFIELD(BGSPC, TTCSPC_NONE); break;
215 moan("unknown colour code %u in `%.*s' string in `%s'",
216 arg, (int)n, p, user);
223 case 2: st = (st&~ST_MASK) | ST_RED; break;
224 case 5: st = (st&~ST_MASK) | ST_CLR; break;
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;
236 { spc = TTCSPC_1BPC; clr = arg; }
238 { spc = TTCSPC_1BPCBR; clr = (arg - 8) | TT1BPC_BRI; }
240 { spc = TTCSPC_6LPC; clr = arg - 16; }
242 { spc = TTCSPC_24LGS; clr = arg - 232; }
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;
250 if (st&ST_BG) { SETAFIELD(BGSPC, spc); bg = clr; }
251 else { SETAFIELD(FGSPC, spc); fg = clr; }
256 { clr = U32(arg) << 16; st = ST_GREEN; }
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;
268 { clr |= U32(arg) << 8; st = ST_BLUE; }
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;
280 clr |= U32(arg) << 0;
281 if (st&ST_BG) { SETAFIELD(BGSPC, TTCSPC_8BPC); bg = clr; }
282 else { SETAFIELD(FGSPC, TTCSPC_8BPC); fg = clr; }
285 moan("blue channel %u out of range "
286 "in `%.*s' string in `%s'",
287 arg, (int)n, p, user);
296 if (!*q || *q == ':') break;
297 else if (*q != ';') {
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;
305 attr[i].f = a; attr[i]._res0 = 0; attr[i].fg = fg; attr[i].bg = bg;
312 /*----- That's all, folks -------------------------------------------------*/