gui: Move input related code out of do_select().
[paraslash.git] / gui.c
1 /*
2 * Copyright (C) 1998-2014 Andre Noll <maan@systemlinux.org>
3 *
4 * Licensed under the GPL v2. For licencing details see COPYING.
5 */
6
7 /** \file gui.c Curses-based interface for paraslash. */
8
9 #include <regex.h>
10 #include <signal.h>
11 #include <sys/types.h>
12 #include <curses.h>
13 #include <locale.h>
14 #include <sys/time.h>
15
16 #include "gui.cmdline.h"
17 #include "para.h"
18 #include "gui.h"
19 #include "string.h"
20 #include "ringbuffer.h"
21 #include "fd.h"
22 #include "error.h"
23 #include "list.h"
24 #include "sched.h"
25 #include "signal.h"
26 #include "ggo.h"
27 #include "version.h"
28
29 /** define the array of error lists needed by para_gui */
30 INIT_GUI_ERRLISTS;
31 static char *stat_content[NUM_STAT_ITEMS];
32
33 static int signal_pipe;
34
35 static struct gui_window {
36 WINDOW *win;
37 } top, bot, sb, in, sep;
38
39 #define RINGBUFFER_SIZE 512
40 struct rb_entry {
41 char *msg;
42 size_t len;
43 int color;
44 };
45 static struct ringbuffer *bot_win_rb;
46
47 static unsigned scroll_position;
48
49 static pid_t cmd_pid;
50
51 static int command_fds[2] = {-1, -1};
52 static int stat_pipe = -1;
53 static struct gui_args_info conf;
54 static int loglevel;
55
56 enum {GETCH_MODE, COMMAND_MODE, EXTERNAL_MODE};
57
58
59 /**
60 * Codes for various colors.
61 *
62 * Each status item has its own color pair. The ones defined here start at a
63 * higher number so that they do not overlap with these.
64 */
65 enum gui_color_pair {
66 COLOR_STATUSBAR = NUM_STAT_ITEMS + 1,
67 COLOR_COMMAND,
68 COLOR_OUTPUT,
69 COLOR_MSG,
70 COLOR_ERRMSG,
71 COLOR_SEPARATOR,
72 COLOR_TOP,
73 COLOR_BOT,
74 };
75
76 struct gui_command {
77 const char *key;
78 const char *name;
79 const char *description;
80 void (*handler)(void);
81 };
82
83 static struct gui_theme theme;
84
85 static void com_help(void);
86 static void com_reread_conf(void);
87 static void com_enlarge_top_win(void);
88 static void com_shrink_top_win(void);
89 static void com_version(void);
90 __noreturn static void com_quit(void);
91 static void com_refresh(void);
92 static void com_ll_incr(void);
93 static void com_ll_decr(void);
94 static void com_prev_theme(void);
95 static void com_next_theme(void);
96 static void com_scroll_up(void);
97 static void com_scroll_down(void);
98 static void com_page_up(void);
99 static void com_page_down(void);
100 static void com_cancel_scrolling(void);
101 static void com_scroll_top(void);
102
103 static struct gui_command command_list[] = {
104 {
105 .key = "?",
106 .name = "help",
107 .description = "print help",
108 .handler = com_help
109 }, {
110 .key = "+",
111 .name = "enlarge_win",
112 .description = "enlarge the top window",
113 .handler = com_enlarge_top_win
114 }, {
115 .key = "-",
116 .name = "shrink_win",
117 .description = "shrink the top window",
118 .handler = com_shrink_top_win
119 }, {
120 .key = "r",
121 .name = "reread_conf",
122 .description = "reread configuration file",
123 .handler = com_reread_conf
124 }, {
125 .key = "q",
126 .name = "quit",
127 .description = "exit para_gui",
128 .handler = com_quit
129 }, {
130 .key = "^L",
131 .name = "refresh",
132 .description = "redraw the screen",
133 .handler = com_refresh
134 }, {
135 .key = ".",
136 .name = "next_theme",
137 .description = "switch to next theme",
138 .handler = com_next_theme
139 }, {
140 .key = ",",
141 .name = "prev_theme",
142 .description = "switch to previous stream",
143 .handler = com_prev_theme
144 }, {
145 .key = ">",
146 .name = "ll_incr",
147 .description = "increase loglevel (decreases verbosity)",
148 .handler = com_ll_incr
149 }, {
150 .key = "<",
151 .name = "ll_decr",
152 .description = "decrease loglevel (increases verbosity)",
153 .handler = com_ll_decr
154 }, {
155 .key = "V",
156 .name = "version",
157 .description = "show the para_gui version",
158 .handler = com_version
159 }, {
160 .key = "<up>",
161 .name = "scroll_up",
162 .description = "scroll up one line",
163 .handler = com_scroll_up
164 }, {
165 .key = "<down>",
166 .name = "scroll_down",
167 .description = "scroll down one line",
168 .handler = com_scroll_down
169 }, {
170 .key = "<ppage>",
171 .name = "page_up",
172 .description = "scroll up one page",
173 .handler = com_page_up
174 }, {
175 .key = "<npage>",
176 .name = "page_down",
177 .description = "scroll down one page",
178 .handler = com_page_down
179 }, {
180 .key = "<home>",
181 .name = "scroll_top",
182 .description = "scroll to top of buffer",
183 .handler = com_scroll_top
184 }, {
185 .key = "<end>",
186 .name = "cancel_scroll",
187 .description = "deactivate scroll mode",
188 .handler = com_cancel_scrolling
189 }, {
190 .handler = NULL
191 }
192 };
193
194 static int find_cmd_byname(char *name)
195 {
196 int i;
197
198 for (i = 0; command_list[i].handler; i++)
199 if (!strcmp(command_list[i].name, name))
200 return i;
201 return -1;
202 }
203
204 /*
205 * Even though ncurses provides getmaxx and getmaxy, these functions/macros are
206 * not described in the XSI Curses standard.
207 */
208 static int get_num_lines(struct gui_window *w)
209 {
210 int lines;
211 __a_unused int cols; /* avoid "set but not used" warnings */
212
213 getmaxyx(w->win, lines, cols);
214 return lines;
215 }
216
217 static int get_num_cols(struct gui_window *w)
218 {
219 __a_unused int lines; /* avoid "set but not used" warnings */
220 int cols;
221
222 getmaxyx(w->win, lines, cols);
223 return cols;
224 }
225
226 /** Number of lines of the window are occupied by an output line. */
227 #define NUM_LINES(len) (1 + (len) / get_num_cols(&bot))
228
229 /* isendwin() returns false before initscr() was called */
230 static bool curses_active(void)
231 {
232 return top.win && !isendwin();
233 }
234
235 /* taken from mutt */
236 static char *km_keyname(int c)
237 {
238 static char buf[10];
239
240 if (c == KEY_UP) {
241 sprintf(buf, "<up>");
242 return buf;
243 }
244 if (c == KEY_DOWN) {
245 sprintf(buf, "<down>");
246 return buf;
247 }
248 if (c == KEY_LEFT) {
249 sprintf(buf, "<left>");
250 return buf;
251 }
252 if (c == KEY_RIGHT) {
253 sprintf(buf, "<right>");
254 return buf;
255 }
256 if (c == KEY_NPAGE) {
257 sprintf(buf, "<npage>");
258 return buf;
259 }
260 if (c == KEY_PPAGE) {
261 sprintf(buf, "<ppage>");
262 return buf;
263 }
264 if (c == KEY_HOME) {
265 sprintf(buf, "<home>");
266 return buf;
267 }
268 if (c == KEY_END) {
269 sprintf(buf, "<end>");
270 return buf;
271 }
272 if (c < 256 && c > -128 && iscntrl((unsigned char) c)) {
273 if (c < 0)
274 c += 256;
275 if (c < 128) {
276 buf[0] = '^';
277 buf[1] = (c + '@') & 0x7f;
278 buf[2] = 0;
279 } else
280 snprintf(buf, sizeof(buf), "\\%d%d%d", c >> 6,
281 (c >> 3) & 7, c & 7);
282 } else if (c >= KEY_F0 && c < KEY_F(256))
283 sprintf(buf, "<F%d>", c - KEY_F0);
284 else if (isprint(c))
285 snprintf(buf, sizeof(buf), "%c", (unsigned char) c);
286 else
287 snprintf(buf, sizeof(buf), "\\x%hx", (unsigned short) c);
288 return buf;
289 }
290
291 /* Print given number of spaces to curses window. */
292 static void add_spaces(WINDOW* win, unsigned int num)
293 {
294 char space[] = " ";
295 unsigned sz = sizeof(space) - 1; /* number of spaces */
296
297 while (num >= sz) {
298 waddstr(win, space);
299 num -= sz;
300 }
301 if (num > 0) {
302 assert(num < sz);
303 space[num] = '\0';
304 waddstr(win, space);
305 }
306 }
307
308 /*
309 * print aligned string to curses window. This function always prints
310 * exactly len chars.
311 */
312 static int align_str(WINDOW* win, char *str, unsigned int len,
313 unsigned int align)
314 {
315 int ret, i, num; /* of spaces */
316 size_t width;
317
318 if (!win || !str)
319 return 0;
320 ret = strwidth(str, &width);
321 if (ret < 0) {
322 PARA_ERROR_LOG("%s\n", para_strerror(-ret));
323 width = 0;
324 str[0] = '\0';
325 }
326 num = len - width;
327 if (num < 0) {
328 str[len] = '\0';
329 num = 0;
330 }
331 /* replace control characters by spaces */
332 for (i = 0; i < len && str[i]; i++) {
333 if (str[i] == '\n' || str[i] == '\r' || str[i] == '\f')
334 str[i] = ' ';
335 }
336 if (align == LEFT) {
337 waddstr(win, str);
338 add_spaces(win, num);
339 } else if (align == RIGHT) {
340 add_spaces(win, num);
341 waddstr(win, str);
342 } else {
343 add_spaces(win, num / 2);
344 waddstr(win, str[0]? str: "");
345 add_spaces(win, num - num / 2);
346 }
347 return 1;
348 }
349
350 __printf_2_3 static void print_in_bar(int color, const char *fmt,...)
351 {
352 char *msg;
353 va_list ap;
354
355 if (!curses_active())
356 return;
357 wattron(in.win, COLOR_PAIR(color));
358 va_start(ap, fmt);
359 xvasprintf(&msg, fmt, ap);
360 va_end(ap);
361 wmove(in.win, 0, 0);
362 align_str(in.win, msg, get_num_cols(&in), LEFT);
363 free(msg);
364 wrefresh(in.win);
365 }
366
367 static void print_status_bar(void)
368 {
369 char *tmp;
370
371 tmp = para_strdup("para_gui " PACKAGE_VERSION " (hit ? for help)");
372 wmove(sb.win, 0, 0);
373 align_str(sb.win, tmp, get_num_cols(&sb), CENTER);
374 free(tmp);
375 }
376
377 /*
378 * get the number of the oldest rbe that is (partially) visible. On return,
379 * lines contains the sum of the number of lines of all visible entries. If the
380 * first one is only partially visible, lines is greater than bot.lines.
381 */
382 static int first_visible_rbe(unsigned *lines)
383 {
384 int i, bot_lines = get_num_lines(&bot);
385
386 *lines = 0;
387 for (i = scroll_position; i < RINGBUFFER_SIZE; i++) {
388 struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i);
389 int rbe_lines;
390 if (!rbe)
391 return i - 1;
392 rbe_lines = NUM_LINES(rbe->len);
393 if (rbe_lines > bot_lines)
394 return -1;
395 *lines += rbe_lines;
396 if (*lines >= bot_lines)
397 return i;
398 }
399 return RINGBUFFER_SIZE - 1;
400 }
401
402 /*
403 returns number of first visible rbe, *lines is the number of lines drawn.
404 */
405 static int draw_top_rbe(unsigned *lines)
406 {
407 int bot_cols, bot_lines, ret, fvr = first_visible_rbe(lines);
408 struct rb_entry *rbe;
409 size_t bytes_to_skip, cells_to_skip, width;
410
411 if (fvr < 0)
412 return -1;
413 wmove(bot.win, 0, 0);
414 rbe = ringbuffer_get(bot_win_rb, fvr);
415 if (!rbe)
416 return -1;
417 getmaxyx(bot.win, bot_lines, bot_cols);
418 if (*lines > bot_lines) {
419 /* rbe is partially visible multi-line */
420 cells_to_skip = (*lines - bot_lines) * bot_cols;
421 ret = skip_cells(rbe->msg, cells_to_skip, &bytes_to_skip);
422 if (ret < 0)
423 return ret;
424 ret = strwidth(rbe->msg + bytes_to_skip, &width);
425 if (ret < 0)
426 return ret;
427 } else {
428 bytes_to_skip = 0;
429 width = rbe->len;
430 }
431 wattron(bot.win, COLOR_PAIR(rbe->color));
432 waddstr(bot.win, rbe->msg + bytes_to_skip);
433 *lines = NUM_LINES(width);
434 return fvr;
435 }
436
437 static void redraw_bot_win(void)
438 {
439 unsigned lines;
440 int i, bot_lines = get_num_lines(&bot);
441
442 wmove(bot.win, 0, 0);
443 wclear(bot.win);
444 i = draw_top_rbe(&lines);
445 if (i <= 0)
446 goto out;
447 while (i > 0 && lines < bot_lines) {
448 struct rb_entry *rbe = ringbuffer_get(bot_win_rb, --i);
449 if (!rbe) {
450 lines++;
451 waddstr(bot.win, "\n");
452 continue;
453 }
454 lines += NUM_LINES(rbe->len);
455 wattron(bot.win, COLOR_PAIR(rbe->color));
456 waddstr(bot.win, "\n");
457 waddstr(bot.win, rbe->msg);
458 }
459 out:
460 wrefresh(bot.win);
461 }
462
463 static void rb_add_entry(int color, char *msg)
464 {
465 struct rb_entry *old, *new;
466 int x, y;
467 size_t len;
468
469 if (strwidth(msg, &len) < 0)
470 return;
471 new = para_malloc(sizeof(struct rb_entry));
472 new->color = color;
473 new->len = len;
474 new->msg = msg;
475 old = ringbuffer_add(bot_win_rb, new);
476 if (old) {
477 free(old->msg);
478 free(old);
479 }
480 if (scroll_position) {
481 /* discard current scrolling, like xterm does */
482 scroll_position = 0;
483 redraw_bot_win();
484 return;
485 }
486 wattron(bot.win, COLOR_PAIR(color));
487 getyx(bot.win, y, x);
488 if (y || x)
489 waddstr(bot.win, "\n");
490 waddstr(bot.win, msg);
491 }
492
493 /*
494 * print formated output to bot win and refresh
495 */
496 __printf_2_3 static void outputf(int color, const char* fmt,...)
497 {
498 char *msg;
499 va_list ap;
500
501 if (!curses_active())
502 return;
503 va_start(ap, fmt);
504 xvasprintf(&msg, fmt, ap);
505 va_end(ap);
506 rb_add_entry(color, msg);
507 wrefresh(bot.win);
508 }
509
510 static int add_output_line(char *line, void *data)
511 {
512 int color = *(int *)data? COLOR_ERRMSG : COLOR_OUTPUT;
513
514 if (!curses_active())
515 return 1;
516 rb_add_entry(color, para_strdup(line));
517 return 1;
518 }
519
520 static __printf_2_3 void curses_log(int ll, const char *fmt,...)
521 {
522 va_list ap;
523
524 if (ll < loglevel)
525 return;
526 va_start(ap, fmt);
527 if (curses_active()) {
528 int color = ll <= LL_NOTICE? COLOR_MSG : COLOR_ERRMSG;
529 char *msg;
530 unsigned bytes = xvasprintf(&msg, fmt, ap);
531 if (bytes > 0 && msg[bytes - 1] == '\n')
532 msg[bytes - 1] = '\0'; /* cut trailing newline */
533 rb_add_entry(color, msg);
534 wrefresh(bot.win);
535 } else if (cmd_pid <= 0) /* no external command running */
536 vfprintf(stderr, fmt, ap);
537 va_end(ap);
538 }
539 __printf_2_3 void (*para_log)(int, const char*, ...) = curses_log;
540
541 static void setup_signal_handling(void)
542 {
543 signal_pipe = para_signal_init();
544 para_install_sighandler(SIGINT);
545 para_install_sighandler(SIGTERM);
546 para_install_sighandler(SIGCHLD);
547 para_install_sighandler(SIGWINCH);
548 para_install_sighandler(SIGUSR1);
549 }
550
551 static void shutdown_curses(void)
552 {
553 def_prog_mode();
554 endwin();
555 }
556
557 /* disable curses, print a message, kill running processes and exit */
558 __noreturn __printf_2_3 static void die(int exit_code, const char* fmt, ...)
559 {
560 va_list argp;
561
562 shutdown_curses();
563 va_start(argp, fmt);
564 vfprintf(stderr, fmt, argp);
565 va_end(argp);
566 /* kill every process in the process group and exit */
567 para_sigaction(SIGTERM, SIG_IGN);
568 kill(0, SIGTERM);
569 exit(exit_code);
570 }
571
572 /*
573 * Print stat item #i to curses window
574 */
575 static void print_stat_item(int i)
576 {
577 char *tmp;
578 struct stat_item_data d = theme.data[i];
579 char *c = stat_content[i];
580 int top_lines = get_num_lines(&top);
581
582 if (!curses_active() || !d.len || !c)
583 return;
584 tmp = make_message("%s%s%s", d.prefix, c, d.postfix);
585 wmove(top.win, d.y * top_lines / 100, d.x * COLS / 100);
586 wrefresh(top.win);
587 wattron(top.win, COLOR_PAIR(i + 1));
588 align_str(top.win, tmp, d.len * COLS / 100, d.align);
589 free(tmp);
590 wrefresh(top.win);
591 }
592
593 static int update_item(int item_num, char *buf)
594 {
595 char **c = stat_content + item_num;
596
597 free(*c);
598 if (buf && buf[0])
599 goto dup;
600 switch (item_num) {
601 case SI_ARTIST:
602 *c = para_strdup("(artist tag not set)");
603 goto print;
604 case SI_TITLE:
605 *c = para_strdup("(title tag not set)");
606 goto print;
607 case SI_YEAR:
608 *c = para_strdup("????");
609 goto print;
610 case SI_ALBUM:
611 *c = para_strdup("(album tag not set)");
612 goto print;
613 case SI_COMMENT:
614 *c = para_strdup("(comment tag not set)");
615 goto print;
616 }
617 dup:
618 *c = para_strdup(buf);
619 print:
620 print_stat_item(item_num);
621 return 1;
622 }
623
624 static void print_all_items(void)
625 {
626 int i;
627
628 if (!curses_active())
629 return;
630 FOR_EACH_STATUS_ITEM(i)
631 print_stat_item(i);
632 }
633
634 static void clear_all_items(void)
635 {
636 int i;
637
638 FOR_EACH_STATUS_ITEM(i) {
639 free(stat_content[i]);
640 stat_content[i] = para_strdup("");
641 }
642 }
643
644 static void status_post_select(fd_set *rfds)
645 {
646 static char *buf;
647 static int bufsize, loaded;
648 int ret, ret2;
649 size_t sz;
650
651 if (stat_pipe < 0)
652 return;
653 if (loaded >= bufsize) {
654 if (bufsize > 1000 * 1000) {
655 loaded = 0;
656 return;
657 }
658 bufsize += bufsize + 1000;
659 buf = para_realloc(buf, bufsize);
660 }
661 assert(loaded < bufsize);
662 ret = read_nonblock(stat_pipe, buf + loaded, bufsize - loaded,
663 rfds, &sz);
664 loaded += sz;
665 ret2 = for_each_stat_item(buf, loaded, update_item);
666 if (ret < 0 || ret2 < 0) {
667 loaded = 0;
668 PARA_NOTICE_LOG("closing stat pipe: %s\n", para_strerror(-ret));
669 close(stat_pipe);
670 stat_pipe = -1;
671 clear_all_items();
672 free(stat_content[SI_BASENAME]);
673 stat_content[SI_BASENAME] =
674 para_strdup("stat command terminated!?");
675 print_all_items();
676 return;
677 }
678 sz = ret2; /* what is left */
679 if (sz > 0 && sz < loaded)
680 memmove(buf, buf + loaded - sz, sz);
681 loaded = sz;
682 }
683
684 /*
685 * init all windows
686 */
687 static void init_wins(int top_lines)
688 {
689 int top_y = 0, bot_y = top_lines + 1, sb_y = LINES - 2,
690 in_y = LINES - 1, sep_y = top_lines;
691 int bot_lines = LINES - top_lines - 3, sb_lines = 1, in_lines = 1,
692 sep_lines = 1;
693
694 assume_default_colors(theme.default_fg, theme.default_bg);
695 if (top.win) {
696 wresize(top.win, top_lines, COLS);
697 mvwin(top.win, top_y, 0);
698
699 wresize(sb.win, sb_lines, COLS);
700 mvwin(sb.win, sb_y, 0);
701
702 wresize(sep.win, sep_lines, COLS);
703 mvwin(sep.win, sep_y, 0);
704
705 wresize(bot.win, bot_lines, COLS);
706 mvwin(bot.win, bot_y, 0);
707
708 wresize(in.win, in_lines, COLS);
709 mvwin(in.win, in_y, 0);
710 } else {
711 sep.win = newwin(sep_lines, COLS, sep_y, 0);
712 top.win = newwin(top_lines, COLS, top_y, 0);
713 bot.win = newwin(bot_lines, COLS, bot_y, 0);
714 sb.win = newwin(sb_lines, COLS, sb_y, 0);
715 in.win = newwin(in_lines, COLS, in_y, 0);
716 if (!top.win || !bot.win || !sb.win || !in.win || !sep.win)
717 die(EXIT_FAILURE, "Error: Cannot create curses windows\n");
718 wclear(bot.win);
719 wclear(sb.win);
720 wclear(in.win);
721 scrollok(bot.win, 1);
722 wattron(sb.win, COLOR_PAIR(COLOR_STATUSBAR));
723 wattron(sep.win, COLOR_PAIR(COLOR_SEPARATOR));
724 wattron(bot.win, COLOR_PAIR(COLOR_BOT));
725 wattron(top.win, COLOR_PAIR(COLOR_TOP));
726 nodelay(top.win, 1);
727 nodelay(bot.win, 1);
728 nodelay(sb.win, 1);
729 nodelay(in.win, 0);
730
731 keypad(top.win, 1);
732 keypad(bot.win, 1);
733 keypad(sb.win, 1);
734 keypad(in.win, 1);
735 }
736 wmove(sep.win, 0, 0);
737 whline(sep.win, theme.sep_char, COLS);
738 wclear(top.win);
739 print_all_items();
740 //wclear(bot.win);
741 wnoutrefresh(top.win);
742 wnoutrefresh(bot.win);
743 print_status_bar();
744 wnoutrefresh(sb.win);
745 wnoutrefresh(in.win);
746 wnoutrefresh(sep.win);
747 doupdate();
748 }
749
750 static void init_pair_or_die(short pair, short f, short b)
751 {
752 if (init_pair(pair, f, b) == ERR)
753 die(EXIT_FAILURE, "fatal: init_pair() failed\n");
754 }
755
756 static void init_colors_or_die(void)
757 {
758 int i;
759
760 if (!has_colors())
761 die(EXIT_FAILURE, "fatal: No color term\n");
762 if (start_color() == ERR)
763 die(EXIT_FAILURE, "fatal: failed to start colors\n");
764 FOR_EACH_STATUS_ITEM(i)
765 if (theme.data[i].len)
766 init_pair_or_die(i + 1, theme.data[i].fg,
767 theme.data[i].bg);
768 init_pair_or_die(COLOR_STATUSBAR, theme.sb_fg, theme.sb_bg);
769 init_pair_or_die(COLOR_COMMAND, theme.cmd_fg, theme.cmd_bg);
770 init_pair_or_die(COLOR_OUTPUT, theme.output_fg, theme.output_bg);
771 init_pair_or_die(COLOR_MSG, theme.msg_fg, theme.msg_bg);
772 init_pair_or_die(COLOR_ERRMSG, theme.err_msg_fg, theme.err_msg_bg);
773 init_pair_or_die(COLOR_SEPARATOR, theme.sep_fg, theme.sep_bg);
774 init_pair_or_die(COLOR_TOP, theme.default_fg, theme.default_bg);
775 init_pair_or_die(COLOR_BOT, theme.default_fg, theme.default_bg);
776 }
777
778 /* (Re-)initialize the curses library. */
779 static void init_curses(void)
780 {
781 if (curses_active())
782 return;
783 if (top.win && refresh() == ERR) /* refresh is really needed */
784 die(EXIT_FAILURE, "refresh() failed\n");
785 if (LINES < theme.lines_min || COLS < theme.cols_min)
786 die(EXIT_FAILURE, "Terminal (%dx%d) too small"
787 " (need at least %dx%d)\n", COLS, LINES,
788 theme.cols_min, theme.lines_min);
789 curs_set(0); /* make cursor invisible, ignore errors */
790 nonl(); /* do not NL->CR/NL on output, always returns OK */
791 /* don't echo input */
792 if (noecho() == ERR)
793 die(EXIT_FAILURE, "fatal: noecho() failed\n");
794 /* take input chars one at a time, no wait for \n */
795 if (cbreak() == ERR)
796 die(EXIT_FAILURE, "fatal: cbreak() failed\n");
797 init_colors_or_die();
798 clear(); /* ignore non-fatal errors */
799 init_wins(theme.top_lines_default);
800 // noecho(); /* don't echo input */
801 }
802
803 static void check_sigchld(void)
804 {
805 int ret;
806 pid_t pid;
807
808 reap_next_child:
809 ret = para_reap_child(&pid);
810 if (ret <= 0)
811 return;
812 if (pid == cmd_pid)
813 cmd_pid = 0;
814 goto reap_next_child;
815 }
816
817 /*
818 * This sucker modifies its first argument. *handler and *arg are
819 * pointers to 0-terminated strings (inside line). Crap.
820 */
821 static int split_key_map(char *line, char **handler, char **arg)
822 {
823 if (!(*handler = strchr(line + 1, ':')))
824 goto err_out;
825 **handler = '\0';
826 (*handler)++;
827 if (!(*arg = strchr(*handler, ':')))
828 goto err_out;
829 **arg = '\0';
830 (*arg)++;
831 return 1;
832 err_out:
833 return 0;
834 }
835
836 static void check_key_map_args_or_die(void)
837 {
838 int i;
839 char *tmp = NULL;
840
841 for (i = 0; i < conf.key_map_given; ++i) {
842 char *handler, *arg;
843
844 free(tmp);
845 tmp = para_strdup(conf.key_map_arg[i]);
846 if (!split_key_map(tmp, &handler, &arg))
847 break;
848 if (strlen(handler) != 1)
849 break;
850 if (*handler != 'x' && *handler != 'd' && *handler != 'i'
851 && *handler != 'p')
852 break;
853 if (*handler != 'i')
854 continue;
855 if (find_cmd_byname(arg) < 0)
856 break;
857 }
858 if (i != conf.key_map_given)
859 die(EXIT_FAILURE, "invalid key map: %s\n", conf.key_map_arg[i]);
860 free(tmp);
861 }
862
863 static void parse_config_file_or_die(bool override)
864 {
865 bool err;
866 char *config_file;
867 struct gui_cmdline_parser_params params = {
868 .override = override,
869 .initialize = 0,
870 .check_required = !override,
871 .check_ambiguity = 0,
872 .print_errors = 1,
873 };
874
875 if (conf.config_file_given)
876 config_file = para_strdup(conf.config_file_arg);
877 else {
878 char *home = para_homedir();
879 config_file = make_message("%s/.paraslash/gui.conf", home);
880 free(home);
881 }
882 if (!file_exists(config_file)) {
883 if (!conf.config_file_given)
884 err = false;
885 else {
886 PARA_EMERG_LOG("config file %s does not exist\n",
887 config_file);
888 err = true;
889 }
890 goto out;
891 }
892 gui_cmdline_parser_config_file(config_file, &conf, &params);
893 loglevel = get_loglevel_by_name(conf.loglevel_arg);
894 check_key_map_args_or_die();
895 theme_init(conf.theme_arg, &theme);
896 err = false;
897 out:
898 free(config_file);
899 if (err)
900 exit(EXIT_FAILURE);
901 }
902
903 /* reread configuration, terminate on errors */
904 static void reread_conf(void)
905 {
906 /*
907 * gengetopt might print to stderr and exit on errors. So we have to
908 * shutdown curses first.
909 */
910 shutdown_curses();
911 parse_config_file_or_die(true /* override */);
912 init_curses();
913 print_in_bar(COLOR_MSG, "config file reloaded\n");
914 }
915
916 /*
917 * React to various signal-related events
918 */
919 static void handle_signal(int sig)
920 {
921 switch (sig) {
922 case SIGTERM:
923 die(EXIT_FAILURE, "only the good die young (caught SIGTERM)\n");
924 return;
925 case SIGWINCH:
926 if (curses_active()) {
927 shutdown_curses();
928 init_curses();
929 redraw_bot_win();
930 }
931 return;
932 case SIGINT:
933 PARA_WARNING_LOG("caught SIGINT, reset\n");
934 /* Nothing to do. SIGINT killed our child which gets noticed
935 * by do_select and resets everything.
936 */
937 return;
938 case SIGUSR1:
939 PARA_NOTICE_LOG("got SIGUSR1, rereading configuration\n");
940 reread_conf();
941 return;
942 case SIGCHLD:
943 check_sigchld();
944 return;
945 }
946 }
947
948 static void status_pre_select(fd_set *rfds, int *max_fileno, struct timeval *tv)
949 {
950 static struct timeval next_exec, atm, diff;
951 int ret, fds[3] = {0, 1, 0};
952 pid_t pid;
953
954 if (stat_pipe >= 0)
955 goto success;
956 /* Avoid busy loop */
957 gettimeofday(&atm, NULL);
958 if (tv_diff(&next_exec, &atm, &diff) > 0) {
959 if (tv_diff(&diff, tv, NULL) < 0)
960 *tv = diff;
961 return;
962 }
963 next_exec.tv_sec = atm.tv_sec + 2;
964 ret = para_exec_cmdline_pid(&pid, conf.stat_cmd_arg, fds);
965 if (ret < 0)
966 return;
967 ret = mark_fd_nonblocking(fds[1]);
968 if (ret < 0) {
969 close(fds[1]);
970 return;
971 }
972 stat_pipe = fds[1];
973 success:
974 para_fd_set(stat_pipe, rfds, max_fileno);
975 }
976
977 #define COMMAND_BUF_SIZE 32768
978
979 static void command_pre_select(int mode, fd_set *rfds, int *max_fileno)
980 {
981 /* check command pipe only in COMMAND_MODE */
982 if (mode != COMMAND_MODE)
983 return;
984 if (command_fds[0] >= 0)
985 para_fd_set(command_fds[0], rfds, max_fileno);
986 if (command_fds[1] >= 0)
987 para_fd_set(command_fds[1], rfds, max_fileno);
988 }
989
990 static int command_post_select(int mode, fd_set *rfds)
991 {
992 int i, ret;
993 static char command_buf[2][COMMAND_BUF_SIZE];
994 static int cbo[2]; /* command buf offsets */
995 static unsigned flags[2]; /* for for_each_line() */
996
997 if (mode != COMMAND_MODE)
998 return 0;
999 for (i = 0; i < 2; i++) {
1000 size_t sz;
1001 if (command_fds[i] < 0)
1002 continue;
1003 ret = read_nonblock(command_fds[i],
1004 command_buf[i] + cbo[i],
1005 COMMAND_BUF_SIZE - 1 - cbo[i], rfds, &sz);
1006 cbo[i] += sz;
1007 sz = cbo[i];
1008 cbo[i] = for_each_line(flags[i], command_buf[i], cbo[i],
1009 add_output_line, &i);
1010 if (sz != cbo[i]) { /* at least one line found */
1011 wrefresh(bot.win);
1012 flags[i] = 0;
1013 }
1014 if (ret < 0) {
1015 PARA_NOTICE_LOG("closing command fd %d: %s",
1016 i, para_strerror(-ret));
1017 close(command_fds[i]);
1018 command_fds[i] = -1;
1019 flags[i] = 0;
1020 cbo[i] = 0;
1021 if (command_fds[!i] < 0) /* both fds closed */
1022 return -1;
1023 }
1024 if (cbo[i] == COMMAND_BUF_SIZE - 1) {
1025 PARA_NOTICE_LOG("discarding overlong line");
1026 cbo[i] = 0;
1027 flags[i] = FELF_DISCARD_FIRST;
1028 }
1029 }
1030 return 1;
1031 }
1032
1033 static void input_pre_select(int mode, fd_set *rfds, int *max_fileno)
1034 {
1035 if (mode == GETCH_MODE || mode == COMMAND_MODE)
1036 para_fd_set(STDIN_FILENO, rfds, max_fileno);
1037 }
1038
1039 static int input_post_select(int mode)
1040 {
1041 int ret;
1042
1043 switch (mode) {
1044 case COMMAND_MODE:
1045 ret = wgetch(top.win);
1046 if (ret != ERR && ret != KEY_RESIZE) {
1047 if (cmd_pid)
1048 kill(cmd_pid, SIGTERM);
1049 return -1;
1050 }
1051 return 0;
1052 case GETCH_MODE:
1053 ret = wgetch(top.win);
1054 if (ret != ERR && ret != KEY_RESIZE)
1055 return ret;
1056 return 0;
1057 case EXTERNAL_MODE:
1058 if (cmd_pid == 0)
1059 return -1;
1060 return 0;
1061 default:
1062 assert(false); /* bug */
1063 }
1064 }
1065
1066 /*
1067 * This is the core select loop. Besides the (internal) signal
1068 * pipe, the following other fds are checked according to the mode:
1069 *
1070 * GETCH_MODE: check stdin, return when key is pressed
1071 *
1072 * COMMAND_MODE: check command fds and stdin. Return when peer has closed both
1073 * stdout and stderr or when any key is pressed.
1074 *
1075 * EXTERNAL_MODE: Check only signal pipe. Used when an external command
1076 * is running. During that time curses is disabled. Returns when
1077 * cmd_pid == 0.
1078 */
1079 static int do_select(int mode)
1080 {
1081 fd_set rfds;
1082 int ret, max_fileno;
1083 struct timeval tv;
1084
1085 repeat:
1086 tv.tv_sec = conf.timeout_arg / 1000;
1087 tv.tv_usec = (conf.timeout_arg % 1000) * 1000;
1088 // ret = refresh_status();
1089 FD_ZERO(&rfds);
1090 max_fileno = 0;
1091 status_pre_select(&rfds, &max_fileno, &tv);
1092 /* signal pipe */
1093 para_fd_set(signal_pipe, &rfds, &max_fileno);
1094 command_pre_select(mode, &rfds, &max_fileno);
1095 input_pre_select(mode, &rfds, &max_fileno);
1096 ret = para_select(max_fileno + 1, &rfds, NULL, &tv);
1097 if (ret <= 0)
1098 goto check_return; /* skip fd checks */
1099 /* signals */
1100 ret = para_next_signal(&rfds);
1101 if (ret > 0)
1102 handle_signal(ret);
1103 /* read command pipe if ready */
1104 ret = command_post_select(mode, &rfds);
1105 if (ret < 0)
1106 return 0;
1107 status_post_select(&rfds);
1108 check_return:
1109 ret = input_post_select(mode);
1110 if (ret != 0)
1111 return ret;
1112 goto repeat;
1113 }
1114
1115 /* read from command pipe and print data to bot window */
1116 static void exec_and_display_cmd(const char *cmd)
1117 {
1118 int ret, fds[3] = {0, 1, 1};
1119
1120 outputf(COLOR_COMMAND, "%s", cmd);
1121 ret = para_exec_cmdline_pid(&cmd_pid, cmd, fds);
1122 if (ret < 0)
1123 return;
1124 ret = mark_fd_nonblocking(fds[1]);
1125 if (ret < 0)
1126 goto fail;
1127 ret = mark_fd_nonblocking(fds[2]);
1128 if (ret < 0)
1129 goto fail;
1130 command_fds[0] = fds[1];
1131 command_fds[1] = fds[2];
1132 if (do_select(COMMAND_MODE) >= 0)
1133 PARA_INFO_LOG("command complete\n");
1134 else
1135 PARA_NOTICE_LOG("command aborted\n");
1136 print_in_bar(COLOR_MSG, " ");
1137 return;
1138 fail:
1139 PARA_ERROR_LOG("%s\n", para_strerror(-ret));
1140 close(command_fds[0]);
1141 close(command_fds[1]);
1142 }
1143
1144 static void para_cmd(char *cmd)
1145 {
1146 char *c;
1147
1148 print_in_bar(COLOR_MSG, "executing client command, hit any key to abort\n");
1149 c = make_message(BINDIR "/para_client -- %s", cmd);
1150 exec_and_display_cmd(c);
1151 free(c);
1152 }
1153
1154 static void display_cmd(char *cmd)
1155 {
1156 print_in_bar(COLOR_MSG, "executing display command, hit any key to abort");
1157 exec_and_display_cmd(cmd);
1158 }
1159
1160 /*
1161 * shutdown curses and stat pipe before executing external commands
1162 */
1163 static void external_cmd(char *cmd)
1164 {
1165 int fds[3] = {-1, -1, -1};
1166
1167 if (cmd_pid)
1168 return;
1169 shutdown_curses();
1170 if (para_exec_cmdline_pid(&cmd_pid, cmd, fds) < 0)
1171 return;
1172 do_select(EXTERNAL_MODE);
1173 init_curses();
1174 }
1175
1176 static void print_scroll_msg(void)
1177 {
1178 unsigned lines_total, filled = ringbuffer_filled(bot_win_rb);
1179 int first_rbe = first_visible_rbe(&lines_total);
1180
1181 print_in_bar(COLOR_MSG, "scrolled view: %d-%d/%d\n", filled - first_rbe,
1182 filled - scroll_position, ringbuffer_filled(bot_win_rb));
1183 }
1184
1185 static void com_scroll_top(void)
1186 {
1187 int i = RINGBUFFER_SIZE - 1, bot_lines = get_num_lines(&bot);
1188 unsigned lines = 0;
1189
1190 while (i > 0 && !ringbuffer_get(bot_win_rb, i))
1191 i--;
1192 /* i is oldest entry */
1193 for (; lines < bot_lines && i >= 0; i--) {
1194 struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i);
1195 if (!rbe)
1196 break;
1197 lines += NUM_LINES(rbe->len);
1198 }
1199 i++;
1200 if (lines > 0 && scroll_position != i) {
1201 scroll_position = i;
1202 redraw_bot_win();
1203 print_scroll_msg();
1204 return;
1205 }
1206 print_in_bar(COLOR_ERRMSG, "top of buffer is shown\n");
1207 }
1208
1209 static void com_cancel_scrolling(void)
1210 {
1211
1212 if (scroll_position == 0) {
1213 print_in_bar(COLOR_ERRMSG, "bottom of buffer is shown\n");
1214 return;
1215 }
1216 scroll_position = 0;
1217 redraw_bot_win();
1218 }
1219
1220 static void com_page_down(void)
1221 {
1222 unsigned lines = 0;
1223 int i = scroll_position, bot_lines = get_num_lines(&bot);
1224
1225 while (lines < bot_lines && --i > 0) {
1226 struct rb_entry *rbe = ringbuffer_get(bot_win_rb, i);
1227 if (!rbe)
1228 break;
1229 lines += NUM_LINES(rbe->len);
1230 }
1231 if (lines) {
1232 scroll_position = i;
1233 redraw_bot_win();
1234 print_scroll_msg();
1235 return;
1236 }
1237 print_in_bar(COLOR_ERRMSG, "bottom of buffer is shown\n");
1238 }
1239
1240 static void com_page_up(void)
1241 {
1242 unsigned lines;
1243 int fvr = first_visible_rbe(&lines), bot_lines = get_num_lines(&bot);
1244
1245 if (fvr < 0 || fvr + 1 >= ringbuffer_filled(bot_win_rb)) {
1246 print_in_bar(COLOR_ERRMSG, "top of buffer is shown\n");
1247 return;
1248 }
1249 scroll_position = fvr + 1;
1250 for (; scroll_position > 0; scroll_position--) {
1251 first_visible_rbe(&lines);
1252 if (lines == bot_lines)
1253 break;
1254 }
1255 redraw_bot_win();
1256 print_scroll_msg();
1257 }
1258
1259 static void com_scroll_down(void)
1260 {
1261 struct rb_entry *rbe;
1262 int rbe_lines, bot_lines = get_num_lines(&bot);
1263
1264 if (!scroll_position) {
1265 print_in_bar(COLOR_ERRMSG, "bottom of buffer is shown\n");
1266 return;
1267 }
1268 scroll_position--;
1269 rbe = ringbuffer_get(bot_win_rb, scroll_position);
1270 rbe_lines = NUM_LINES(rbe->len);
1271 wscrl(bot.win, rbe_lines);
1272 wmove(bot.win, bot_lines - rbe_lines, 0);
1273 wattron(bot.win, COLOR_PAIR(rbe->color));
1274 waddstr(bot.win, rbe->msg);
1275 wrefresh(bot.win);
1276 print_scroll_msg();
1277 }
1278
1279 static void com_scroll_up(void)
1280 {
1281 struct rb_entry *rbe = NULL;
1282 unsigned lines;
1283 int i, first_rbe, num_scroll;
1284
1285 /* the entry that is going to vanish */
1286 rbe = ringbuffer_get(bot_win_rb, scroll_position);
1287 if (!rbe)
1288 goto err_out;
1289 num_scroll = NUM_LINES(rbe->len);
1290 first_rbe = first_visible_rbe(&lines);
1291 if (first_rbe < 0 || (first_rbe == ringbuffer_filled(bot_win_rb) - 1))
1292 goto err_out;
1293 scroll_position++;
1294 wscrl(bot.win, -num_scroll);
1295 i = draw_top_rbe(&lines);
1296 if (i < 0)
1297 goto err_out;
1298 while (i > 0 && lines < num_scroll) {
1299 int rbe_lines;
1300 rbe = ringbuffer_get(bot_win_rb, --i);
1301 if (!rbe)
1302 break;
1303 rbe_lines = NUM_LINES(rbe->len);
1304 lines += rbe_lines;
1305 wattron(bot.win, COLOR_PAIR(rbe->color));
1306 waddstr(bot.win, "\n");
1307 waddstr(bot.win, rbe->msg);
1308 if (!i)
1309 break;
1310 i--;
1311 }
1312 wrefresh(bot.win);
1313 print_scroll_msg();
1314 return;
1315 err_out:
1316 print_in_bar(COLOR_ERRMSG, "top of buffer is shown\n");
1317 }
1318
1319 static void com_ll_decr(void)
1320 {
1321 if (loglevel <= LL_DEBUG) {
1322 print_in_bar(COLOR_ERRMSG,
1323 "loglevel already at maximal verbosity\n");
1324 return;
1325 }
1326 loglevel--;
1327 print_in_bar(COLOR_MSG, "loglevel set to %d\n", loglevel);
1328 }
1329
1330 static void com_ll_incr(void)
1331 {
1332 if (loglevel >= LL_EMERG) {
1333 print_in_bar(COLOR_ERRMSG,
1334 "loglevel already at minimal verbosity\n");
1335 return;
1336 }
1337 loglevel++;
1338 print_in_bar(COLOR_MSG, "loglevel set to %d\n", loglevel);
1339 }
1340
1341 static void com_reread_conf(void)
1342 {
1343 reread_conf();
1344 }
1345
1346 static void com_help(void)
1347 {
1348 int i;
1349
1350 for (i = 0; i < conf.key_map_given; ++i) {
1351 char *handler, *arg, *tmp = para_strdup(conf.key_map_arg[i]);
1352 const char *handler_text = "???", *desc = NULL;
1353
1354 if (!split_key_map(tmp, &handler, &arg)) {
1355 free(tmp);
1356 return;
1357 }
1358 switch (*handler) {
1359 case 'i':
1360 handler_text = "internal";
1361 desc = command_list[find_cmd_byname(arg)].description;
1362 break;
1363 case 'x': handler_text = "external"; break;
1364 case 'd': handler_text = "display "; break;
1365 case 'p': handler_text = "para "; break;
1366 }
1367 outputf(COLOR_MSG, "%s\t%s\t%s%s\t%s", tmp, handler_text, arg,
1368 strlen(arg) < 8? "\t" : "",
1369 desc? desc : "");
1370 free(tmp);
1371 }
1372 for (i = 0; command_list[i].handler; i++) {
1373 struct gui_command gc = command_list[i];
1374
1375 outputf(COLOR_MSG, "%s\tinternal\t%s\t%s%s", gc.key, gc.name,
1376 strlen(gc.name) < 8? "\t" : "",
1377 gc.description);
1378 }
1379 print_in_bar(COLOR_MSG, "try \"para_gui -h\" or \"para_client help\" "
1380 "for more info");
1381 }
1382
1383 static void com_shrink_top_win(void)
1384 {
1385 int top_lines = get_num_lines(&top);
1386
1387 if (top_lines <= theme.top_lines_min) {
1388 PARA_WARNING_LOG("can not decrease top window\n");
1389 return;
1390 }
1391 init_wins(top_lines - 1);
1392 print_in_bar(COLOR_MSG, "%s", "decreased top window");
1393 }
1394
1395 static void com_enlarge_top_win(void)
1396 {
1397 int top_lines = get_num_lines(&top), bot_lines = get_num_lines(&bot);
1398
1399 if (bot_lines < 3) {
1400 PARA_WARNING_LOG("can not increase top window\n");
1401 return;
1402 }
1403 init_wins(top_lines + 1);
1404 print_in_bar(COLOR_MSG, "increased top window");
1405 }
1406
1407 static void com_version(void)
1408 {
1409 print_in_bar(COLOR_MSG, "%s", version_single_line("gui"));
1410 }
1411
1412 __noreturn static void com_quit(void)
1413 {
1414 die(EXIT_SUCCESS, "%s", "");
1415 }
1416
1417 static void com_refresh(void)
1418 {
1419 shutdown_curses();
1420 init_curses();
1421 }
1422
1423 static void com_next_theme(void)
1424 {
1425 theme_next(&theme);
1426 com_refresh();
1427 }
1428
1429 static void com_prev_theme(void)
1430 {
1431 theme_prev(&theme);
1432 com_refresh();
1433 }
1434
1435 static void handle_command(int c)
1436 {
1437 int i;
1438
1439 /* first check user-defined key bindings */
1440 for (i = 0; i < conf.key_map_given; ++i) {
1441 char *tmp, *handler, *arg;
1442
1443 tmp = para_strdup(conf.key_map_arg[i]);
1444 if (!split_key_map(tmp, &handler, &arg)) {
1445 free(tmp);
1446 return;
1447 }
1448 if (strcmp(tmp, km_keyname(c))) {
1449 free(tmp);
1450 continue;
1451 }
1452 if (*handler == 'd')
1453 display_cmd(arg);
1454 else if (*handler == 'x')
1455 external_cmd(arg);
1456 else if (*handler == 'p')
1457 para_cmd(arg);
1458 else if (*handler == 'i') {
1459 int num = find_cmd_byname(arg);
1460 if (num >= 0)
1461 command_list[num].handler();
1462 }
1463 free(tmp);
1464 return;
1465 }
1466 /* not found, check internal key bindings */
1467 for (i = 0; command_list[i].handler; i++) {
1468 if (!strcmp(km_keyname(c), command_list[i].key)) {
1469 command_list[i].handler();
1470 return;
1471 }
1472 }
1473 print_in_bar(COLOR_ERRMSG, "key '%s' is not bound, press ? for help",
1474 km_keyname(c));
1475 }
1476
1477 __noreturn static void print_help_and_die(void)
1478 {
1479 struct ggo_help h = DEFINE_GGO_HELP(gui);
1480 bool d = conf.detailed_help_given;
1481
1482 ggo_print_help(&h, d? GPH_STANDARD_FLAGS_DETAILED : GPH_STANDARD_FLAGS);
1483 exit(0);
1484 }
1485
1486 int main(int argc, char *argv[])
1487 {
1488 int ret;
1489
1490 gui_cmdline_parser(argc, argv, &conf); /* exits on errors */
1491 loglevel = get_loglevel_by_name(conf.loglevel_arg);
1492 version_handle_flag("gui", conf.version_given);
1493 if (conf.help_given || conf.detailed_help_given)
1494 print_help_and_die();
1495 parse_config_file_or_die(false /* override */);
1496 setup_signal_handling();
1497 bot_win_rb = ringbuffer_new(RINGBUFFER_SIZE);
1498 setlocale(LC_CTYPE, "");
1499 initscr(); /* needed only once, always successful */
1500 init_curses();
1501 for (;;) {
1502 print_status_bar();
1503 ret = do_select(GETCH_MODE);
1504 if (!ret)
1505 continue;
1506 print_in_bar(COLOR_MSG, " ");
1507 handle_command(ret);
1508 }
1509 }