gui: Catch SIGWINCH.
[paraslash.git] / gui.c
diff --git a/gui.c b/gui.c
index b239714..69a9243 100644 (file)
--- a/gui.c
+++ b/gui.c
@@ -1,8 +1,4 @@
-/*
- * Copyright (C) 1998-2014 Andre Noll <maan@systemlinux.org>
- *
- * Licensed under the GPL v2. For licencing details see COPYING.
- */
+/* Copyright (C) 1998 Andre Noll <maan@tuebingen.mpg.de>, see file COPYING. */
 
 /** \file gui.c Curses-based interface for paraslash. */
 
@@ -12,8 +8,9 @@
 #include <curses.h>
 #include <locale.h>
 #include <sys/time.h>
+#include <lopsub.h>
 
-#include "gui.cmdline.h"
+#include "gui.lsg.h"
 #include "para.h"
 #include "gui.h"
 #include "string.h"
 #include "list.h"
 #include "sched.h"
 #include "signal.h"
-#include "ggo.h"
 #include "version.h"
 
-/** define the array of error lists needed by para_gui */
-INIT_GUI_ERRLISTS;
-static char *stat_content[NUM_STAT_ITEMS];
+/** Array of error strings. */
+DEFINE_PARA_ERRLIST;
+
+static struct lls_parse_result *cmdline_lpr, *lpr;
 
-static int signal_pipe;
+#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, sb, in, sep;
 
+/** How many lines of output to remember. */
 #define RINGBUFFER_SIZE 512
+
 struct rb_entry {
        char *msg;
        size_t len;
@@ -45,18 +52,15 @@ struct rb_entry {
 static struct ringbuffer *bot_win_rb;
 
 static unsigned scroll_position;
-
-static pid_t cmd_pid;
-
-static int command_fds[2] = {-1, -1};
-static int stat_pipe = -1;
-static struct gui_args_info conf;
+static pid_t exec_pid;
+static int exec_fds[2] = {-1, -1};
 static int loglevel;
 
-enum cmd_status {
-       CMDS_IDLE, /* no command running */
-       CMDS_DCMD, /* para or display command running */
-       CMDS_XCMD, /* external command running */
+/** 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. */
 };
 
 /**
@@ -122,7 +126,30 @@ GUI_COMMANDS
 
 static struct gui_command command_list[] = {GUI_COMMANDS {.name = NULL}};
 
-static int find_cmd_byname(char *name)
+struct input_task {
+       struct task *task;
+};
+
+struct status_task {
+       struct task *task;
+       pid_t pid;
+       char *buf;
+       int bufsize, loaded;
+       struct timeval next_exec;
+       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_byname(const char *name)
 {
        int i;
 
@@ -220,10 +247,10 @@ static char *km_keyname(int c)
 }
 
 /* Print given number of spaces to curses window. */
-static void add_spaces(WINDOWwin, unsigned int num)
+static void add_spaces(WINDOW *win, unsigned int num)
 {
-       char space[] = "                                ";
-       unsigned sz = sizeof(space) - 1; /* number of spaces */
+       const char space[] = "                                ";
+       const unsigned sz = sizeof(space) - 1; /* number of spaces */
 
        while (num >= sz)  {
                waddstr(win, space);
@@ -231,53 +258,57 @@ static void add_spaces(WINDOW* win, unsigned int num)
        }
        if (num > 0) {
                assert(num < sz);
-               space[num] = '\0';
-               waddstr(win, space);
+               waddstr(win, space + sz - num);
        }
 }
 
 /*
- * print aligned string to curses window. This function always prints
+ * Print aligned string to curses window. This function always prints
  * exactly len chars.
  */
-static int align_str(WINDOW* win, char *str, unsigned int len,
+static int align_str(WINDOW *win, const char *str, unsigned int len,
                unsigned int align)
 {
-       int ret, i, num; /* of spaces */
+       int ret, num; /* of spaces */
        size_t width;
+       char *sstr; /* sanitized string */
 
        if (!win || !str)
                return 0;
-       ret = strwidth(str, &width);
+       ret = sanitize_str(str, len, &sstr, &width);
        if (ret < 0) {
                PARA_ERROR_LOG("%s\n", para_strerror(-ret));
                width = 0;
-               str[0] = '\0';
+               sstr = para_strdup(NULL);
        }
+       assert(width <= len);
        num = len - width;
-       if (num < 0) {
-               str[len] = '\0';
-               num = 0;
-       }
-       /* replace control characters by spaces */
-       for (i = 0; i < len && str[i]; i++) {
-               if (str[i] == '\n' || str[i] == '\r' || str[i] == '\f')
-                       str[i] = ' ';
-       }
        if (align == LEFT) {
-               waddstr(win, str);
+               waddstr(win, sstr);
                add_spaces(win, num);
        } else if (align == RIGHT) {
                add_spaces(win, num);
-               waddstr(win, str);
+               waddstr(win, sstr);
        } else {
                add_spaces(win, num / 2);
-               waddstr(win, str[0]? str: "");
+               waddstr(win, sstr);
                add_spaces(win, num - num / 2);
        }
+       free(sstr);
        return 1;
 }
 
+static void refresh_window(struct gui_window *gw)
+{
+       gw->needs_update = true;
+}
+
+static bool window_update_needed(void)
+{
+       return top.needs_update || bot.needs_update || sb.needs_update ||
+               in.needs_update || sep.needs_update;
+}
+
 __printf_2_3 static void print_in_bar(int color, const char *fmt,...)
 {
        char *msg;
@@ -292,7 +323,7 @@ __printf_2_3 static void print_in_bar(int color, const char *fmt,...)
        wmove(in.win, 0, 0);
        align_str(in.win, msg, get_num_cols(&in), LEFT);
        free(msg);
-       wrefresh(in.win);
+       refresh_window(&in);
 }
 
 static void print_status_bar(void)
@@ -388,7 +419,7 @@ static void redraw_bot_win(void)
                waddstr(bot.win, rbe->msg);
        }
 out:
-       wrefresh(bot.win);
+       refresh_window(&bot);
 }
 
 static void rb_add_entry(int color, char *msg)
@@ -421,10 +452,8 @@ static void rb_add_entry(int color, char *msg)
        waddstr(bot.win, msg);
 }
 
