1 /*
2 * MOC - music on console
3 * Copyright (C) 2004 - 2006 Damian Pietras <daper@daper.net>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 */
11
12 #ifdef HAVE_CONFIG_H
13 # include "config.h"
14 #endif
15
16 #ifdef HAVE_NCURSESW_H
17 # include <ncursesw/curses.h>
18 #elif HAVE_NCURSES_H
19 # include <ncurses.h>
20 #elif HAVE_CURSES_H
21 # include <curses.h>
22 #endif
23
24 #include <stdio.h>
25 #include <assert.h>
26 #include <string.h>
27 #include <strings.h>
28 #include <errno.h>
29
30 #include "common.h"
31 #include "interface.h"
32 #include "themes.h"
33 #include "files.h"
34 #include "options.h"
35
36 /* ncurses extension */
37 #ifndef COLOR_DEFAULT
38 # define COLOR_DEFAULT -2
39 #endif
40
41 /* hidden color? */
42 #ifndef COLOR_GREY
43 # define COLOR_GREY 10
44 #endif
45
46 static char current_theme[PATH_MAX];
47
48 static int colors[CLR_LAST];
49
50 /* Counter used for making colors (init_pair()) */
51 static short pair_count = 1;
52
53 /* Initialize a color item of given index (CLR_*) with colors and
54 * attributes. Do nothing if the item is already initialized. */
make_color(const enum color_index index,const short foreground,const short background,const attr_t attr)55 static void make_color (const enum color_index index, const short foreground,
56 const short background, const attr_t attr)
57 {
58 assert (pair_count < COLOR_PAIRS);
59 assert (index < CLR_LAST);
60
61 if (colors[index] == -1) {
62 init_pair (pair_count, foreground, background);
63 colors[index] = COLOR_PAIR (pair_count) | attr;
64
65 pair_count++;
66 }
67 }
68
set_default_colors()69 static void set_default_colors ()
70 {
71 make_color (CLR_BACKGROUND, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
72 make_color (CLR_FRAME, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
73 make_color (CLR_WIN_TITLE, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
74 make_color (CLR_MENU_ITEM_DIR, COLOR_WHITE, COLOR_BLUE, A_BOLD);
75 make_color (CLR_MENU_ITEM_DIR_SELECTED, COLOR_WHITE, COLOR_BLACK,
76 A_BOLD);
77 make_color (CLR_MENU_ITEM_PLAYLIST, COLOR_WHITE, COLOR_BLUE, A_BOLD);
78 make_color (CLR_MENU_ITEM_PLAYLIST_SELECTED, COLOR_WHITE, COLOR_BLACK,
79 A_BOLD);
80 make_color (CLR_MENU_ITEM_FILE, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
81 make_color (CLR_MENU_ITEM_FILE_SELECTED, COLOR_WHITE,
82 COLOR_BLACK, A_NORMAL);
83 make_color (CLR_MENU_ITEM_FILE_MARKED, COLOR_GREEN, COLOR_BLUE,
84 A_BOLD);
85 make_color (CLR_MENU_ITEM_FILE_MARKED_SELECTED, COLOR_GREEN,
86 COLOR_BLACK, A_BOLD);
87 make_color (CLR_MENU_ITEM_INFO, COLOR_BLUE, COLOR_BLUE, A_BOLD);
88 make_color (CLR_MENU_ITEM_INFO_SELECTED, COLOR_BLUE, COLOR_BLACK, A_BOLD);
89 make_color (CLR_MENU_ITEM_INFO_MARKED, COLOR_BLUE, COLOR_BLUE, A_BOLD);
90 make_color (CLR_MENU_ITEM_INFO_MARKED_SELECTED, COLOR_BLUE, COLOR_BLACK, A_BOLD);
91 make_color (CLR_STATUS, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
92 make_color (CLR_TITLE, COLOR_WHITE, COLOR_BLUE, A_BOLD);
93 make_color (CLR_STATE, COLOR_WHITE, COLOR_BLUE, A_BOLD);
94 make_color (CLR_TIME_CURRENT, COLOR_WHITE, COLOR_BLUE, A_BOLD);
95 make_color (CLR_TIME_LEFT, COLOR_WHITE, COLOR_BLUE, A_BOLD);
96 make_color (CLR_TIME_TOTAL_FRAMES, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
97 make_color (CLR_TIME_TOTAL, COLOR_WHITE, COLOR_BLUE, A_BOLD);
98 make_color (CLR_SOUND_PARAMS, COLOR_WHITE, COLOR_BLUE, A_BOLD);
99 make_color (CLR_LEGEND, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
100 make_color (CLR_INFO_DISABLED, COLOR_BLUE, COLOR_BLUE, A_BOLD);
101 make_color (CLR_INFO_ENABLED, COLOR_WHITE, COLOR_BLUE, A_BOLD);
102 make_color (CLR_MIXER_BAR_EMPTY, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
103 make_color (CLR_MIXER_BAR_FILL, COLOR_BLACK, COLOR_CYAN, A_NORMAL);
104 make_color (CLR_TIME_BAR_EMPTY, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
105 make_color (CLR_TIME_BAR_FILL, COLOR_BLACK, COLOR_CYAN, A_NORMAL);
106 make_color (CLR_ENTRY, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
107 make_color (CLR_ENTRY_TITLE, COLOR_BLACK, COLOR_CYAN, A_BOLD);
108 make_color (CLR_ERROR, COLOR_RED, COLOR_BLUE, A_BOLD);
109 make_color (CLR_MESSAGE, COLOR_GREEN, COLOR_BLUE, A_BOLD);
110 make_color (CLR_PLIST_TIME, COLOR_WHITE, COLOR_BLUE, A_NORMAL);
111 }
112
113 /* Set default colors for black and white terminal. */
set_bw_colors()114 static void set_bw_colors ()
115 {
116 colors[CLR_BACKGROUND] = A_NORMAL;
117 colors[CLR_FRAME] = A_NORMAL;
118 colors[CLR_WIN_TITLE] = A_NORMAL;
119 colors[CLR_MENU_ITEM_DIR] = A_NORMAL;
120 colors[CLR_MENU_ITEM_DIR_SELECTED] = A_REVERSE;
121 colors[CLR_MENU_ITEM_PLAYLIST] = A_NORMAL;
122 colors[CLR_MENU_ITEM_PLAYLIST_SELECTED] = A_REVERSE;
123 colors[CLR_MENU_ITEM_FILE] = A_NORMAL;
124 colors[CLR_MENU_ITEM_FILE_SELECTED] = A_REVERSE;
125 colors[CLR_MENU_ITEM_FILE_MARKED] = A_BOLD;
126 colors[CLR_MENU_ITEM_FILE_MARKED_SELECTED] = A_BOLD | A_REVERSE;
127 colors[CLR_MENU_ITEM_INFO] = A_NORMAL;
128 colors[CLR_MENU_ITEM_INFO_SELECTED] = A_REVERSE;
129 colors[CLR_MENU_ITEM_INFO_MARKED] = A_BOLD;
130 colors[CLR_MENU_ITEM_INFO_MARKED_SELECTED] = A_BOLD | A_REVERSE;
131 colors[CLR_STATUS] = A_NORMAL;
132 colors[CLR_TITLE] = A_BOLD;
133 colors[CLR_STATE] = A_BOLD;
134 colors[CLR_TIME_CURRENT] = A_BOLD;
135 colors[CLR_TIME_LEFT] = A_BOLD;
136 colors[CLR_TIME_TOTAL_FRAMES] = A_NORMAL;
137 colors[CLR_TIME_TOTAL] = A_BOLD;
138 colors[CLR_SOUND_PARAMS] = A_BOLD;
139 colors[CLR_LEGEND] = A_NORMAL;
140 colors[CLR_INFO_DISABLED] = A_BOLD;
141 colors[CLR_INFO_ENABLED] = A_BOLD;
142 colors[CLR_MIXER_BAR_EMPTY] = A_NORMAL;
143 colors[CLR_MIXER_BAR_FILL] = A_REVERSE;
144 colors[CLR_TIME_BAR_EMPTY] = A_NORMAL;
145 colors[CLR_TIME_BAR_FILL] = A_REVERSE;
146 colors[CLR_ENTRY] = A_NORMAL;
147 colors[CLR_ENTRY_TITLE] = A_BOLD;
148 colors[CLR_ERROR] = A_BOLD;
149 colors[CLR_MESSAGE] = A_BOLD;
150 colors[CLR_PLIST_TIME] = A_NORMAL;
151 }
152
theme_parse_error(const int line,const char * msg)153 static void theme_parse_error (const int line, const char *msg)
154 {
155 interface_fatal ("Parse error in theme file line %d: %s", line, msg);
156 }
157
158 /* Find the index of a color element by name. Return CLR_WRONG if not found. */
find_color_element_name(const char * name)159 static enum color_index find_color_element_name (const char *name)
160 {
161 size_t ix;
162 static struct
163 {
164 char *name;
165 enum color_index idx;
166 } color_tab[] = {
167 { "background", CLR_BACKGROUND },
168 { "frame", CLR_FRAME },
169 { "window_title", CLR_WIN_TITLE },
170 { "directory", CLR_MENU_ITEM_DIR },
171 { "selected_directory", CLR_MENU_ITEM_DIR_SELECTED },
172 { "playlist", CLR_MENU_ITEM_PLAYLIST },
173 { "selected_playlist", CLR_MENU_ITEM_PLAYLIST_SELECTED },
174 { "file", CLR_MENU_ITEM_FILE },
175 { "selected_file", CLR_MENU_ITEM_FILE_SELECTED },
176 { "marked_file", CLR_MENU_ITEM_FILE_MARKED },
177 { "marked_selected_file", CLR_MENU_ITEM_FILE_MARKED_SELECTED },
178 { "info", CLR_MENU_ITEM_INFO },
179 { "selected_info", CLR_MENU_ITEM_INFO_SELECTED },
180 { "marked_info", CLR_MENU_ITEM_INFO_MARKED },
181 { "marked_selected_info", CLR_MENU_ITEM_INFO_MARKED_SELECTED },
182 { "status", CLR_STATUS },
183 { "title", CLR_TITLE },
184 { "state", CLR_STATE },
185 { "current_time", CLR_TIME_CURRENT },
186 { "time_left", CLR_TIME_LEFT },
187 { "total_time", CLR_TIME_TOTAL },
188 { "time_total_frames", CLR_TIME_TOTAL_FRAMES },
189 { "sound_parameters", CLR_SOUND_PARAMS },
190 { "legend", CLR_LEGEND },
191 { "disabled", CLR_INFO_DISABLED },
192 { "enabled", CLR_INFO_ENABLED },
193 { "empty_mixer_bar", CLR_MIXER_BAR_EMPTY },
194 { "filled_mixer_bar", CLR_MIXER_BAR_FILL },
195 { "empty_time_bar", CLR_TIME_BAR_EMPTY },
196 { "filled_time_bar", CLR_TIME_BAR_FILL },
197 { "entry", CLR_ENTRY },
198 { "entry_title", CLR_ENTRY_TITLE },
199 { "error", CLR_ERROR },
200 { "message", CLR_MESSAGE },
201 { "plist_time", CLR_PLIST_TIME }
202 };
203
204 assert (name != NULL);
205
206 for (ix = 0; ix < ARRAY_SIZE(color_tab); ix += 1) {
207 if (!strcasecmp(color_tab[ix].name, name))
208 return color_tab[ix].idx;
209 }
210
211 return CLR_WRONG;
212 }
213
214 /* Find the curses color by name. Return -1 if the color is unknown. */
find_color_name(const char * name)215 static short find_color_name (const char *name)
216 {
217 size_t ix;
218 static struct
219 {
220 char *name;
221 short color;
222 } color_tab[] = {
223 { "black", COLOR_BLACK },
224 { "red", COLOR_RED },
225 { "green", COLOR_GREEN },
226 { "yellow", COLOR_YELLOW },
227 { "blue", COLOR_BLUE },
228 { "magenta", COLOR_MAGENTA },
229 { "cyan", COLOR_CYAN },
230 { "white", COLOR_WHITE },
231 { "default", COLOR_DEFAULT },
232 { "grey", COLOR_GREY }
233 };
234
235 for (ix = 0; ix < ARRAY_SIZE(color_tab); ix += 1) {
236 if (!strcasecmp(color_tab[ix].name, name))
237 return color_tab[ix].color;
238 }
239
240 return -1;
241 }
242
new_colordef(const int line_num,const char * name,const short red,const short green,const short blue,const int errors_are_fatal)243 static int new_colordef (const int line_num, const char *name, const short red,
244 const short green, const short blue, const int errors_are_fatal)
245 {
246 short color = find_color_name (name);
247
248 if (color == -1) {
249 if (errors_are_fatal)
250 theme_parse_error (line_num, "bad color name");
251 return 0;
252 }
253 if (can_change_color())
254 init_color (color, red, green, blue);
255
256 return 1;
257 }
258
259 /* Find path to the theme for the given name. Returned memory is static. */
find_theme_file(const char * name)260 static char *find_theme_file (const char *name)
261 {
262 static char path[PATH_MAX];
263
264 path[sizeof(path)-1] = 0;
265 if (name[0] == '/') {
266
267 /* Absolute path */
268 strncpy (path, name, sizeof(path));
269 if (path[sizeof(path)-1])
270 interface_fatal ("Theme path too long!");
271 return path;
272 }
273
274 /* Try the user directory */
275 if (snprintf(path, sizeof(path), "%s/%s", create_file_name("themes"),
276 name) >= (int)sizeof(path))
277 interface_fatal ("Theme path too long!");
278 if (file_exists(path))
279 return path;
280
281 /* Try the system directory */
282 if (snprintf(path, sizeof(path), "%s/%s", SYSTEM_THEMES_DIR,
283 name) >= (int)sizeof(path))
284 interface_fatal ("Theme path too long!");
285 if (file_exists(path))
286 return path;
287
288 /* File related to the current directory? */
289 strncpy (path, name, sizeof(path));
290 if (path[sizeof(path)-1])
291 interface_fatal ("Theme path too long!");
292 return path;
293 }
294
295 /* Parse a theme element line. strtok() should be already invoked and consumed
296 * the element name.
297 * On error: if errors_are_fatal is true,
298 * theme_parse_error() is invoked, otherwise 0 is returned. */
parse_theme_element(const int line_num,const char * name,const int errors_are_fatal)299 static int parse_theme_element (const int line_num, const char *name,
300 const int errors_are_fatal)
301 {
302 char *tmp;
303 char *foreground, *background, *attributes;
304 attr_t curses_attr = 0;
305 enum color_index element;
306 short clr_fore, clr_back;
307
308 if (!(tmp = strtok(NULL, " \t")) || strcmp(tmp, "=")) {
309 if (errors_are_fatal)
310 theme_parse_error (line_num, "expected '='");
311 return 0;
312 }
313 if (!(foreground = strtok(NULL, " \t"))) {
314 if (errors_are_fatal)
315 theme_parse_error (line_num,
316 "foreground color not specified");
317 return 0;
318 }
319 if (!(background = strtok(NULL, " \t"))) {
320 if (errors_are_fatal)
321 theme_parse_error (line_num,
322 "background color not specified");
323 return 0;
324 }
325 if ((attributes = strtok(NULL, " \t"))) {
326 char *attr;
327
328 if ((tmp = strtok(NULL, " \t"))) {
329 if (errors_are_fatal)
330 theme_parse_error (line_num,
331 "unexpected chars at the end of line");
332 return 0;
333 }
334
335 attr = strtok (attributes, ",");
336
337 do {
338 if (!strcasecmp(attr, "normal"))
339 curses_attr |= A_NORMAL;
340 else if (!strcasecmp(attr, "standout"))
341 curses_attr |= A_STANDOUT;
342 else if (!strcasecmp(attr, "underline"))
343 curses_attr |= A_UNDERLINE;
344 else if (!strcasecmp(attr, "reverse"))
345 curses_attr |= A_REVERSE;
346 else if (!strcasecmp(attr, "blink"))
347 curses_attr |= A_BLINK;
348 else if (!strcasecmp(attr, "dim"))
349 curses_attr |= A_DIM;
350 else if (!strcasecmp(attr, "bold"))
351 curses_attr |= A_BOLD;
352 else if (!strcasecmp(attr, "protect"))
353 curses_attr |= A_PROTECT;
354 else {
355 if (errors_are_fatal)
356 theme_parse_error (line_num,
357 "unknown attribute");
358 return 0;
359 }
360 } while ((attr = strtok(NULL, ",")));
361 }
362
363 if ((element = find_color_element_name(name)) == CLR_WRONG) {
364 if (errors_are_fatal)
365 theme_parse_error (line_num, "unknown element");
366 return 0;
367 }
368 if ((clr_fore = find_color_name(foreground)) == -1) {
369 if (errors_are_fatal)
370 theme_parse_error (line_num,
371 "bad foreground color name");
372 return 0;
373 }
374 if ((clr_back = find_color_name(background)) == -1) {
375 if (errors_are_fatal)
376 theme_parse_error (line_num,
377 "bad background color name");
378 return 0;
379 }
380
381 make_color (element, clr_fore, clr_back, curses_attr);
382
383 return 1;
384 }
385
386 /* Parse a color value. strtok() should be already invoked and should "point"
387 * to the number. If errors_are_fatal, use theme_parse_error() on error,
388 * otherwise return -1. */
parse_rgb_color_value(const int line_num,const int errors_are_fatal)389 static short parse_rgb_color_value (const int line_num,
390 const int errors_are_fatal)
391 {
392 char *tmp;
393 char *end;
394 long color;
395
396 if (!(tmp = strtok(NULL, " \t"))) {
397 if (errors_are_fatal)
398 theme_parse_error (line_num, "3 color values expected");
399 return -1;
400 }
401 color = strtol (tmp, &end, 10);
402 if (*end) {
403 if (errors_are_fatal)
404 theme_parse_error (line_num,
405 "color value is not a valid number");
406 return -1;
407 }
408 if (!RANGE(0, color, 1000)) {
409 if (errors_are_fatal)
410 theme_parse_error (line_num,
411 "color value should be in range 0-1000");
412 return -1;
413 }
414
415 return color;
416 }
417
418 /* Parse a theme color definition. strtok() should be already invoked and
419 * consumed 'colordef'. On error: if errors_are_fatal is true,
420 * theme_parse_error() is invoked, otherwise 0 is returned. */
parse_theme_colordef(const int line_num,const int errors_are_fatal)421 static int parse_theme_colordef (const int line_num,
422 const int errors_are_fatal)
423 {
424 char *name;
425 char *tmp;
426 short red, green, blue;
427
428 if (!(name = strtok(NULL, " \t"))) {
429 if (errors_are_fatal)
430 theme_parse_error (line_num, "expected color name");
431 return 0;
432 }
433 if (!(tmp = strtok(NULL, " \t")) || strcmp(tmp, "=")) {
434 if (errors_are_fatal)
435 theme_parse_error (line_num, "expected '='");
436 return 0;
437 }
438
439 red = parse_rgb_color_value (line_num, errors_are_fatal);
440 green = parse_rgb_color_value (line_num, errors_are_fatal);
441 blue = parse_rgb_color_value (line_num, errors_are_fatal);
442 if (red == -1 || green == -1 || blue == -1)
443 return 0;
444
445 if (!new_colordef(line_num, name, red, green, blue, errors_are_fatal))
446 return 0;
447
448 return 1;
449 }
450
451 /* The lines should be in format:
452 *
453 * ELEMENT = FOREGROUND BACKGROUND [ATTRIBUTE[,ATTRIBUTE,..]]
454 * or:
455 * colordef COLORNAME = RED GREEN BLUE
456 *
457 * Blank lines and beginning with # are ignored, see example_theme.
458 *
459 * On error: if errors_are_fatal is true, interface_fatal() is invoked,
460 * otherwise 0 is returned. */
parse_theme_line(const int line_num,char * line,const int errors_are_fatal)461 static int parse_theme_line (const int line_num, char *line,
462 const int errors_are_fatal)
463 {
464 char *name;
465
466 if (line[0] == '#' || !(name = strtok(line, " \t"))) {
467
468 /* empty line or a comment */
469 return 1;
470 }
471
472 if (!strcasecmp(name, "colordef"))
473 return parse_theme_colordef (line_num, errors_are_fatal);
474 return parse_theme_element (line_num, name, errors_are_fatal);
475 }
476
477 /* Load a color theme. If errors_are_fatal is true, errors cause
478 * interface_fatal(), otherwise 0 is returned on error. */
load_color_theme(const char * name,const int errors_are_fatal)479 static int load_color_theme (const char *name, const int errors_are_fatal)
480 {
481 FILE *file;
482 char *line;
483 int result = 1;
484 int line_num = 0;
485 char *theme_file = find_theme_file (name);
486
487 if (!(file = fopen(theme_file, "r"))) {
488 if (errors_are_fatal)
489 interface_fatal ("Can't open theme file: %s", strerror(errno));
490 return 0;
491 }
492
493 while (result && (line = read_line (file))) {
494 line_num++;
495 result = parse_theme_line (line_num, line, errors_are_fatal);
496 free (line);
497 }
498
499 fclose (file);
500
501 return result;
502 }
503
reset_colors_table()504 static void reset_colors_table ()
505 {
506 int i;
507
508 pair_count = 1;
509 for (i = 0; i < CLR_LAST; i++)
510 colors[i] = -1;
511 }
512
theme_init(bool has_xterm)513 void theme_init (bool has_xterm)
514 {
515 reset_colors_table ();
516
517 if (has_colors ()) {
518 char *file;
519
520 if ((file = options_get_str ("ForceTheme"))) {
521 load_color_theme (file, 1);
522 strncpy (current_theme, find_theme_file (file), PATH_MAX);
523 }
524 else if (has_xterm && (file = options_get_str ("XTermTheme"))) {
525 load_color_theme (file, 1);
526 strncpy (current_theme, find_theme_file (file), PATH_MAX);
527 }
528 else if ((file = options_get_str ("Theme"))) {
529 load_color_theme (file, 1);
530 strncpy (current_theme, find_theme_file (file), PATH_MAX);
531 }
532 else
533 snprintf (current_theme, PATH_MAX, "%s/example_theme",
534 SYSTEM_THEMES_DIR);
535
536 set_default_colors ();
537 }
538 else
539 set_bw_colors ();
540 }
541
get_color(const enum color_index index)542 int get_color (const enum color_index index)
543 {
544 return colors[index];
545 }
546
themes_switch_theme(const char * file)547 void themes_switch_theme (const char *file)
548 {
549 if (has_colors()) {
550 reset_colors_table ();
551 if (!load_color_theme(file, 0)) {
552 interface_error ("Error loading theme!");
553 reset_colors_table ();
554 }
555 else
556 strncpy (current_theme, file, PATH_MAX);
557
558 set_default_colors ();
559 }
560 }
561
get_current_theme()562 const char *get_current_theme ()
563 {
564 return current_theme;
565 }
566