/* SPDX-License-Identifier: GPL-2.0 */ /** \file gui_theme.c Theme definitions. */ #include #include "para.h" #include "gui.h" #include "error.h" #include "string.h" #include "fd.h" static char *create_title(uint64_t mask, char **stat_content) { char *artist = stat_content[SI_artist]; char *title = stat_content[SI_title]; char *path = stat_content[SI_path]; bool path_change = status_item_bit_set(SI_path, mask); bool artist_change = status_item_bit_set(SI_artist, mask); bool title_change = status_item_bit_set(SI_title, mask); const char *slash; if (!path_change && !artist_change && !title_change) return NULL; if (!(artist && title) && !path) return NULL; if (artist && title) return make_message("%s: %s", artist, title); slash = strrchr(path, '/'); return para_strdup(slash && slash[1]? slash + 1 : path); } struct figlet_output { size_t max_width; unsigned num_lines; char **lines; }; /* Maximal height of a character, depends on the figlet font */ #define MAX_FIGLET_LINES 10 static int figlet_line_cb(char *line, void *data) { struct figlet_output *figo = data; size_t this_width; int ret; if (figo->num_lines >= MAX_FIGLET_LINES) return -ERRNO_TO_PARA_ERROR(EOVERFLOW); ret = strwidth(line, &this_width); if (ret < 0) return 0; if (this_width > figo->max_width) figo->max_width = this_width; figo->lines[figo->num_lines] = para_strdup(line); figo->num_lines++; return 0; } static void figlet_print(struct figlet_output *figo, WINDOW *win) { int cut1, cut2; if (figo->max_width <= COLS) { int num_spaces = COLS - figo->max_width; for (unsigned n = 0; n < figo->num_lines; n++) { char *str = make_message("%*s%s%*s", num_spaces / 2, "", figo->lines[n], (num_spaces + 1) / 2, ""); assert(mvwaddstr(win, n, 0, str) == OK); free(str); } return; } /* cut1 + dots + (max_with - cut2) == COLS */ cut1 = COLS / 2 - 9 / 2; cut2 = cut1 + 9 + figo->max_width - COLS; for (unsigned n = 0; n < figo->num_lines; n++) { char *dots, *str; if (n == figo->num_lines - 2) dots = " _ _ _ "; else if (n == figo->num_lines - 1) dots = " (_|_|_) "; else dots = " "; str = make_message("%.*s%s%s", cut1, figo->lines[n], dots, figo->lines[n] + cut2); assert(mvwaddstr(win, n, 0, str) == OK); free(str); } } static int figlet_exec(const char *str, WINDOW *win) { const char * const cmd = "figlet -C utf8 -w 9999 -f big"; char *buf; size_t bytes_loaded = 0, buf_bytes, max_bytes = 1024 * 1024; int fds[3] = {1, 1, -1}; int ret, status; struct figlet_output figo = {.max_width = 0}; pid_t pid; ret = xexec(&pid, cmd, fds); if (ret < 0) return ret; write_all(fds[0], str, strlen(str)); close(fds[0]); buf_bytes = 10; buf = alloc(buf_bytes); do { if (bytes_loaded + 1 >= buf_bytes) { if (bytes_loaded > max_bytes) return ERRNO_TO_PARA_ERROR(EOVERFLOW); buf_bytes *= 2; assert(buf_bytes > bytes_loaded + 1); buf = para_realloc(buf, buf_bytes); } ret = read(fds[1], buf + bytes_loaded, buf_bytes - bytes_loaded - 1); if (ret < 0) return ret; bytes_loaded += ret; } while (ret > 0); close(fds[1]); do ret = waitpid(pid, &status, 0); while (ret == -1 && errno == EINTR); buf[bytes_loaded] = '\0'; figo.lines = arr_alloc(MAX_FIGLET_LINES, sizeof(char *)); ret = for_each_line(FELF_READ_ONLY, buf, bytes_loaded, figlet_line_cb, &figo); free(buf); if (ret >= 0) figlet_print(&figo, win); for (unsigned n = 0; n < figo.num_lines; n++) free(figo.lines[n]); free(figo.lines); return ret; } static bool figlet_print_status_items(uint64_t mask, char **items, WINDOW *win) { int ret; char *str = create_title(mask, items); if (!str) return false; ret = figlet_exec(*str? str : " ", win); free(str); if (ret < 0) PARA_ERROR_LOG("figlet exec error: %s\n", para_strerror(-ret)); return true; } static bool simple_print_status_items(uint64_t mask, char **items, WINDOW *win) { char *str = create_title(mask, items); align_str(win, str, COLS, CENTER); /* noop if str == NULL */ free(str); return str != NULL; } /* How the colorful blackness theme displays one status item. */ struct stat_item_data { const char *prefix; /* Text to print before the item content. */ const char *postfix; /* Text to print after item content. */ unsigned x; /* Horizontal start coordinate for this item. */ unsigned y; /* Vertical start coordinate for this item. */ unsigned len; /* Item width, including prefix and postfix. */ struct gui_color_spec color; /* Foreground and background color. */ int align; /* How to align this item. */ }; static const struct stat_item_data colorful_blackness_data[NUM_STAT_ITEMS] = { [SI_play_time] = { .prefix = "", .postfix = "", .color.fg = COLOR_CYAN, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 7, .len = 35, }, [SI_basename] = { .prefix = "", .postfix = "", .color.fg = COLOR_CYAN, .color.bg = COLOR_BLACK, .align = LEFT, .x = 35, .y = 7, .len = 65, }, [SI_status] = { .prefix = "", .postfix = " ", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 0, .y = 17, .len = 11, }, [SI_status_flags] = { .prefix = "(", .postfix = ")", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = LEFT, .x = 11, .y = 17, .len = 8, }, [SI_image_id] = { .prefix = "img: ", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 19, .y = 17, .len = 12, }, [SI_lyrics_id] = { .prefix = "lyr: ", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 31, .y = 17, .len = 11, }, [SI_format] = { .prefix = "format: ", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 42, .y = 17, .len = 16, }, [SI_num_played] = { .prefix = "#", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = LEFT, .x = 58, .y = 17, .len = 5, }, [SI_bitrate] = { .prefix = "", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 65, .y = 17, .len = 13, }, [SI_frequency] = { .prefix = "", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 78, .y = 17, .len = 10, }, [SI_score] = { .prefix = "sc: ", .postfix = "", .color.fg = COLOR_RED, .color.bg = COLOR_BLACK, .align = CENTER, .x = 88, .y = 17, .len = 10, }, [SI_audiod_status] = { .prefix = "", .postfix = "", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 27, .len = 5, }, [SI_decoder_flags] = { .prefix = "[", .postfix = "]", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 5, .y = 27, .len = 10, }, [SI_mtime] = { .prefix = "mod: ", .postfix = "", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 15, .y = 27, .len = 22, }, [SI_file_size] = { .prefix = "", .postfix = "", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 37, .y = 27, .len = 10, }, [SI_channels] = { .prefix = "", .postfix = "ch", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 47, .y = 27, .len = 5, }, [SI_last_played] = { .prefix = "lp: ", .postfix = "", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = CENTER, .x = 52, .y = 27, .len = 21, }, [SI_num_chunks] = { .prefix = "", .postfix = "x", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 73, .y = 27, .len = 11, }, [SI_chunk_time] = { .prefix = "", .postfix = "ms", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = LEFT, .x = 84, .y = 27, .len = 8, }, [SI_amplification] = { .prefix = "amp: ", .postfix = "", .color.fg = COLOR_MAGENTA, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 91, .y = 27, .len = 9, }, [SI_techinfo] = { .prefix = "", .postfix = "", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 43, .len = 100, }, [SI_artist] = { .prefix = "Artist: ", .postfix = " ", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 0, .y = 53, .len = 50, }, [SI_title] = { .prefix = "Title: ", .postfix = "", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = LEFT, .x = 50, .y = 53, .len = 45, }, [SI_year] = { .prefix = "(", .postfix = ")", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 90, .y = 53, .len = 10, }, [SI_comment] = { .prefix = "Comment: ", .postfix = " ", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = RIGHT, .x = 0, .y = 63, .len = 50, }, [SI_album] = { .prefix = "Album: ", .postfix = "", .color.fg = COLOR_GREEN, .color.bg = COLOR_BLACK, .align = LEFT, .x = 50, .y = 63, .len = 50, }, [SI_afs_mode] = { .prefix = "", .postfix = "", .color.fg = COLOR_YELLOW, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 77, .len = 100, }, [SI_attributes_txt] = { .prefix = "", .postfix = "", .color.fg = COLOR_YELLOW, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 87, .len = 100, }, [SI_directory] = { .prefix = "dir: ", .postfix = "", .color.fg = COLOR_YELLOW, .color.bg = COLOR_BLACK, .align = CENTER, .x = 0, .y = 97, .len = 100, } }; static bool colorful_blackness_print_status_items(uint64_t mask, char **items, WINDOW *win) { static bool initialized; int i, top_lines = getmaxy(win); if (!initialized) { FOR_EACH_STATUS_ITEM(i) { struct stat_item_data d = colorful_blackness_data[i]; if (!d.len) continue; assert(init_pair(i + 1, d.color.fg, d.color.bg) == OK); } initialized = true; } FOR_EACH_STATUS_ITEM(i) { struct stat_item_data d = colorful_blackness_data[i]; char *c = items[i]; if (!c) continue; if (!status_item_bit_set(i, mask)) continue; if (i == SI_status_flags && c[0] == '\0') c = "down"; if (d.len > 0) { char *tmp = make_message("%s%s%s", d.prefix, c, d.postfix); wmove(win, d.y * top_lines / 100, d.x * COLS / 100); wattron(win, COLOR_PAIR(i + 1)); align_str(win, tmp, d.len * COLS / 100, d.align); free(tmp); } } return true; } static const struct gui_theme themes[] = { { .name = "colorful blackness", .lines_min = 15, .cols_min = 80, .top_lines_min = 9, .top_lines_default = 11, .vbar.bg = COLOR_GREEN, .vbar.fg = COLOR_BLACK, .cmd.bg = COLOR_BLACK, .cmd.fg = COLOR_YELLOW, .output.bg = COLOR_BLACK, .output.fg = COLOR_WHITE, .msg.bg = COLOR_BLACK, .msg.fg = COLOR_CYAN, .err_msg.bg = COLOR_RED, .err_msg.fg = COLOR_WHITE, .pbar.bg = COLOR_BLACK, .pbar.fg = COLOR_BLUE, .duration.bg = COLOR_BLACK, .duration.fg = COLOR_WHITE, .top.bg = COLOR_BLACK, .top.fg = COLOR_MAGENTA, .bot.bg = COLOR_BLACK, .bot.fg = COLOR_MAGENTA, .dflt.bg = COLOR_BLACK, .dflt.fg = COLOR_MAGENTA, .print_status_items = colorful_blackness_print_status_items, }, { .name = "simple", .lines_min = 5, .top_lines_min = 1, .cols_min = 40, .top_lines_default = 1, .vbar.bg = COLOR_CYAN, .vbar.fg = COLOR_BLACK, .cmd.bg = COLOR_WHITE, .cmd.fg = COLOR_BLACK, .output.bg = COLOR_BLUE, .output.fg = COLOR_WHITE, .msg.bg = COLOR_BLUE, .msg.fg = COLOR_YELLOW, .err_msg.bg = COLOR_RED, .err_msg.fg = COLOR_WHITE, .pbar.bg = COLOR_BLUE, .pbar.fg = COLOR_CYAN, .duration.bg = COLOR_BLUE, .duration.fg = COLOR_WHITE, .top.bg = COLOR_BLUE, .top.fg = COLOR_WHITE, .bot.bg = COLOR_BLUE, .bot.fg = COLOR_WHITE, .dflt.bg = COLOR_BLUE, .dflt.fg = COLOR_WHITE, .print_status_items = simple_print_status_items, }, { .name = "figlet", .cols_min = 40, .lines_min = 23, .top_lines_min = 9, .top_lines_default = 9, .vbar.bg = COLOR_GREEN, .vbar.fg = COLOR_BLACK, .cmd.bg = COLOR_BLACK, .cmd.fg = COLOR_YELLOW, .output.bg = COLOR_BLACK, .output.fg = COLOR_WHITE, .msg.bg = COLOR_BLACK, .msg.fg = COLOR_WHITE, .err_msg.bg = COLOR_RED, .err_msg.fg = COLOR_WHITE, .pbar.bg = COLOR_BLACK, .pbar.fg = COLOR_BLUE, .duration.bg = COLOR_BLACK, .duration.fg = COLOR_WHITE, .top.bg = COLOR_BLACK, .top.fg = COLOR_YELLOW, .dflt.bg = COLOR_BLACK, .dflt.fg = COLOR_YELLOW, .print_status_items = figlet_print_status_items, } }; /** Number of elements in the \a themes array. */ #define NUM_THEMES (ARRAY_SIZE(themes)) static unsigned current_theme_num; /** * Initialize a theme, given its name. * * \param name Name of the theme to be initialized. * * If the named theme is not found, print the names of all available themes * and exit. */ const struct gui_theme *theme_init(const char *name) { if (!name) { current_theme_num = 0U; return themes; } for (unsigned n = 0; n < NUM_THEMES; n++) if (strcmp(name, themes[n].name) == 0) { current_theme_num = n; return themes + current_theme_num; } fprintf(stderr, "Available themes:\n"); for (unsigned n = 0; n < NUM_THEMES; n++) fprintf(stderr, "\t%s\n", themes[n].name); exit(EXIT_FAILURE); } /** * Activate the previous theme. * * This picks the theme that comes before the currently active one, or the last * available theme, if the current one is the first. * * \sa \ref theme_next(). */ const struct gui_theme *theme_prev(void) { if (current_theme_num == 0) current_theme_num = NUM_THEMES - 1; else current_theme_num--; return themes + current_theme_num; } /** * Activate the next theme. * * This works exactly as \ref theme_prev() but cycles forwards through the * list of available themes. */ const struct gui_theme *theme_next(void) { current_theme_num = ((current_theme_num + 1) % NUM_THEMES); return themes + current_theme_num; }