-/*
- * print formated output to bot win and refresh
- */
-__printf_2_3 static void outputf(int color, const char* fmt,...)
+/* Print formatted output to bot win and refresh. */
+__printf_2_3 static void outputf(int color, const char *fmt,...)
 {
        char *msg;
        va_list ap;
@@ -435,7 +464,7 @@ __printf_2_3 static void outputf(int color, const char* fmt,...)
        xvasprintf(&msg, fmt, ap);
        va_end(ap);
        rb_add_entry(color, msg);
-       wrefresh(bot.win);
+       refresh_window(&bot);
 }
 
 static int add_output_line(char *line, void *data)
@@ -462,47 +491,55 @@ static __printf_2_3 void curses_log(int ll, const char *fmt,...)
                if (bytes > 0 && msg[bytes - 1] == '\n')
                        msg[bytes - 1] = '\0'; /* cut trailing newline */
                rb_add_entry(color, msg);
-               wrefresh(bot.win);
-       } else if (cmd_pid <= 0) /* no external command running */
+               refresh_window(&bot);
+       } else if (exec_pid <= 0) /* no external command running */
                vfprintf(stderr, fmt, ap);
        va_end(ap);
 }
-__printf_2_3 void (*para_log)(int, const char*, ...) = curses_log;
 
-static void setup_signal_handling(void)
-{
-       signal_pipe = para_signal_init();
-       para_install_sighandler(SIGINT);
-       para_install_sighandler(SIGTERM);
-       para_install_sighandler(SIGCHLD);
-       para_install_sighandler(SIGWINCH);
-       para_install_sighandler(SIGUSR1);
-}
+/** 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)
 {
-       def_prog_mode();
+       /*
+        * 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 charfmt, ...)
+/* 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 */
+#ifdef NCURSES_MOUSE_VERSION
+       mousemask(~(mmask_t)0, NULL); /* Avoid bad terminal state with xterm. */
+#endif
        shutdown_curses();
        va_start(argp, fmt);
        vfprintf(stderr, fmt, argp);
        va_end(argp);
-       /* kill every process in the process group and exit */
-       para_sigaction(SIGTERM, SIG_IGN);
-       kill(0, SIGTERM);
        exit(exit_code);
 }
 
-/*
- * Print stat item #i to curses window
- */
+/* Print stat item #i to curses window. */
 static void print_stat_item(int i)
 {
        char *tmp;
@@ -514,11 +551,10 @@ static void print_stat_item(int i)
                return;
        tmp = make_message("%s%s%s", d.prefix, c, d.postfix);
        wmove(top.win, d.y * top_lines / 100, d.x * COLS / 100);
-       wrefresh(top.win);
        wattron(top.win, COLOR_PAIR(i + 1));
        align_str(top.win, tmp, d.len * COLS / 100, d.align);
        free(tmp);
-       wrefresh(top.win);
+       refresh_window(&top);
 }
 
 static int update_item(int item_num, char *buf)
@@ -529,19 +565,19 @@ static int update_item(int item_num, char *buf)
        if (buf && buf[0])
                goto dup;
        switch (item_num) {
-       case SI_ARTIST:
+       case SI_artist:
                *c = para_strdup("(artist tag not set)");
                goto print;
-       case SI_TITLE:
+       case SI_title:
                *c = para_strdup("(title tag not set)");
                goto print;
-       case SI_YEAR:
+       case SI_year:
                *c = para_strdup("????");
                goto print;
-       case SI_ALBUM:
+       case SI_album:
                *c = para_strdup("(album tag not set)");
                goto print;
-       case SI_COMMENT:
+       case SI_comment:
                *c = para_strdup("(comment tag not set)");
                goto print;
        }
