/* SPDX-License-Identifier: GPL-2.0 */ /** \file gui.c Curses-based user interface. * * This file contains the bulk of the source code of para_gui(1), including * main(). Another related source file is \ref gui_theme.c, which contains * the theme definitions. * * para_gui(1) employs the scheduler to perform I/O on various file descriptors * without threading. It does not use the buffer tree API, though. * * At startup the stat subcommand of para_audioc(1) is executed to obtain * status information about para_server(1) and para_audiod(1). Moreover, * the program runs arbitrary user-defined programs via configurable key * bindings. The helper functions of \ref exec.c are called to execute both * types of programs. * * The program also maintains a simple ring buffer, implemented in \ref * ringbuffer.c, to remember recent command output. */ /** \cond doxygen_ignore */ #include #include #include #include #include #include #include #include #include #include #include "gui.lsg.h" #include "para.h" #include "gui.h" #include "lsu.h" #include "string.h" #include "ringbuffer.h" #include "fd.h" #include "error.h" #include "list.h" #include "sched.h" #include "signal.h" #include "audioc.h" #include "sideband.h" #include "net.h" DEFINE_PARA_ERRLIST; static struct lls_parse_result *cmdline_lpr, *lpr; #define CMD_PTR (lls_cmd(0, gui_suite)) #define OPT_RESULT(_name) (lls_opt_result(LSG_GUI_PARA_GUI_OPT_ ## _name, lpr)) #define OPT_GIVEN(_name) (lls_opt_given(OPT_RESULT(_name))) #define OPT_STRING_VAL(_name) (lls_string_val(0, OPT_RESULT(_name))) #define OPT_UINT32_VAL(_name) (lls_uint32_val(0, OPT_RESULT(_name))) #define FOR_EACH_KEY_MAP(_i) for (_i = 0; _i < OPT_GIVEN(KEY_MAP); _i++) static char *stat_content[NUM_STAT_ITEMS]; static struct gui_window { WINDOW *win; bool needs_update; } top, bot, sbar, vbar, pbar; /* status, version, progress bar */ /* How many lines of output to remember. */ #define RINGBUFFER_SIZE 512 struct rb_entry { char *msg; size_t width; /* see strwidth() */ int color; }; static struct ringbuffer *bot_win_rb; static unsigned scroll_position; static pid_t exec_pid; static int exec_fds[2] = {-1, -1}; static int loglevel; /* Type of the process currently being executed. */ enum exec_status { EXEC_IDLE, /**< No process running. */ EXEC_DCMD, /**< para or display process running. */ EXEC_XCMD, /**< External process running. */ }; /* * Codes for various colors. * * Each status item has its own color pair. The enumeration items defined here * start at a higher number so that they do not overlap with the status items. */ enum gui_color_pair { COLOR_STATUSBAR = NUM_STAT_ITEMS + 1, COLOR_COMMAND, COLOR_OUTPUT, COLOR_MSG, COLOR_ERRMSG, COLOR_PBAR, COLOR_DURATION, COLOR_TOP, COLOR_BOT, }; struct gui_command { const char *key; const char *name; const char *description; void (*handler)(void); }; static const struct gui_theme *theme; #define GUI_COMMANDS \ GUI_COMMAND(help, "?", "print help") \ GUI_COMMAND(enlarge_top_win, "+", "enlarge the top window") \ GUI_COMMAND(shrink_top_win, "-", "shrink the top window") \ GUI_COMMAND(reread_conf, "r", "reread configuration file") \ GUI_COMMAND(quit, "q", "exit para_gui") \ GUI_COMMAND(refresh, "^L", "redraw the screen") \ GUI_COMMAND(clear_bot_win, "^B", "clear bottom window") \ GUI_COMMAND(next_theme, ".", "switch to next theme") \ GUI_COMMAND(prev_theme, ",", "switch to previous theme") \ GUI_COMMAND(ll_incr, ">", "increase loglevel (decreases verbosity)") \ GUI_COMMAND(ll_decr, "<", "decrease loglevel (increases verbosity)") \ GUI_COMMAND(scroll_up, "", "scroll up one line") \ GUI_COMMAND(scroll_down, "", "scroll_down") \ GUI_COMMAND(page_up, "", "scroll up one page") \ GUI_COMMAND(page_down, "", "scroll down one page") \ GUI_COMMAND(scroll_top, "", "scroll to top of buffer") \ GUI_COMMAND(cancel_scroll, "", "deactivate scroll mode") \ /* declare command handlers */ #define GUI_COMMAND(_c, _k, _d) \ static void com_ ## _c(void); GUI_COMMANDS #undef GUI_COMMAND /* define command array */ #define GUI_COMMAND(_c, _k, _d) \ { \ .key = _k, \ .name = #_c, \ .description = _d, \ .handler = com_ ## _c \ }, static struct gui_command command_list[] = {GUI_COMMANDS}; struct input_task { struct task *task; }; struct status_task { struct task *task; struct sb_context *ctx; struct timeval connect_barrier; int fd; }; /* Stdout/stderr of the executing process is read in chunks of this size. */ #define COMMAND_BUF_SIZE 32768 struct exec_task { struct task *task; char command_buf[2][COMMAND_BUF_SIZE]; /* stdout/stderr of command */ int cbo[2]; /* command buf offsets */ unsigned flags[2]; /* passed to for_each_line() */ }; static int find_cmd_by_name(const char *name) { for (int i = 0; i < ARRAY_SIZE(command_list); i++) if (!strcmp(command_list[i].name, name)) return i; return -1; } /* Number of on-screen lines needed to show this many (wide) characters. */ static size_t width_to_lines(size_t width) { return 1 + width / COLS; } /* isendwin() returns false before initscr() was called */ static bool curses_active(void) { return top.win && !isendwin(); } /* taken from mutt */ static const char *km_keyname(int c) { static char buf[10]; if (c == KEY_UP) { sprintf(buf, ""); return buf; } if (c == KEY_DOWN) { sprintf(buf, ""); return buf; } if (c == KEY_LEFT) { sprintf(buf, ""); return buf; } if (c == KEY_RIGHT) { sprintf(buf, ""); return buf; } if (c == KEY_NPAGE) { sprintf(buf, ""); return buf; } if (c == KEY_PPAGE) { sprintf(buf, ""); return buf; } if (c == KEY_HOME) { sprintf(buf, ""); return buf; } if (c == KEY_END) { sprintf(buf, ""); return buf; } if (c < 256 && c > -128 && iscntrl((unsigned char) c)) { if (c < 0) c += 256; if (c < 128) { buf[0] = '^'; buf[1] = (c + '@') & 0x7f; buf[2] = 0; } else snprintf(buf, sizeof(buf), "\\%d%d%d", c >> 6, (c >> 3) & 7, c & 7); } else if (c >= KEY_F0 && c < KEY_F(256)) sprintf(buf, "", c - KEY_F0); else if (isprint(c)) snprintf(buf, sizeof(buf), "%c", (unsigned char) c); else snprintf(buf, sizeof(buf), "\\x%hx", (unsigned short) c); return buf; } /* Print given number of spaces to curses window. */ static void add_spaces(WINDOW *win, unsigned int num) { const char space[] = " "; const unsigned sz = sizeof(space) - 1; /* number of spaces */ while (num >= sz) { waddstr(win, space); num -= sz; } if (num > 0) { assert(num < sz); waddstr(win, space + sz - num); } } /** * Print an aligned string to a curses window. * * \param win Print the string to this subwindow. * \param len The width of the given string. * \param align LEFT, RIGHT or CENTER. * * This function sanitizes the string, and it always prints exactly len chars. * * \sa \ref sanitize_str() */ int align_str(WINDOW *win, const char *str, unsigned int len, unsigned int align) { int ret, num; /* of spaces */ size_t width; char *sstr; /* sanitized string */ if (!win || !str) return 0; ret = sanitize_str(str, len, &sstr, &width); if (ret < 0) { PARA_ERROR_LOG("%s\n", para_strerror(-ret)); width = 0; sstr = para_strdup(NULL); } assert(width <= len); num = len - width; if (align == LEFT) { waddstr(win, sstr); add_spaces(win, num); } else if (align == RIGHT) { add_spaces(win, num); waddstr(win, sstr); } else { add_spaces(win, num / 2); waddstr(win, sstr); add_spaces(win, num - num / 2); } free(sstr); return 1; } static bool window_update_needed(void) { return top.needs_update || bot.needs_update || vbar.needs_update || sbar.needs_update || pbar.needs_update; } __printf_2_3 static void print_status_bar(int color, const char *fmt,...) { char *msg; va_list ap; if (!curses_active()) return; wattron(sbar.win, COLOR_PAIR(color)); va_start(ap, fmt); xvasprintf(&msg, fmt, ap); va_end(ap); wmove(sbar.win, 0, 0); align_str(sbar.win, msg, COLS, LEFT); free(msg); sbar.needs_update = true; } static void print_version_and_hostname(void) { char *tmp, *hostname_str = NULL; if (OPT_GIVEN(PRINT_HOSTNAME)) hostname_str = make_message(" on %s", para_hostname()); tmp = make_message("para_gui-%s%s (hit ? for help)", paraslash_version(), hostname_str? hostname_str : ""); free(hostname_str); wmove(vbar.win, 0, 0); align_str(vbar.win, tmp, COLS, CENTER); free(tmp); } /* * get the number of the oldest rbe that is (partially) visible. On return, * lines contains the sum of the number of lines of all visible entries. If the * first one is only partially visible, lines is greater than bot.lines. */ static int first_visible_rbe(unsigned *lines) { int i, bot_lines = getmaxy(bot.win); *lines = 0; for (i = scroll_position; i < RINGBUFFER_SIZE; i++) { struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i); int rbe_lines; if (!rbe) return i - 1; rbe_lines = width_to_lines(rbe->width); if (rbe_lines > bot_lines) return -1; *lines += rbe_lines; if (*lines >= bot_lines) return i; } return RINGBUFFER_SIZE - 1; } /* * Returns the number of the first visible rbe. After the call, *lines is * the number of lines drawn. */ static int draw_top_rbe(unsigned *lines) { int bot_cols, bot_lines, ret, fvr = first_visible_rbe(lines); struct rb_entry *rbe; size_t bytes_to_skip, cells_to_skip, width; if (fvr < 0) return -1; wmove(bot.win, 0, 0); rbe = ringbuffer_get(bot_win_rb, fvr); if (!rbe) return -1; getmaxyx(bot.win, bot_lines, bot_cols); if (*lines > bot_lines) { /* rbe is partially visible multi-line */ cells_to_skip = (*lines - bot_lines) * bot_cols; ret = skip_cells(rbe->msg, cells_to_skip, &bytes_to_skip); if (ret < 0) return ret; ret = strwidth(rbe->msg + bytes_to_skip, &width); if (ret < 0) return ret; } else { bytes_to_skip = 0; width = rbe->width; } wattron(bot.win, COLOR_PAIR(rbe->color)); waddstr(bot.win, rbe->msg + bytes_to_skip); *lines = width_to_lines(width); return fvr; } static void redraw_bot_win(void) { unsigned lines; int i, bot_lines = getmaxy(bot.win); wmove(bot.win, 0, 0); wclear(bot.win); i = draw_top_rbe(&lines); if (i <= 0) goto out; while (i > 0 && lines < bot_lines) { struct rb_entry *rbe = ringbuffer_get(bot_win_rb, --i); if (!rbe) { lines++; waddstr(bot.win, "\n"); continue; } lines += width_to_lines(rbe->width); wattron(bot.win, COLOR_PAIR(rbe->color)); waddstr(bot.win, "\n"); waddstr(bot.win, rbe->msg); } out: bot.needs_update = true; } static void rb_add_entry(int color, char *msg) { struct rb_entry *old, *new; int x, y; size_t width; if (strwidth(msg, &width) < 0) return; new = alloc(sizeof(struct rb_entry)); new->color = color; new->width = width; new->msg = msg; old = ringbuffer_add(bot_win_rb, new); if (old) { free(old->msg); free(old); } if (scroll_position) { /* discard current scrolling, like xterm does */ scroll_position = 0; redraw_bot_win(); return; } wattron(bot.win, COLOR_PAIR(color)); getyx(bot.win, y, x); if (y || x) waddstr(bot.win, "\n"); waddstr(bot.win, msg); } /* Print formatted output to bot win and refresh. */ __printf_2_3 static void outputf(int color, const char *fmt,...) { char *msg; va_list ap; if (!curses_active()) return; va_start(ap, fmt); xvasprintf(&msg, fmt, ap); va_end(ap); rb_add_entry(color, msg); bot.needs_update = true; } static int add_output_line(char *line, void *data) { int color = *(int *)data? COLOR_ERRMSG : COLOR_OUTPUT; if (!curses_active()) return 1; rb_add_entry(color, para_strdup(line)); return 1; } static __printf_2_3 void curses_log(int ll, const char *fmt,...) { va_list ap; if (ll < loglevel) return; va_start(ap, fmt); if (curses_active()) { int color = ll <= LL_NOTICE? COLOR_MSG : COLOR_ERRMSG; char *msg; unsigned bytes = xvasprintf(&msg, fmt, ap); if (bytes > 0 && msg[bytes - 1] == '\n') msg[bytes - 1] = '\0'; /* cut trailing newline */ rb_add_entry(color, msg); bot.needs_update = true; } else if (exec_pid <= 0) /* no external command running */ vfprintf(stderr, fmt, ap); va_end(ap); } /* The log function of para_gui, always set to curses_log(). */ __printf_2_3 void (*para_log)(int, const char *, ...) = curses_log; /* Call endwin() to reset the terminal into non-visual mode. */ static void shutdown_curses(void) { /* * If para_gui received a terminating signal in external mode, the * terminal can be in an unusable state at this point because the child * process might not have caught the signal. In this case endwin() has * already been called and must not be called again. So we first return * to program mode, then call endwin(). */ if (!curses_active()) reset_prog_mode(); endwin(); } /* Disable curses, print a message, kill running processes and exit. */ __noreturn __printf_2_3 static void die(int exit_code, const char *fmt, ...) { va_list argp; /* Kill every process in our process group. */ para_sigaction(SIGTERM, SIG_IGN); kill(0, SIGTERM); /* Wait up to two seconds for child processes to die. */ alarm(2); while (waitpid(0, NULL, 0) >= 0) ; /* nothing */ alarm(0); /* mousemask() exists only in ncurses */ mousemask(~(mmask_t)0, NULL); /* Avoid bad terminal state with xterm. */ shutdown_curses(); va_start(argp, fmt); vfprintf(stderr, fmt, argp); va_end(argp); exit(exit_code); } /* Print modified stat items to curses window. */ static void print_stat_items(uint64_t mask) { if (!curses_active()) return; wattron(top.win, COLOR_PAIR(COLOR_TOP)); if (theme->print_status_items(mask, stat_content, top.win)) top.needs_update = true; } /* 1:23 [4:56] (78%/9:01) */ static void print_progress_bar(void) { int i, num_bars, cut; unsigned dlen, play_time, duration, mins; char *dstr, *colon, *item; wmove(pbar.win, 0, 0); pbar.needs_update = true; item = stat_content[SI_play_time]; if (!item) goto empty; if (*item == '~') item++; mins = atoi(item); /* 1 */ colon = strchr(item, ':'); if (!colon || colon[1] == '\0' || colon[2] == '\0') goto empty; item = stat_content[SI_seconds_total]; if (!item) goto empty; duration = atoi(item); if (duration == 0) goto empty; /* success */ dlen = xasprintf(&dstr, " %u:%02u", duration / 60, duration % 60); num_bars = COLS - dlen; play_time = mins * 60 + (colon[1] - '0') * 10 + colon[2] - '0'; /* 83 */ cut = (num_bars * play_time + duration / 2) / duration; wattron(pbar.win, COLOR_PAIR(COLOR_PBAR)); for (i = 0; i < cut; i++) waddstr(pbar.win, "▒"); for (; i < num_bars; i++) waddstr(pbar.win, "░"); wattron(pbar.win, COLOR_PAIR(COLOR_DURATION)); waddstr(pbar.win, dstr); free(dstr); return; empty: whline(pbar.win, ' ', COLS); } static void update_items(uint64_t mask) { if (status_item_bit_set(SI_play_time, mask)) print_progress_bar(); if (status_item_bit_set(SI_file_size, mask) && stat_content[SI_file_size]) { int64_t x; char unit; if (para_atoi64(stat_content[SI_file_size], &x) < 0) x = 0; if (x < 2 * 1024) unit = 'K'; else if (x < 2 * 1024 * 1024) { x /= 1024; unit = 'M'; } else { x /= 1024 * 1024; unit = 'G'; } free(stat_content[SI_file_size]); stat_content[SI_file_size] = make_message("%" PRId64 "%c", x, unit); } print_stat_items(mask); } static void status_pre_monitor(struct sched *s, void *context) { struct status_task *st = context; if (st->fd >= 0) sched_monitor_readfd(st->fd, s); else sched_request_barrier_or_min_delay(&st->connect_barrier, s); } static int recv_status(struct sched *s, struct status_task *st) { size_t bufsize = 1024; struct iovec iov; struct sb_buffer sbb; int ret; uint64_t mask; if (st->fd < 0) return 0; if (!sched_read_ok(st->fd, s)) return 0; if (!st->ctx) st->ctx = sb_new_recv(bufsize, NULL, NULL); sb_get_recv_buffer(st->ctx, &iov); ret = recv_bin_buffer(st->fd, iov.iov_base, iov.iov_len); if (ret < 0) return ret; if (ret == 0) return -E_EOF; ret = sb_received(st->ctx, ret, &sbb); if (ret < 0) return ret; if (ret == 0) return 0; st->ctx = NULL; if (!sideband_log("audiod", &sbb)) { mask = parse_status_items(sbb.iov.iov_base, sbb.iov.iov_len, stat_content); update_items(mask); } free(sbb.iov.iov_base); return 0; } static int status_post_monitor(struct sched *s, void *context) { struct status_task *st = context; int ret, i; const char *socket_name = OPT_GIVEN(SOCKET)? OPT_STRING_VAL(SOCKET) : NULL; char *argv[] = {"stat", "-p"}; if (st->fd >= 0) { ret = recv_status(s, st); if (ret < 0) goto close_fd; return 0; } /* Avoid busy loop */ if (tv_diff(&st->connect_barrier, now, NULL) > 0) return 0; st->connect_barrier.tv_sec = now->tv_sec + 2; ret = connect_audiod(socket_name, ARRAY_SIZE(argv), argv); if (ret < 0) goto log_error; st->fd = ret; ret = mark_fd_nonblocking(st->fd); if (ret < 0) goto close_fd; return 0; close_fd: PARA_NOTICE_LOG("closing status file descriptor\n"); close(st->fd); st->fd = -1; log_error: sb_free(st->ctx); st->ctx = NULL; FOR_EACH_STATUS_ITEM(i) { free(stat_content[i]); stat_content[i] = para_strdup(""); } print_stat_items(~0ULL); /* print all items */ print_progress_bar(); PARA_NOTICE_LOG("%s\n", para_strerror(-ret)); return 0; } /* Initialize all windows. */ static void init_wins(int top_lines) { int top_y = 0, bot_y = top_lines + 1, sb_y = LINES - 2, in_y = LINES - 1; int bot_lines = LINES - top_lines - 3, sb_lines = 1, in_lines = 1; assume_default_colors(theme->dflt.fg, theme->dflt.bg); if (top.win) { wresize(top.win, top_lines, COLS); mvwin(top.win, top_y, 0); wresize(vbar.win, sb_lines, COLS); mvwin(vbar.win, sb_y, 0); wresize(pbar.win, 1, COLS); mvwin(pbar.win, top_lines, 0); wresize(bot.win, bot_lines, COLS); mvwin(bot.win, bot_y, 0); wresize(sbar.win, in_lines, COLS); mvwin(sbar.win, in_y, 0); } else { pbar.win = newwin(1, COLS, top_lines, 0); top.win = newwin(top_lines, COLS, top_y, 0); bot.win = newwin(bot_lines, COLS, bot_y, 0); vbar.win = newwin(sb_lines, COLS, sb_y, 0); sbar.win = newwin(in_lines, COLS, in_y, 0); if (!top.win || !bot.win || !vbar.win || !sbar.win || !pbar.win) die(EXIT_FAILURE, "Error: Cannot create curses windows\n"); wclear(bot.win); wclear(vbar.win); wclear(sbar.win); scrollok(bot.win, 1); wattron(vbar.win, COLOR_PAIR(COLOR_STATUSBAR)); wattron(pbar.win, COLOR_PAIR(COLOR_PBAR)); wattron(pbar.win, COLOR_PAIR(COLOR_DURATION)); wattron(bot.win, COLOR_PAIR(COLOR_BOT)); wattron(top.win, COLOR_PAIR(COLOR_TOP)); nodelay(top.win, 1); nodelay(bot.win, 1); nodelay(vbar.win, 1); nodelay(sbar.win, 0); keypad(top.win, 1); keypad(bot.win, 1); keypad(vbar.win, 1); keypad(sbar.win, 1); } wclear(top.win); print_stat_items(~0ULL); /* print all items */ print_progress_bar(); wnoutrefresh(top.win); wnoutrefresh(bot.win); print_version_and_hostname(); wnoutrefresh(vbar.win); wnoutrefresh(sbar.win); wnoutrefresh(pbar.win); doupdate(); } static void init_pair_or_die(short pair, short f, short b) { if (init_pair(pair, f, b) == ERR) die(EXIT_FAILURE, "fatal: init_pair() failed\n"); } static void init_colors_or_die(void) { if (!has_colors()) die(EXIT_FAILURE, "fatal: No color term\n"); if (start_color() == ERR) die(EXIT_FAILURE, "fatal: failed to start colors\n"); init_pair_or_die(COLOR_STATUSBAR, theme->vbar.fg, theme->vbar.bg); init_pair_or_die(COLOR_COMMAND, theme->cmd.fg, theme->cmd.bg); init_pair_or_die(COLOR_OUTPUT, theme->output.fg, theme->output.bg); init_pair_or_die(COLOR_MSG, theme->msg.fg, theme->msg.bg); init_pair_or_die(COLOR_ERRMSG, theme->err_msg.fg, theme->err_msg.bg); init_pair_or_die(COLOR_PBAR, theme->pbar.fg, theme->pbar.bg); init_pair_or_die(COLOR_DURATION, theme->duration.fg, theme->duration.bg); init_pair_or_die(COLOR_TOP, theme->top.fg, theme->top.bg); init_pair_or_die(COLOR_BOT, theme->bot.fg, theme->bot.bg); } /* (Re-)initialize the curses library. */ static void init_curses(void) { if (curses_active()) return; if (refresh() == ERR) /* refresh is really needed */ die(EXIT_FAILURE, "refresh() failed\n"); if (LINES < theme->lines_min || COLS < theme->cols_min) die(EXIT_FAILURE, "Terminal (%dx%d) too small" " (need at least %dx%d)\n", COLS, LINES, theme->cols_min, theme->lines_min); curs_set(0); /* make cursor invisible, ignore errors */ nonl(); /* do not NL->CR/NL on output, always returns OK */ /* don't echo input */ if (noecho() == ERR) die(EXIT_FAILURE, "fatal: noecho() failed\n"); /* take input chars one at a time, no wait for \n */ if (cbreak() == ERR) die(EXIT_FAILURE, "fatal: cbreak() failed\n"); init_colors_or_die(); clear(); /* ignore non-fatal errors */ init_wins(theme->top_lines_default); // noecho(); /* don't echo input */ } /* * This sucker modifies its first argument. *handler and *arg are * pointers to 0-terminated strings (inside line). Crap. */ static int split_key_map(char *line, char **handler, char **arg) { if (!(*handler = strchr(line + 1, ':'))) goto err_out; **handler = '\0'; (*handler)++; if (!(*arg = strchr(*handler, ':'))) goto err_out; **arg = '\0'; (*arg)++; return 1; err_out: return 0; } static void check_key_map_args_or_die(void) { int i; char *tmp = NULL; const struct lls_opt_result *lor = OPT_RESULT(KEY_MAP); FOR_EACH_KEY_MAP(i) { char *handler, *arg; free(tmp); tmp = para_strdup(lls_string_val(i, lor)); if (!split_key_map(tmp, &handler, &arg)) break; if (strlen(handler) != 1) break; if (*handler != 'x' && *handler != 'd' && *handler != 'i' && *handler != 'p') break; if (*handler != 'i') continue; if (find_cmd_by_name(arg) < 0) break; } if (i != OPT_GIVEN(KEY_MAP)) die(EXIT_FAILURE, "invalid key map: %s\n", lls_string_val(i, lor)); free(tmp); } static void parse_config_file_or_die(bool reload) { int ret; unsigned flags = MCF_DONT_FREE; if (lpr != cmdline_lpr) lls_free_parse_result(lpr, CMD_PTR); lpr = cmdline_lpr; if (reload) flags |= MCF_OVERRIDE; ret = lsu_merge_config_file_options(OPT_STRING_VAL(CONFIG_FILE), "gui.conf", &lpr, CMD_PTR, gui_suite, flags); if (ret < 0) { PARA_EMERG_LOG("failed to parse config file: %s\n", para_strerror(-ret)); exit(EXIT_FAILURE); } loglevel = OPT_UINT32_VAL(LOGLEVEL); check_key_map_args_or_die(); theme = theme_init(OPT_STRING_VAL(THEME)); } /* Reread configuration, terminate on errors. */ static void com_reread_conf(void) { /* * If the reload of the config file fails, we are about to exit. In * this case we print the error message to stderr rather than to the * curses window. So we have to shutdown curses first. */ shutdown_curses(); parse_config_file_or_die(true); init_curses(); print_status_bar(COLOR_MSG, "config file reloaded\n"); } /* React to various signal-related events. */ static int signal_post_monitor(struct sched *s, __a_unused void *context) { int ret = para_next_signal(); if (ret <= 0) return 0; switch (ret) { case SIGTERM: die(EXIT_FAILURE, "only the good die young (caught SIGTERM)\n"); case SIGWINCH: PARA_NOTICE_LOG("got SIGWINCH\n"); if (curses_active()) { shutdown_curses(); init_curses(); redraw_bot_win(); } return 1; case SIGCHLD: task_notify_all(s, E_GUI_SIGCHLD); return 1; } return 1; } static enum exec_status exec_status(void) { if (exec_fds[0] >= 0 || exec_fds[1] >= 0) return EXEC_DCMD; if (exec_pid > 0) return EXEC_XCMD; return EXEC_IDLE; } static void exec_pre_monitor(struct sched *s, __a_unused void *context) { if (exec_fds[0] >= 0) sched_monitor_readfd(exec_fds[0], s); if (exec_fds[1] >= 0) sched_monitor_readfd(exec_fds[1], s); } static int exec_post_monitor(__a_unused struct sched *s, void *context) { struct exec_task *ct = context; int i, ret; ret = task_get_notification(ct->task); if (ret == -E_GUI_SIGCHLD && exec_pid > 0) { int exit_status; if (waitpid(exec_pid, &exit_status, WNOHANG) == exec_pid) { exec_pid = 0; init_curses(); PARA_INFO_LOG("command exit status: %d", exit_status); print_status_bar(COLOR_MSG, " "); } } for (i = 0; i < 2; i++) { size_t sz; if (exec_fds[i] < 0) continue; ret = read_nonblock(exec_fds[i], ct->command_buf[i] + ct->cbo[i], COMMAND_BUF_SIZE - 1 - ct->cbo[i], &sz); ct->cbo[i] += sz; sz = ct->cbo[i]; ct->cbo[i] = for_each_line(ct->flags[i], ct->command_buf[i], ct->cbo[i], add_output_line, &i); if (sz != ct->cbo[i]) { /* at least one line found */ bot.needs_update = true; ct->flags[i] = 0; } if (ret < 0 || exec_pid == 0) { if (ret < 0 && ret != -E_EOF) PARA_ERROR_LOG("closing command fd %d: %s", i, para_strerror(-ret)); close(exec_fds[i]); exec_fds[i] = -1; ct->flags[i] = 0; ct->cbo[i] = 0; if (exec_fds[!i] < 0) /* both fds closed */ return 1; } if (ct->cbo[i] == COMMAND_BUF_SIZE - 1) { PARA_NOTICE_LOG("discarding overlong line"); ct->cbo[i] = 0; ct->flags[i] = FELF_DISCARD_FIRST; } } return 0; } static void input_pre_monitor(struct sched *s, __a_unused void *context) { if (exec_status() != EXEC_XCMD) { sched_monitor_readfd(STDIN_FILENO, s); if (window_update_needed()) sched_min_delay(s); } } /* Read from command pipe and print data to bot window. */ static void exec_and_display(const char *file_and_args) { int ret, fds[3] = {0, 1, 1}; outputf(COLOR_COMMAND, "%s", file_and_args); ret = xexec(&exec_pid, file_and_args, fds); if (ret < 0) return; ret = mark_fd_nonblocking(fds[1]); if (ret < 0) goto fail; ret = mark_fd_nonblocking(fds[2]); if (ret < 0) goto fail; exec_fds[0] = fds[1]; exec_fds[1] = fds[2]; print_status_bar(COLOR_MSG, "hit any key to abort\n"); return; fail: PARA_ERROR_LOG("%s\n", para_strerror(-ret)); close(fds[1]); close(fds[2]); } static void exec_para(const char *args) { char *file_and_args = make_message("para_client -- %s", args); exec_and_display(file_and_args); free(file_and_args); } /* Shutdown curses and stat pipe before executing external commands. */ static void exec_external(char *file_and_args) { int fds[3] = {-1, -1, -1}; if (exec_pid) return; shutdown_curses(); xexec(&exec_pid, file_and_args, fds); } static void handle_command(int c) { int i; const struct lls_opt_result *lor = OPT_RESULT(KEY_MAP); const char *keyname = km_keyname(c); /* first check user-defined key bindings */ FOR_EACH_KEY_MAP(i) { char *tmp, *handler, *arg; tmp = para_strdup(lls_string_val(i, lor)); if (!split_key_map(tmp, &handler, &arg)) { free(tmp); return; } if (strcmp(tmp, keyname)) { free(tmp); continue; } if (*handler == 'd') exec_and_display(arg); else if (*handler == 'x') exec_external(arg); else if (*handler == 'p') exec_para(arg); else if (*handler == 'i') { int num = find_cmd_by_name(arg); if (num >= 0) command_list[num].handler(); } free(tmp); return; } /* not found, check internal key bindings */ for (i = 0; i < ARRAY_SIZE(command_list); i++) { if (!strcmp(keyname, command_list[i].key)) { command_list[i].handler(); return; } } print_status_bar(COLOR_ERRMSG, "key '%s' is not bound, press ? for help", keyname); } static int input_post_monitor(__a_unused struct sched *s, __a_unused void *context) { int ret; enum exec_status exs = exec_status(); if (exs == EXEC_XCMD) return 0; if (window_update_needed()) { if (top.needs_update) assert(wnoutrefresh(top.win) == OK); if (bot.needs_update) assert(wnoutrefresh(bot.win) == OK); if (pbar.needs_update) assert(wnoutrefresh(pbar.win) == OK); if (vbar.needs_update) assert(wnoutrefresh(vbar.win) == OK); if (sbar.needs_update) assert(wnoutrefresh(sbar.win) == OK); doupdate(); top.needs_update = bot.needs_update = sbar.needs_update = vbar.needs_update = pbar.needs_update = false; } ret = wgetch(top.win); if (ret == ERR) return 0; if (ret == KEY_RESIZE) /* already handled in signal_post_monitor() */ return 0; if (exs == EXEC_IDLE) handle_command(ret); else if (exec_pid > 0) /* negate to kill whole process group */ kill(-exec_pid, SIGTERM); return 0; } static void print_scroll_msg(void) { unsigned lines_total, filled = ringbuffer_filled(bot_win_rb); int first_rbe = first_visible_rbe(&lines_total); print_status_bar(COLOR_MSG, "scrolled view: %u-%u/%u\n", filled - first_rbe, filled - scroll_position, ringbuffer_filled(bot_win_rb)); } static void com_scroll_top(void) { int i = RINGBUFFER_SIZE - 1, bot_lines = getmaxy(bot.win); unsigned lines = 0; while (i > 0 && !ringbuffer_get(bot_win_rb, i)) i--; /* i is oldest entry */ for (; lines < bot_lines && i >= 0; i--) { struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i); if (!rbe) break; lines += width_to_lines(rbe->width); } i++; if (lines > 0 && scroll_position != i) { scroll_position = i; redraw_bot_win(); print_scroll_msg(); return; } print_status_bar(COLOR_ERRMSG, "top of buffer is shown\n"); } static void com_cancel_scroll(void) { if (scroll_position == 0) { print_status_bar(COLOR_ERRMSG, "bottom of buffer is shown\n"); return; } scroll_position = 0; redraw_bot_win(); print_status_bar(COLOR_MSG, " "); } static void com_page_down(void) { unsigned lines = 0; int i = scroll_position, bot_lines = getmaxy(bot.win); while (lines < bot_lines && --i > 0) { struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i); if (!rbe) break; lines += width_to_lines(rbe->width); } if (lines) { scroll_position = i; redraw_bot_win(); print_scroll_msg(); return; } print_status_bar(COLOR_ERRMSG, "bottom of buffer is shown\n"); } static void com_page_up(void) { unsigned lines; int fvr = first_visible_rbe(&lines), bot_lines = getmaxy(bot.win); if (fvr < 0 || fvr + 1 >= ringbuffer_filled(bot_win_rb)) { print_status_bar(COLOR_ERRMSG, "top of buffer is shown\n"); return; } scroll_position = fvr + 1; for (; scroll_position > 0; scroll_position--) { first_visible_rbe(&lines); if (lines == bot_lines) break; } redraw_bot_win(); print_scroll_msg(); } static void com_scroll_down(void) { struct rb_entry *rbe; int rbe_lines, bot_lines = getmaxy(bot.win); if (!scroll_position) { print_status_bar(COLOR_ERRMSG, "bottom of buffer is shown\n"); return; } scroll_position--; rbe = ringbuffer_get(bot_win_rb, scroll_position); rbe_lines = width_to_lines(rbe->width); wscrl(bot.win, rbe_lines); wmove(bot.win, bot_lines - rbe_lines, 0); wattron(bot.win, COLOR_PAIR(rbe->color)); waddstr(bot.win, rbe->msg); bot.needs_update = true; print_scroll_msg(); } static void com_scroll_up(void) { struct rb_entry *rbe = NULL; unsigned lines; int i, first_rbe, num_scroll; /* the entry that is going to vanish */ rbe = ringbuffer_get(bot_win_rb, scroll_position); if (!rbe) goto err_out; num_scroll = width_to_lines(rbe->width); first_rbe = first_visible_rbe(&lines); if (first_rbe < 0 || (first_rbe == ringbuffer_filled(bot_win_rb) - 1)) goto err_out; scroll_position++; wscrl(bot.win, -num_scroll); i = draw_top_rbe(&lines); if (i < 0) goto err_out; while (i > 0 && lines < num_scroll) { int rbe_lines; rbe = ringbuffer_get(bot_win_rb, --i); if (!rbe) break; rbe_lines = width_to_lines(rbe->width); lines += rbe_lines; wattron(bot.win, COLOR_PAIR(rbe->color)); waddstr(bot.win, "\n"); waddstr(bot.win, rbe->msg); if (!i) break; i--; } bot.needs_update = true; print_scroll_msg(); return; err_out: print_status_bar(COLOR_ERRMSG, "top of buffer is shown\n"); } static void print_ll_msg(void) { const char *sev[] = {SEVERITIES}; print_status_bar(COLOR_MSG, "new loglevel: %s\n", sev[loglevel]); } static void com_ll_decr(void) { if (loglevel <= LL_DEBUG) { print_status_bar(COLOR_ERRMSG, "loglevel already at maximal verbosity\n"); return; } loglevel--; print_ll_msg(); } static void com_ll_incr(void) { if (loglevel >= LL_EMERG) { print_status_bar(COLOR_ERRMSG, "loglevel already at minimal verbosity\n"); return; } loglevel++; print_ll_msg(); } static void com_help(void) { int i; const struct lls_opt_result *lor = OPT_RESULT(KEY_MAP); FOR_EACH_KEY_MAP(i) { char *handler, *arg, *tmp = para_strdup(lls_string_val(i, lor)); const char *handler_text = "???", *desc = NULL; if (!split_key_map(tmp, &handler, &arg)) { free(tmp); return; } switch (*handler) { case 'i': handler_text = "internal"; desc = command_list[find_cmd_by_name(arg)].description; break; case 'x': handler_text = "external"; break; case 'd': handler_text = "display "; break; case 'p': handler_text = "para "; break; } outputf(COLOR_OUTPUT, "%s\t%s\t%s%s\t%s", tmp, handler_text, arg, strlen(arg) < 8? "\t" : "", desc? desc : ""); free(tmp); } for (i = 0; i < ARRAY_SIZE(command_list); i++) { struct gui_command gc = command_list[i]; outputf(COLOR_OUTPUT, "%s\tinternal\t%s\t%s%s", gc.key, gc.name, strlen(gc.name) < 8? "\t" : "", gc.description); } print_status_bar(COLOR_MSG, "try \"para_gui -h\" or \"para_client help\" " "for more info"); } static void com_shrink_top_win(void) { int top_lines = getmaxy(top.win); if (top_lines <= theme->top_lines_min) { PARA_WARNING_LOG("can not decrease top window\n"); return; } init_wins(top_lines - 1); print_status_bar(COLOR_MSG, "%s", "decreased top window"); } static void com_enlarge_top_win(void) { int top_lines = getmaxy(top.win), bot_lines = getmaxy(bot.win); if (bot_lines < 3) { PARA_WARNING_LOG("can not increase top window\n"); return; } init_wins(top_lines + 1); print_status_bar(COLOR_MSG, "increased top window"); } static void com_clear_bot_win(void) { ringbuffer_flush(bot_win_rb, free); wclear(bot.win); bot.needs_update = true; } __noreturn static void com_quit(void) { die(EXIT_SUCCESS, "%s", ""); } static void com_refresh(void) { shutdown_curses(); init_curses(); } static void switch_theme(bool next) { theme = next? theme_next() : theme_prev(); scroll_position = 0; init_wins(theme->top_lines_default); init_colors_or_die(); redraw_bot_win(); print_status_bar(COLOR_MSG, "new theme: %s", theme->name); } static void com_next_theme(void) { switch_theme(true); } static void com_prev_theme(void) { switch_theme(false); } /* * All four post-monitor methods always return non-negative, so the call to * \ref schedule() should only return if there was an out-of-memory condition. */ __noreturn static void setup_tasks_and_schedule(void) { struct exec_task exec_task = {.task = NULL}; struct status_task status_task = {.fd = -1}; struct input_task input_task = {.task = NULL}; struct signal_task signal_task; struct sched *sched = sched_new(NULL); exec_task.task = task_register(&(struct task_info) { .name = "exec", .pre_monitor = exec_pre_monitor, .post_monitor = exec_post_monitor, .context = &exec_task, }, sched); status_task.task = task_register(&(struct task_info) { .name = "status", .pre_monitor = status_pre_monitor, .post_monitor = status_post_monitor, .context = &status_task, }, sched); input_task.task = task_register(&(struct task_info) { .name = "input", .pre_monitor = input_pre_monitor, .post_monitor = input_post_monitor, .context = &input_task, }, sched); signal_task.fd = signal_init(); para_sigaction(SIGINT, SIG_IGN); para_install_sighandler(SIGTERM); para_install_sighandler(SIGCHLD); para_install_sighandler(SIGWINCH); signal_task.task = task_register(&(struct task_info) { .name = "signal", .pre_monitor = signal_pre_monitor, .post_monitor = signal_post_monitor, .context = &signal_task, }, sched); schedule(sched); exit(EXIT_FAILURE); /* only reached on OOM */ } static void handle_help_flags(void) { char *help; if (OPT_GIVEN(DETAILED_HELP)) help = lls_long_help(CMD_PTR); else if (OPT_GIVEN(HELP)) help = lls_short_help(CMD_PTR); else return; printf("%s\n", help); free(help); exit(EXIT_SUCCESS); } /** \endcond * * The main function of para_gui. * * \param argc Options are defined in the gui lopsub suite. * \param argv The gui suite defines no subcommands. * * After initialization para_gui registers the following tasks to the paraslash * scheduler: status, exec, signal, input. * * The status task executes the para_audioc stat command to obtain the status * of para_server and para_audiod, and displays this information in the top * window of para_gui. * * The exec task is responsible for printing the output of the currently * running executable to the bottom window. * * The signal task performs various actions according to signals received. For * example, it recreates all curses windows on SIGWINCH, and it shuts down * the curses system on SIGTERM to restore the terminal settings before exit. * * The input task reads single key strokes from stdin. For each key pressed, it * executes the command handler associated with this key. * * \return EXIT_SUCCESS or EXIT_FAILURE. */ int main(int argc, char *argv[]) { int ret; char *errctx; char *langinfo; ret = lls(lls_parse(argc, argv, CMD_PTR, &cmdline_lpr, &errctx)); if (ret < 0) goto fail; lpr = cmdline_lpr; loglevel = OPT_UINT32_VAL(LOGLEVEL); version_handle_flag("gui", OPT_GIVEN(VERSION)); handle_help_flags(); parse_config_file_or_die(false); setlocale(LC_CTYPE, ""); langinfo = nl_langinfo(CODESET); if (!langinfo || strcmp(langinfo, "UTF-8")) { PARA_EMERG_LOG("fatal: UTF-8 terminal required\n"); return EXIT_FAILURE; } bot_win_rb = ringbuffer_new(RINGBUFFER_SIZE); initscr(); /* needed only once, always successful */ init_curses(); setup_tasks_and_schedule(); /* does not return */ fail: if (errctx) PARA_ERROR_LOG("%s\n", errctx); free(errctx); PARA_EMERG_LOG("%s\n", para_strerror(-ret)); return EXIT_FAILURE; }