1/*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2022-2024 Alfonso Sabato Siciliano
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 * SUCH DAMAGE.
26 */
27
28#include <sys/time.h>
29
30#include <limits.h>
31#include <stdio.h>
32#include <stdlib.h>
33#include <string.h>
34#include <unistd.h>
35#include <time.h>
36
37#include <bsddialog.h>
38#include <bsddialog_theme.h>
39
40#include "util.h"
41
42static struct bsddialog_theme t;
43static char title[1024];
44
45#define NPROPERTY  41
46#define NCOLOR      8
47#define NATTR       6
48
49#define PROP_ERROR(name, error) do {                                           \
50	fclose(fp);                                                            \
51	exit_error(false, "%s for \"%s\"", error, name);                       \
52} while (0)
53
54enum typeproperty {
55	BOOL,
56	CHAR,
57	INT,
58	UINT,
59	COLOR,
60	COMPAT
61};
62
63struct property {
64	const char *comment;
65	const char *name;
66	enum typeproperty type;
67	void *value;
68};
69
70struct namevalue {
71	const char *name;
72	unsigned int value;
73};
74
75static struct namevalue color[NCOLOR] = {
76	{"black",   BSDDIALOG_BLACK},
77	{"red",     BSDDIALOG_RED},
78	{"green",   BSDDIALOG_GREEN},
79	{"yellow",  BSDDIALOG_YELLOW},
80	{"blue",    BSDDIALOG_BLUE},
81	{"magenta", BSDDIALOG_MAGENTA},
82	{"cyan",    BSDDIALOG_CYAN},
83	{"white",   BSDDIALOG_WHITE}
84};
85
86static struct namevalue attr[NATTR] = {
87 	{"bold",       BSDDIALOG_BOLD},
88	{"reverse",    BSDDIALOG_REVERSE},
89	{"underline",  BSDDIALOG_UNDERLINE},
90	{"blink",      BSDDIALOG_BLINK},
91	{"halfbright", BSDDIALOG_HALFBRIGHT},
92	{"highlight",  BSDDIALOG_HIGHLIGHT}
93};
94
95static struct property p[NPROPERTY] = {
96	{"\n#Terminal\n", "theme.screen.color", COLOR, &t.screen.color},
97
98	{"\n# Shadow\n",
99	    "theme.shadow.color", COLOR, &t.shadow.color},
100	{"# shift down right from main widget\n",
101	    "theme.shadow.y", UINT, &t.shadow.y},
102	{"", "theme.shadow.x", UINT, &t.shadow.x},
103
104	{"\n# Main widget\n",
105	    "theme.dialog.color", COLOR, &t.dialog.color},
106	{"", "theme.dialog.delimtitle", BOOL, &t.dialog.delimtitle},
107	{"", "theme.dialog.titlecolor", COLOR, &t.dialog.titlecolor},
108	{"", "theme.dialog.lineraisecolor", COLOR, &t.dialog.lineraisecolor},
109	{"", "theme.dialog.linelowercolor", COLOR, &t.dialog.linelowercolor},
110	{"", "theme.dialog.bottomtitlecolor", COLOR,
111	    &t.dialog.bottomtitlecolor},
112	{"", "theme.dialog.arrowcolor", COLOR, &t.dialog.arrowcolor},
113
114	{"\n# Menus: --checklist, --menu, --radiolist\n"
115	    "# prefix [selector] shortcut name desc bottomdesc\n",
116	    "theme.menu.f_prefixcolor", COLOR, &t.menu.f_prefixcolor},
117	{"", "theme.menu.prefixcolor", COLOR, &t.menu.prefixcolor},
118	{"", "theme.menu.f_selectorcolor", COLOR, &t.menu.f_selectorcolor},
119	{"", "theme.menu.selectorcolor", COLOR, &t.menu.selectorcolor},
120	{"", "theme.menu.f_namecolor", COLOR, &t.menu.f_namecolor},
121	{"", "theme.menu.namecolor", COLOR, &t.menu.namecolor},
122	{"", "theme.menu.f_desccolor", COLOR, &t.menu.f_desccolor},
123	{"", "theme.menu.desccolor", COLOR, &t.menu.desccolor},
124	{"", "theme.menu.f_shortcutcolor", COLOR, &t.menu.f_shortcutcolor},
125	{"", "theme.menu.shortcutcolor", COLOR, &t.menu.shortcutcolor},
126	{"", "theme.menu.bottomdesccolor", COLOR, &t.menu.bottomdesccolor},
127	{"# bsddialog_menutype BSDDIALOG_SEPARATOR\n",
128	    "theme.menu.sepnamecolor", COLOR, &t.menu.sepnamecolor},
129	{"", "theme.menu.sepdesccolor", COLOR, &t.menu.sepdesccolor},
130
131	{"\n# Forms\n",
132	    "theme.form.f_fieldcolor", COLOR, &t.form.f_fieldcolor},
133	{"", "theme.form.fieldcolor", COLOR, &t.form.fieldcolor},
134	{"", "theme.form.readonlycolor", COLOR, &t.form.readonlycolor},
135	{"", "theme.form.bottomdesccolor", COLOR, &t.form.bottomdesccolor},
136
137	{"\n# Bar of --gauge, --mixedgauge, --pause, --rangebox\n",
138	    "theme.bar.f_color", COLOR, &t.bar.f_color},
139	{"", "theme.bar.color", COLOR, &t.bar.color},
140
141	{"\n# Buttons\n",
142	    "theme.button.minmargin", UINT, &t.button.minmargin},
143	{"", "theme.button.maxmargin", UINT, &t.button.maxmargin},
144	{"", "theme.button.leftdelim", CHAR, &t.button.leftdelim},
145	{"", "theme.button.rightdelim", CHAR, &t.button.rightdelim},
146	{"", "theme.button.f_delimcolor", COLOR, &t.button.f_delimcolor},
147	{"", "theme.button.delimcolor", COLOR, &t.button.delimcolor},
148	{"", "theme.button.f_color", COLOR, &t.button.f_color},
149	{"", "theme.button.color", COLOR, &t.button.color},
150	{"", "theme.button.f_shortcutcolor", COLOR, &t.button.f_shortcutcolor},
151	{"", "theme.button.shortcutcolor", COLOR, &t.button.shortcutcolor},
152
153	{"\n#Compatibility. Do not use, can be deleted\n",
154	    "use_shadow", COMPAT, NULL}
155};
156
157void savetheme(const char *file)
158{
159	int i, j;
160	unsigned int flags;
161	enum bsddialog_color bg, fg;
162	time_t clock;
163	FILE *fp;
164
165	if (bsddialog_get_theme(&t) != BSDDIALOG_OK)
166		exit_error(false,
167		    "cannot save theme: %s", bsddialog_geterror());
168
169	if (time(&clock) < 0)
170		exit_error(false, "cannot save profile getting current time");
171	if ((fp = fopen(file, "w")) == NULL)
172		exit_error(false, "cannot open %s to save profile", file);
173
174	fprintf(fp, "### bsddialog theme - %s\n", ctime(&clock));
175
176	fputs("# Colors: ", fp);
177	fputs("black red green yellow blue magenta cyan white.\n", fp);
178	fputs("# Attributes: ", fp);
179	fputs("bold reverse underline blink halfbright highlight.\n", fp);
180	fputs("# f_* refers to focus for an element with selected or ", fp);
181	fputs("unselected state.\n\n", fp);
182
183	fprintf(fp, "version %s\n", LIBBSDDIALOG_VERSION);
184
185	for (i = 0; i < NPROPERTY; i++) {
186		if (p[i].type == COMPAT)
187			continue;
188		fprintf(fp, "%s%s", p[i].comment, p[i].name);
189		switch (p[i].type) {
190		case CHAR:
191			fprintf(fp, " %c\n", *((char*)p[i].value));
192			break;
193		case INT:
194			fprintf(fp, " %d\n", *((int*)p[i].value));
195			break;
196		case UINT:
197			fprintf(fp, " %u\n", *((unsigned int*)p[i].value));
198			break;
199		case BOOL:
200			fprintf(fp, " %s\n",
201			    *((bool*)p[i].value) ? "true" : "false");
202			break;
203		case COLOR:
204			bsddialog_color_attrs(*(int*)p[i].value, &fg, &bg,
205			    &flags);
206			fprintf(fp, " %s %s", color[fg].name, color[bg].name);
207			for (j = 0; j < NATTR; j++)
208				if (flags & attr[j].value)
209					fprintf(fp, " %s", attr[j].name);
210			fputs("\n", fp);
211			break;
212		case COMPAT:
213			/* Do not save compat property for now */
214			break;
215		}
216	}
217
218	fclose(fp);
219}
220
221void loadtheme(const char *file, bool compatibility)
222{
223	bool boolvalue;
224	char charvalue, *value;
225	char line[BUFSIZ], name[BUFSIZ], c1[BUFSIZ], c2[BUFSIZ];
226	int i, j, intvalue;
227	unsigned int uintvalue, flags;
228	enum bsddialog_color bg, fg;
229	FILE *fp;
230
231	if (bsddialog_hascolors() == false)
232		return;
233
234	if (bsddialog_get_theme(&t) != BSDDIALOG_OK)
235		exit_error(false, "Cannot get current theme: %s",
236		    bsddialog_geterror());
237
238	if ((fp = fopen(file, "r")) == NULL)
239		exit_error(false, "Cannot open theme \"%s\" file", file);
240
241	while (fgets(line, BUFSIZ, fp) != NULL) {
242		if (line[0] == '#' || line[0] == '\n')
243			continue;  /* superfluous, only for efficiency */
244		sscanf(line, "%s", name);
245		value = NULL; /* useless init, fix compiler warning */
246		for (i = 0; i < NPROPERTY; i++) {
247			if (strcmp(name, p[i].name) == 0) {
248				value = &line[strlen(name)];
249				break;
250			}
251		}
252		if (i >= NPROPERTY) {
253			/* unknown name in property p[] */
254			if (strcmp(name, "version") == 0)
255				continue; /* nothing for now */
256			else if (compatibility)
257				continue; /* just ignore */
258			else
259				PROP_ERROR(name, "Unknown theme property name");
260		}
261		switch (p[i].type) {
262		case CHAR:
263			while (value[0] == ' ' || value[0] == '\n' ||
264			    value[0] == '\0')
265				value++;
266			if (sscanf(value, "%c", &charvalue) != 1)
267				PROP_ERROR(p[i].name, "Cannot get a char");
268			*((int*)p[i].value) = charvalue;
269			break;
270		case INT:
271			if (sscanf(value, "%d", &intvalue) != 1)
272				PROP_ERROR(p[i].name, "Cannot get a int");
273			*((int*)p[i].value) = intvalue;
274			break;
275		case UINT:
276			if (sscanf(value, "%u", &uintvalue) != 1)
277				PROP_ERROR(p[i].name, "Cannot get a uint");
278			*((unsigned int*)p[i].value) = uintvalue;
279			break;
280		case BOOL:
281			boolvalue = (strstr(value, "true") != NULL) ?
282			    true :false;
283			*((bool*)p[i].value) = boolvalue;
284			break;
285		case COLOR:
286			if (sscanf(value, "%s %s", c1, c2) != 2)
287				PROP_ERROR(p[i].name, "Cannot get 2 colors");
288			/* Foreground */
289			for (j = 0; j < NCOLOR ; j++)
290				if ((strstr(c1, color[j].name)) != NULL)
291					break;
292			if (j >= NCOLOR)
293				PROP_ERROR(p[i].name, "Bad foreground");
294			fg = color[j].value;
295			/* Background */
296			for (j = 0; j < NCOLOR ; j++)
297				if ((strstr(c2, color[j].name)) != NULL)
298					break;
299			if (j >= NCOLOR)
300				PROP_ERROR(p[i].name, "Bad background");
301			bg = color[j].value;
302			/* Flags */
303			flags = 0;
304			for (j = 0; j < NATTR; j++)
305				if (strstr(value, attr[j].name) != NULL)
306					flags |= attr[j].value;
307			*((int*)p[i].value) = bsddialog_color(fg, bg, flags);
308			break;
309		case COMPAT:
310			/*
311			 * usr.sbin/bsdconfig/share/dialog.subr:2255
312			 * uses this parameter to set NO_SHADOW.
313			 * Set t.shadow.[y|x] for compatibilty.
314			 */
315			if (strcmp(name, "use_shadow") == 0) {
316				 if (strcasestr(value, "off") != NULL)
317				 	t.shadow.y = t.shadow.x = 0;
318			}
319			break;
320		}
321	}
322
323	fclose(fp);
324
325	if (bsddialog_set_theme(&t) != BSDDIALOG_OK)
326		exit_error(false, bsddialog_geterror());
327}
328
329void setdeftheme(enum bsddialog_default_theme theme)
330{
331	if (bsddialog_hascolors() == false)
332		return;
333	if (bsddialog_set_default_theme(theme) != BSDDIALOG_OK)
334		exit_error(false, bsddialog_geterror());
335}
336
337void startuptheme(void)
338{
339	bool sep;
340	char *env, *file, *home, path[PATH_MAX];
341
342	env = getenv("NO_COLOR");
343	if (env != NULL && env[0] != '\0')
344		setdeftheme(BSDDIALOG_THEME_BLACKWHITE);
345
346	if ((home = getenv("HOME")) != NULL) {
347		sep = (strcmp(home, "/") == 0) ? false : true;
348
349		snprintf(path, PATH_MAX, "%s%s.bsddialog.conf",
350		    home, sep ? "/" : "");
351		if (access(path, F_OK) == 0)
352			loadtheme(path, false);
353
354		if ((file = getenv("BSDDIALOG_COMPATRC")) != NULL) {
355			snprintf(path, PATH_MAX, "%s%s%s",
356			    home, sep ? "/" : "", file);
357			if (access(path, F_OK) == 0)
358				loadtheme(path, true);
359		}
360	}
361	if ((file = getenv("BSDDIALOG_THEMEFILE")) != NULL) {
362		if (access(file, F_OK) == 0)
363			loadtheme(file, false);
364	}
365}
366
367void bikeshed(struct bsddialog_conf *conf)
368{
369	int margin, i;
370	int colors[8] = {0, 0, 0, 0, 0, 0, 0, 0};
371	char delim[8] = {'[', '<', '(', '|', ']', '>', ')', '|'};
372	enum bsddialog_color col[6];
373	struct timeval tv;
374
375	/* theme */
376	if (bsddialog_get_theme(&t) != BSDDIALOG_OK)
377		exit_error(false, bsddialog_geterror());
378
379	gettimeofday(&tv, NULL);
380	srand(tv.tv_usec);
381	for (i = 0; i < 6; i++) {
382		do {
383			col[i] = rand() % 8;
384		} while (colors[col[i]] == 1);
385		colors[col[i]] = 1;
386	}
387
388	t.screen.color = bsddialog_color(col[4], col[3], 0);
389
390	t.shadow.color   = bsddialog_color(col[0], col[0], 0);
391	t.shadow.y       = 1,
392	t.shadow.x       = 2,
393
394	t.dialog.delimtitle       = (~rand() & 1) ? true : false;
395	t.dialog.titlecolor       = bsddialog_color(col[3], col[5], 0);
396	t.dialog.lineraisecolor   = bsddialog_color(col[0], col[5], 0);
397	t.dialog.linelowercolor   = bsddialog_color(col[0], col[5], 0);
398	t.dialog.color            = bsddialog_color(col[0], col[5], 0);
399	t.dialog.bottomtitlecolor = bsddialog_color(col[0], col[5], 0);
400	t.dialog.arrowcolor       = bsddialog_color(col[3], col[5], 0);
401
402	t.menu.f_prefixcolor   = bsddialog_color(col[5], col[3], 0);
403	t.menu.prefixcolor     = bsddialog_color(col[0], col[5], 0);
404	t.menu.f_selectorcolor = bsddialog_color(col[5], col[3], 0);
405	t.menu.selectorcolor   = bsddialog_color(col[0], col[5], 0);
406	t.menu.f_desccolor     = bsddialog_color(col[5], col[3], 0);
407	t.menu.desccolor       = bsddialog_color(col[0], col[5], 0);
408	t.menu.f_namecolor     = bsddialog_color(col[5], col[3], 0);
409	t.menu.namecolor       = bsddialog_color(col[3], col[5], 0);
410	t.menu.f_shortcutcolor = bsddialog_color(col[1], col[3], 0);
411	t.menu.shortcutcolor   = bsddialog_color(col[1], col[5], 0);
412	t.menu.bottomdesccolor = bsddialog_color(col[4], col[3], 0);
413	t.menu.sepnamecolor    = bsddialog_color(col[1], col[5], 0);
414	t.menu.sepdesccolor    = bsddialog_color(col[1], col[5], 0);
415
416	t.form.f_fieldcolor    = bsddialog_color(col[5], col[3], 0);
417	t.form.fieldcolor      = bsddialog_color(col[5], col[4], 0);
418	t.form.readonlycolor   = bsddialog_color(col[4], col[5], 0);
419	t.form.bottomdesccolor = bsddialog_color(col[4], col[3], 0);
420
421	t.bar.f_color = bsddialog_color(col[5], col[3], 0);
422	t.bar.color   = bsddialog_color(col[3], col[5], 0);
423
424	t.button.minmargin       = 1,
425	t.button.maxmargin       = 5,
426	i = rand() % 4;
427	t.button.leftdelim       = delim[i];
428	t.button.rightdelim      = delim[i + 4];
429	t.button.f_delimcolor    = bsddialog_color(col[5], col[3], 0);
430	t.button.delimcolor      = bsddialog_color(col[0], col[5], 0);
431	t.button.f_color         = bsddialog_color(col[2], col[3], 0);
432	t.button.color           = bsddialog_color(col[0], col[5], 0);
433	t.button.f_shortcutcolor = bsddialog_color(col[5], col[3], 0);
434	t.button.shortcutcolor   = bsddialog_color(col[1], col[5], 0);
435
436	if (bsddialog_set_theme(&t))
437		exit_error(false, bsddialog_geterror());
438
439	/* conf */
440	conf->button.always_active = (~rand() & 1) ? true : false;
441	if ((i = rand() % 3) != 0) /* default "d/m/y" */
442		conf->date.format = (i & 1) ? "m/d/y" : "y/m/d" ;
443	if (conf->title != NULL) {
444		memset(title, 0, 1024);
445		margin = rand() % 5;
446		memset(title, ' ', margin);
447		strcpy(title + margin, conf->title);
448		memset(title + strlen(title), ' ', margin);
449		conf->title = title;
450	}
451}
452