@@ -572,84 +608,88 @@ static void clear_all_items(void)
        }
 }
 
-static struct timeval next_exec;
-
-static void status_pre_select(fd_set *rfds, int *max_fileno, struct timeval *tv)
+static void status_pre_select(struct sched *s, void *context)
 {
-       struct timeval atm, diff;
+       struct status_task *st = context;
 
-       if (stat_pipe >= 0)
-               return para_fd_set(stat_pipe, rfds, max_fileno);
-       gettimeofday(&atm, NULL);
-       if (tv_diff(&next_exec, &atm, &diff) > 0) {
-               *tv = diff;
-               return;
-       }
-       tv->tv_sec = tv->tv_usec = 0; /* min delay */
+       if (st->fd >= 0)
+               para_fd_set(st->fd, &s->rfds, &s->max_fileno);
+       if (task_get_notification(st->task) < 0)
+               return sched_min_delay(s);
+       if (st->fd < 0)
+               sched_request_barrier_or_min_delay(&st->next_exec, s);
 }
 
-static void status_post_select(fd_set *rfds)
+static int status_post_select(struct sched *s, void *context)
 {
-       static char *buf;
-       static int bufsize, loaded;
+       struct status_task *st = context;
        size_t sz;
-       pid_t pid;
        int ret, ret2;
 
-       if (stat_pipe < 0) {
-               struct timeval atm;
+       ret = task_get_notification(st->task);
+       if (ret == -E_GUI_SIGCHLD && st->pid > 0) {
+               int exit_status;
+               if (waitpid(st->pid, &exit_status, WNOHANG) == st->pid) {
+                       st->pid = 0;
+                       PARA_ERROR_LOG("stat command exit status: %d",
+                               exit_status);
+               }
+       }
+       if (st->fd < 0) {
                int fds[3] = {0, 1, 0};
+               if (st->pid > 0)
+                       return 0;
                /* Avoid busy loop */
-               gettimeofday(&atm, NULL);
-               if (tv_diff(&next_exec, &atm, NULL) > 0)
-                       return;
-               next_exec.tv_sec = atm.tv_sec + 2;
-               ret = para_exec_cmdline_pid(&pid, conf.stat_cmd_arg, fds);
+               if (tv_diff(&st->next_exec, now, NULL) > 0)
+                       return 0;
+               st->next_exec.tv_sec = now->tv_sec + 2;
+               ret = para_exec_cmdline_pid(&st->pid,
+                       OPT_STRING_VAL(STAT_CMD), fds);
                if (ret < 0)
-                       return;
+                       return 0;
                ret = mark_fd_nonblocking(fds[1]);
                if (ret < 0) {
                        close(fds[1]);
-                       return;
+                       return 0;
                }
-               stat_pipe = fds[1];
-               return;
+               st->fd = fds[1];
+               return 0;
        }
 
-       if (loaded >= bufsize) {
-               if (bufsize > 1000 * 1000) {
-                       loaded = 0;
-                       return;
+       if (st->loaded >= st->bufsize) {
+               if (st->bufsize > 1000 * 1000) {
+                       st->loaded = 0;
+                       return 0;
                }
-               bufsize += bufsize + 1000;
-               buf = para_realloc(buf, bufsize);
+               st->bufsize += st->bufsize + 1000;
+               st->buf = para_realloc(st->buf, st->bufsize);
        }
-       assert(loaded < bufsize);
-       ret = read_nonblock(stat_pipe, buf + loaded, bufsize - loaded,
-               rfds, &sz);
-       loaded += sz;
-       ret2 = for_each_stat_item(buf, loaded, update_item);
+       assert(st->loaded < st->bufsize);
+       ret = read_nonblock(st->fd, st->buf + st->loaded,
+               st->bufsize - st->loaded, &s->rfds, &sz);
+       st->loaded += sz;
+       ret2 = for_each_stat_item(st->buf, st->loaded, update_item);
        if (ret < 0 || ret2 < 0) {
-               loaded = 0;
-               PARA_NOTICE_LOG("closing stat pipe: %s\n", para_strerror(-ret));
-               close(stat_pipe);
-               stat_pipe = -1;
+               st->loaded = 0;
+               PARA_NOTICE_LOG("closing stat pipe: %s\n",
+                       para_strerror(ret < 0? -ret : -ret2));
+               close(st->fd);
+               st->fd = -1;
                clear_all_items();
-               free(stat_content[SI_BASENAME]);
-               stat_content[SI_BASENAME] =
+               free(stat_content[SI_basename]);
+               stat_content[SI_basename] =
                        para_strdup("stat command terminated!?");
                print_all_items();
-               return;
+               return 0;
        }
        sz = ret2; /* what is left */
-       if (sz > 0 && sz < loaded)
-               memmove(buf, buf + loaded - sz, sz);
-       loaded = sz;
+       if (sz > 0 && sz < st->loaded)
+               memmove(st->buf, st->buf + st->loaded - sz, sz);
+       st->loaded = sz;
+       return 0;
 }
 
-/*
- * init all windows
- */
+/* Initialize all windows. */
 static void init_wins(int top_lines)
 {
        int top_y = 0, bot_y = top_lines + 1, sb_y = LINES - 2,
@@ -657,7 +697,7 @@ static void init_wins(int top_lines)
        int bot_lines = LINES - top_lines - 3, sb_lines = 1, in_lines = 1,
                sep_lines = 1;
 
-       assume_default_colors(theme.default_fg, theme.default_bg);
+       assume_default_colors(theme.dflt.fg, theme.dflt.bg);
        if (top.win) {
                wresize(top.win, top_lines, COLS);
                mvwin(top.win, top_y, 0);
@@ -729,16 +769,16 @@ static void init_colors_or_die(void)
                die(EXIT_FAILURE, "fatal: failed to start colors\n");
        FOR_EACH_STATUS_ITEM(i)
                if (theme.data[i].len)
-                       init_pair_or_die(i + 1, theme.data[i].fg,
-                               theme.data[i].bg);
-       init_pair_or_die(COLOR_STATUSBAR, theme.sb_fg, theme.sb_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_SEPARATOR, theme.sep_fg, theme.sep_bg);
-       init_pair_or_die(COLOR_TOP, theme.default_fg, theme.default_bg);
-       init_pair_or_die(COLOR_BOT, theme.default_fg, theme.default_bg);
+                       init_pair_or_die(i + 1, theme.data[i].color.fg,
+                               theme.data[i].color.bg);
+       init_pair_or_die(COLOR_STATUSBAR, theme.sb.fg, theme.sb.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_SEPARATOR, theme.sep.fg, theme.sep.bg);
+       init_pair_or_die(COLOR_TOP, theme.dflt.fg, theme.dflt.bg);
+       init_pair_or_die(COLOR_BOT, theme.dflt.fg, theme.dflt.bg);
 }
 
 /* (Re-)initialize the curses library. */
@@ -746,7 +786,7 @@ static void init_curses(void)
 {
        if (curses_active())
                return;
-       if (top.win && refresh() == ERR) /* refresh is really needed */
+       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"
@@ -766,23 +806,6 @@ static void init_curses(void)
        // noecho(); /* don't echo input */
 }
 
-static void check_sigchld(void)
-{
-       int ret;
-       pid_t pid;
-
-reap_next_child:
-       ret = para_reap_child(&pid);
-       if (ret <= 0)
-               return;
-       if (pid == cmd_pid) {
-               cmd_pid = 0;
-               init_curses();
-               print_in_bar(COLOR_MSG, " ");
-       }
-       goto reap_next_child;
-}
-
 /*
  * This sucker modifies its first argument. *handler and *arg are
  * pointers to 0-terminated strings (inside line). Crap.
@@ -806,12 +829,13 @@ 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 (i = 0; i < conf.key_map_given; ++i) {
+       FOR_EACH_KEY_MAP(i) {
                char *handler, *arg;
 
                free(tmp);
-               tmp = para_strdup(conf.key_map_arg[i]);
+               tmp = para_strdup(lls_string_val(i, lor));
                if (!split_key_map(tmp, &handler, &arg))
                        break;
                if (strlen(handler) != 1)
@@ -824,181 +848,205 @@ static void check_key_map_args_or_die(void)
                if (find_cmd_byname(arg) < 0)
                        break;
        }
-       if (i != conf.key_map_given)
-               die(EXIT_FAILURE, "invalid key map: %s\n", conf.key_map_arg[i]);
+       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 override)
+static void parse_config_file_or_die(bool reload)
 {
-       bool err;
-       char *config_file;
-       struct gui_cmdline_parser_params params = {
-               .override = override,
-               .initialize = 0,
-               .check_required = !override,
-               .check_ambiguity = 0,
-               .print_errors = 1,
-       };
+       int ret;
+       char *cf = NULL, *errctx = NULL;
+       void *map;
+       size_t sz;
+       int cf_argc;
+       char **cf_argv;
+       struct lls_parse_result *cf_lpr, *merged_lpr;
 
-       if (conf.config_file_given)
-               config_file = para_strdup(conf.config_file_arg);
+       if (OPT_GIVEN(CONFIG_FILE))
+               cf = para_strdup(OPT_STRING_VAL(CONFIG_FILE));
        else {
                char *home = para_homedir();
-               config_file = make_message("%s/.paraslash/gui.conf", home);
+               cf = make_message("%s/.paraslash/gui.conf", home);
                free(home);
        }
-       if (!file_exists(config_file)) {
-               if (!conf.config_file_given)
-                       err = false;
-               else {
-                       PARA_EMERG_LOG("config file %s does not exist\n",
-                               config_file);
-                       err = true;
-               }
-               goto out;
+       ret = mmap_full_file(cf, O_RDONLY, &map, &sz, NULL);
+       if (ret < 0) {
+               if (ret != -E_EMPTY && ret != -ERRNO_TO_PARA_ERROR(ENOENT))
+                       goto free_cf;
+               if (ret == -ERRNO_TO_PARA_ERROR(ENOENT) && OPT_GIVEN(CONFIG_FILE))
+                       goto free_cf;
+               ret = 0;
+               lpr = cmdline_lpr;
+               goto success;
        }
-       gui_cmdline_parser_config_file(config_file, &conf, &params);
-       loglevel = get_loglevel_by_name(conf.loglevel_arg);
+       ret = lls(lls_convert_config(map, sz, NULL, &cf_argv, &errctx));
+       para_munmap(map, sz);
+       if (ret < 0)
+               goto free_cf;
+       cf_argc = ret;
+       ret = lls(lls_parse(cf_argc, cf_argv, CMD_PTR, &cf_lpr, &errctx));
+       lls_free_argv(cf_argv);
+       if (ret < 0)
+               goto free_cf;
+       if (reload) /* config file overrides command line */
+               ret = lls(lls_merge(cf_lpr, cmdline_lpr, CMD_PTR, &merged_lpr,
+                       &errctx));
+       else /* command line options override config file options */
+               ret = lls(lls_merge(cmdline_lpr, cf_lpr, CMD_PTR, &merged_lpr,
+                       &errctx));
+       lls_free_parse_result(cf_lpr, CMD_PTR);
+       if (ret < 0)
+               goto free_cf;
+       if (lpr != cmdline_lpr)
+               lls_free_parse_result(lpr, CMD_PTR);
+       lpr = merged_lpr;
+success:
+       loglevel = OPT_UINT32_VAL(LOGLEVEL);
        check_key_map_args_or_die();
-       theme_init(conf.theme_arg, &theme);
-       err = false;
-out:
-       free(config_file);
-       if (err)
+       theme_init(OPT_STRING_VAL(THEME), &theme);
+free_cf:
+       free(cf);
+       if (ret < 0) {
+               if (errctx)
+                       PARA_ERROR_LOG("%s\n", errctx);
+               free(errctx);
+               PARA_EMERG_LOG("%s\n", para_strerror(-ret));
                exit(EXIT_FAILURE);
+       }
 }
 
-/* reread configuration, terminate on errors */
+/* Reread configuration, terminate on errors. */
 static void reread_conf(void)
 {
        /*
-        * gengetopt might print to stderr and exit on errors. So we have to
-        * shutdown curses first.
+        * 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 /* override */);
+       parse_config_file_or_die(true);
        init_curses();
        print_in_bar(COLOR_MSG, "config file reloaded\n");
 }
 
-/*
- * React to various signal-related events
- */
-static void signal_post_select(fd_set *rfds)
+/* React to various signal-related events. */
+static int signal_post_select(struct sched *s, __a_unused void *context)
 {
-       int ret = para_next_signal(rfds);
+       int ret = para_next_signal(&s->rfds);
+
        if (ret <= 0)
-               return;
+               return 0;
        switch (ret) {
        case SIGTERM:
                die(EXIT_FAILURE, "only the good die young (caught SIGTERM)\n");
-               return;
        case SIGWINCH:
+               PARA_NOTICE_LOG("got SIGWINCH\n");
                if (curses_active()) {
                        shutdown_curses();
                        init_curses();
                        redraw_bot_win();
                }
-               return;
+               return 1;
        case SIGINT:
-               PARA_WARNING_LOG("caught SIGINT, reset\n");
-               /* Nothing to do. SIGINT killed our child which gets noticed
-                * by do_select and resets everything.
-                */
-               return;
+               return 1;
        case SIGUSR1:
                PARA_NOTICE_LOG("got SIGUSR1, rereading configuration\n");
                reread_conf();
-               return;
+               return 1;
        case SIGCHLD:
-               check_sigchld();
-               return;
+               task_notify_all(s, E_GUI_SIGCHLD);
+               return 1;
        }
+       return 1;
 }
 
-#define COMMAND_BUF_SIZE 32768
-
-static enum cmd_status cmd_status(void)
+static enum exec_status exec_status(void)
 {
-       if (command_fds[0] >= 0 || command_fds[1] >= 0)
-               return CMDS_DCMD;
-       if (cmd_pid > 0)
-               return CMDS_XCMD;
-       return CMDS_IDLE;
+       if (exec_fds[0] >= 0 || exec_fds[1] >= 0)
+               return EXEC_DCMD;
+       if (exec_pid > 0)
+               return EXEC_XCMD;
+       return EXEC_IDLE;
 }
 
-static void command_pre_select(fd_set *rfds, int *max_fileno)
+static void exec_pre_select(struct sched *s, void *context)
 {
-       enum cmd_status cmds = cmd_status();
-
-       if (cmds != CMDS_DCMD)
-               return;
-       if (command_fds[0] >= 0)
-               para_fd_set(command_fds[0], rfds, max_fileno);
-       if (command_fds[1] >= 0)
-               para_fd_set(command_fds[1], rfds, max_fileno);
+       struct exec_task *et = context;
+       if (exec_fds[0] >= 0)
+               para_fd_set(exec_fds[0], &s->rfds, &s->max_fileno);
+       if (exec_fds[1] >= 0)
+               para_fd_set(exec_fds[1], &s->rfds, &s->max_fileno);
+       if (task_get_notification(et->task) < 0)
+               sched_min_delay(s);
 }
 
-static void command_post_select(fd_set *rfds)
+static int exec_post_select(struct sched *s, void *context)
 {
+       struct exec_task *ct = context;
        int i, ret;
-       static char command_buf[2][COMMAND_BUF_SIZE];
-       static int cbo[2]; /* command buf offsets */
-       static unsigned flags[2]; /* for for_each_line() */
-       enum cmd_status cmds = cmd_status();
 
-       if (cmds != CMDS_DCMD)
-               return;
+       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_in_bar(COLOR_MSG, " ");
+               }
+       }
        for (i = 0; i < 2; i++) {
                size_t sz;
-               if (command_fds[i] < 0)
+               if (exec_fds[i] < 0)
                        continue;
-               ret = read_nonblock(command_fds[i],
-                       command_buf[i] + cbo[i],
-                       COMMAND_BUF_SIZE - 1 - cbo[i], rfds, &sz);
-               cbo[i] += sz;
-               sz = cbo[i];
-               cbo[i] = for_each_line(flags[i], command_buf[i], cbo[i],
-                       add_output_line, &i);
-               if (sz != cbo[i]) { /* at least one line found */
-                       wrefresh(bot.win);
-                       flags[i] = 0;
+               ret = read_nonblock(exec_fds[i],
+                       ct->command_buf[i] + ct->cbo[i],
+                       COMMAND_BUF_SIZE - 1 - ct->cbo[i], &s->rfds, &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 */
+                       refresh_window(&bot);
+                       ct->flags[i] = 0;
                }
-               if (ret < 0 || cmd_pid == 0) {
+               if (ret < 0 || exec_pid == 0) {
                        if (ret < 0)
                                PARA_NOTICE_LOG("closing command fd %d: %s",
                                        i, para_strerror(-ret));
-                       close(command_fds[i]);
-                       command_fds[i] = -1;
-                       flags[i] = 0;
-                       cbo[i] = 0;
-                       if (command_fds[!i] < 0) /* both fds closed */
-                               return;
+                       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 (cbo[i] == COMMAND_BUF_SIZE - 1) {
+               if (ct->cbo[i] == COMMAND_BUF_SIZE - 1) {
                        PARA_NOTICE_LOG("discarding overlong line");
-                       cbo[i] = 0;
-                       flags[i] = FELF_DISCARD_FIRST;
+                       ct->cbo[i] = 0;
+                       ct->flags[i] = FELF_DISCARD_FIRST;
                }
        }
+       return 0;
 }
 
-static void input_pre_select(fd_set *rfds, int *max_fileno)
+static void input_pre_select(struct sched *s, __a_unused void *context)
 {
-       enum cmd_status cmds = cmd_status();
-
-       if (cmds != CMDS_XCMD)
-               para_fd_set(STDIN_FILENO, rfds, max_fileno);
+       if (exec_status() != EXEC_XCMD)
+               para_fd_set(STDIN_FILENO, &s->rfds, &s->max_fileno);
+       if (window_update_needed())
+               sched_min_delay(s);
 }
 
-/* read from command pipe and print data to bot window */
-static void exec_and_display_cmd(const char *cmd)
+/* 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", cmd);
-       ret = para_exec_cmdline_pid(&cmd_pid, cmd, fds);
+       outputf(COLOR_COMMAND, "%s", file_and_args);
+       ret = para_exec_cmdline_pid(&exec_pid, file_and_args, fds);
        if (ret < 0)
                return;
        ret = mark_fd_nonblocking(fds[1]);
@@ -1007,53 +1055,46 @@ static void exec_and_display_cmd(const char *cmd)
        ret = mark_fd_nonblocking(fds[2]);
        if (ret < 0)
                goto fail;
-       command_fds[0] = fds[1];
-       command_fds[1] = fds[2];
+       exec_fds[0] = fds[1];
+       exec_fds[1] = fds[2];
+       print_in_bar(COLOR_MSG, "hit any key to abort\n");
        return;
 fail:
        PARA_ERROR_LOG("%s\n", para_strerror(-ret));
-       close(command_fds[0]);
-       close(command_fds[1]);
+       close(exec_fds[0]);
+       close(exec_fds[1]);
 }
 
-static void para_cmd(char *cmd)
+static void exec_para(const char *args)
 {
-       char *c;
+       char *file_and_args;
 
-       print_in_bar(COLOR_MSG, "executing client command, hit any key to abort\n");
-       c = make_message(BINDIR "/para_client -- %s", cmd);
-       exec_and_display_cmd(c);
-       free(c);
+       file_and_args = make_message(BINDIR "/para_client -- %s", args);
+       exec_and_display(file_and_args);
+       free(file_and_args);
 }
 
-static void display_cmd(char *cmd)
-{
-       print_in_bar(COLOR_MSG, "executing display command, hit any key to abort");
-       exec_and_display_cmd(cmd);
-}
-
-/*
- * shutdown curses and stat pipe before executing external commands
- */
-static void external_cmd(char *cmd)
+/* Shutdown curses and stat pipe before executing external commands. */
+static void exec_external(char *file_and_args)
 {
        int fds[3] = {-1, -1, -1};
 
-       if (cmd_pid)
+       if (exec_pid)
                return;
        shutdown_curses();
-       para_exec_cmdline_pid(&cmd_pid, cmd, fds);
+       para_exec_cmdline_pid(&exec_pid, file_and_args, fds);
 }
 
 static void handle_command(int c)
 {
        int i;
+       const struct lls_opt_result *lor = OPT_RESULT(KEY_MAP);
 
        /* first check user-defined key bindings */
-       for (i = 0; i < conf.key_map_given; ++i) {
+       FOR_EACH_KEY_MAP(i) {
                char *tmp, *handler, *arg;
 
-               tmp = para_strdup(conf.key_map_arg[i]);
+               tmp = para_strdup(lls_string_val(i, lor));
                if (!split_key_map(tmp, &handler, &arg)) {
                        free(tmp);
                        return;
@@ -1063,11 +1104,11 @@ static void handle_command(int c)
                        continue;
                }
                if (*handler == 'd')
-                       display_cmd(arg);
+                       exec_and_display(arg);
                else if (*handler == 'x')
-                       external_cmd(arg);
+                       exec_external(arg);
                else if (*handler == 'p')
-                       para_cmd(arg);
+                       exec_para(arg);
                else if (*handler == 'i') {
                        int num = find_cmd_byname(arg);
                        if (num >= 0)
@@ -1087,57 +1128,39 @@ static void handle_command(int c)
                km_keyname(c));
 }
 
-static void input_post_select(void)
+static int input_post_select(__a_unused struct sched *s,
+               __a_unused void *context)
 {
        int ret;
-       enum cmd_status cmds = cmd_status();
+       enum exec_status exs = exec_status();
 
-       if (cmds == CMDS_XCMD)
-               return;
+       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 (sep.needs_update)
+                       assert(wnoutrefresh(sep.win) == OK);
+               if (sb.needs_update)
+                       assert(wnoutrefresh(sb.win) == OK);
+               if (in.needs_update)
+                       assert(wnoutrefresh(in.win) == OK);
+               doupdate();
+               top.needs_update = bot.needs_update = sb.needs_update =
+                       in.needs_update = sep.needs_update = false;
+       }
        ret = wgetch(top.win);
-       if (ret == ERR || ret == KEY_RESIZE)
-               return;
-       if (cmds == CMDS_IDLE)
-               return handle_command(ret);
-       if (cmd_pid != 0)
-               kill(cmd_pid, SIGTERM);
-}
-
-static void signal_pre_select(fd_set *rfds, int *max_fileno)
-{
-       para_fd_set(signal_pipe, rfds, max_fileno);
-}
-
-/*
- * This is the core select loop. It checks the following fds:
- *
- * - signal pipe
- * - stdin
- * - stdout/stderr of display or internal commands
- */
-__noreturn static void do_select(void)
-{
-       fd_set rfds;
-       int ret, max_fileno;
-       struct timeval tv;
-
-repeat:
-       tv.tv_sec = conf.timeout_arg  / 1000;
-       tv.tv_usec = (conf.timeout_arg % 1000) * 1000;
-       FD_ZERO(&rfds);
-       max_fileno = 0;
-       status_pre_select(&rfds, &max_fileno, &tv);
-       signal_pre_select(&rfds, &max_fileno);
-       command_pre_select(&rfds, &max_fileno);
-       input_pre_select(&rfds, &max_fileno);
-       ret = para_select(max_fileno + 1, &rfds, NULL, &tv);
-       if (ret <= 0)
-               goto repeat; /* skip fd checks */
-       signal_post_select(&rfds);
-       command_post_select(&rfds);
-       status_post_select(&rfds);
-       input_post_select();
-       goto repeat;
+       if (ret == ERR)
+               return 0;
+       if (ret == KEY_RESIZE) /* already handled in signal_post_select() */
+               return 0;
+       if (exs == EXEC_IDLE)
+               handle_command(ret);
+       else if (exec_pid > 0)
+               kill(exec_pid, SIGTERM);
+       return 0;
 }
 
 static void print_scroll_msg(void)
@@ -1145,7 +1168,7 @@ static void print_scroll_msg(void)
        unsigned lines_total, filled = ringbuffer_filled(bot_win_rb);
        int first_rbe = first_visible_rbe(&lines_total);
 
-       print_in_bar(COLOR_MSG, "scrolled view: %d-%d/%d\n", filled - first_rbe,
+       print_in_bar(COLOR_MSG, "scrolled view: %u-%u/%u\n", filled - first_rbe,
                filled - scroll_position, ringbuffer_filled(bot_win_rb));
 }
 
@@ -1239,7 +1262,7 @@ static void com_scroll_down(void)
        wmove(bot.win, bot_lines - rbe_lines, 0);
        wattron(bot.win, COLOR_PAIR(rbe->color));
        waddstr(bot.win, rbe->msg);
-       wrefresh(bot.win);
+       refresh_window(&bot);
        print_scroll_msg();
 }
 
@@ -1276,7 +1299,7 @@ static void com_scroll_up(void)
                        break;
                i--;
        }
-       wrefresh(bot.win);
+       refresh_window(&bot);
        print_scroll_msg();
        return;
 err_out:
@@ -1313,9 +1336,10 @@ static void com_reread_conf(void)
 static void com_help(void)
 {
        int i;
+       const struct lls_opt_result *lor = OPT_RESULT(KEY_MAP);
 
-       for (i = 0; i < conf.key_map_given; ++i) {
-               char *handler, *arg, *tmp = para_strdup(conf.key_map_arg[i]);
+       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)) {
@@ -1399,27 +1423,121 @@ static void com_prev_theme(void)
        com_refresh();
 }
 
-__noreturn static void print_help_and_die(void)
+static int setup_tasks_and_schedule(void)
 {
-       struct ggo_help h = DEFINE_GGO_HELP(gui);
-       bool d = conf.detailed_help_given;
-
-       ggo_print_help(&h, d? GPH_STANDARD_FLAGS_DETAILED : GPH_STANDARD_FLAGS);
-       exit(0);
+       int ret;
+       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 = {.default_timeout = {.tv_sec = 1}};
+
+       exec_task.task = task_register(&(struct task_info) {
+               .name = "exec",
+               .pre_select = exec_pre_select,
+               .post_select = exec_post_select,
+               .context = &exec_task,
+       }, &sched);
+
+       status_task.task = task_register(&(struct task_info) {
+               .name = "status",
+               .pre_select = status_pre_select,
+               .post_select = status_post_select,
+               .context = &status_task,
+       }, &sched);
+
+       input_task.task = task_register(&(struct task_info) {
+               .name = "input",
+               .pre_select = input_pre_select,
+               .post_select = input_post_select,
+               .context = &input_task,
+       }, &sched);
+
+       signal_task = signal_init_or_die();
+       para_install_sighandler(SIGINT);
+       para_install_sighandler(SIGTERM);
+       para_install_sighandler(SIGCHLD);
+       para_install_sighandler(SIGUSR1);
+       para_install_sighandler(SIGWINCH);
+       signal_task->task = task_register(&(struct task_info) {
+               .name = "signal",
+               .pre_select = signal_pre_select,
+               .post_select = signal_post_select,
+               .context = signal_task,
+       }, &sched);
+       ret = schedule(&sched);
+       sched_shutdown(&sched);
+       signal_shutdown(signal_task);
+       return ret;
+}
+
+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);
 }
 
+/**
+ * The main function of para_gui.
+ *
+ * \param argc Usual argument count.
+ * \param argv Usual argument vector.
+ *
+ * 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 reloads the configuration file on SIGUSR1, 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 \p EXIT_SUCCESS or \p EXIT_FAILURE.
+ */
 int main(int argc, char *argv[])
 {
-       gui_cmdline_parser(argc, argv, &conf); /* exits on errors */
-       loglevel = get_loglevel_by_name(conf.loglevel_arg);
-       version_handle_flag("gui", conf.version_given);
-       if (conf.help_given || conf.detailed_help_given)
-               print_help_and_die();
-       parse_config_file_or_die(false /* override */);
-       setup_signal_handling();
+       int ret;
+       char *errctx;
+
+       ret = lls(lls_parse(argc, argv, CMD_PTR, &cmdline_lpr, &errctx));
+       if (ret < 0)
+               goto out;
+       lpr = cmdline_lpr;
+       loglevel = OPT_UINT32_VAL(LOGLEVEL);
+       version_handle_flag("gui", OPT_GIVEN(VERSION));
+       handle_help_flags();
+       parse_config_file_or_die(false);
        bot_win_rb = ringbuffer_new(RINGBUFFER_SIZE);
        setlocale(LC_CTYPE, "");
        initscr(); /* needed only once, always successful */
        init_curses();
-       do_select();
+       ret = setup_tasks_and_schedule();
+out:
+       lls_free_parse_result(lpr, CMD_PTR);
+       if (lpr != cmdline_lpr)
+               lls_free_parse_result(cmdline_lpr, CMD_PTR);
+       if (ret < 0) {
+               if (errctx)
+                       PARA_ERROR_LOG("%s\n", errctx);
+               free(errctx);
+               PARA_EMERG_LOG("%s\n", para_strerror(-ret));
+       }
+       return ret < 0? EXIT_FAILURE : EXIT_SUCCESS;
 }