handle the server just killing us sometimes
[cycon.git] / ui.c
blob9401e0d0fee54f1b334ba4f326f35daf13c1099b
1 /*
2 * Copyright (c) 2018, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
7 * copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
18 #include <errno.h>
19 #include <poll.h>
20 #include <signal.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <time.h>
25 #include <unistd.h>
26 #include <wchar.h>
28 #include <sys/ioctl.h>
30 #include <termkey.h>
32 #include <unibilium.h>
34 #include "cycon.h"
35 #include "macros.h"
37 #define xmin(a, b) ((a) < (b) ? (a) : (b))
38 #define xmax(a, b) ((a) > (b) ? (a) : (b))
40 #define FG "\x1b[38;2;%d;%d;%dm"
41 #define BG "\x1b[48;2;%d;%d;%dm"
43 /* Resize handling */
44 static volatile sig_atomic_t sigwinch_seen;
46 /* (unibi_var_t) { 0 } is too long */
47 static unibi_var_t Z;
49 /* Printf in format suitable for unibi_format() */
50 static void out_printf(void *ctx, const char *buf, size_t len)
52 ssize_t wout = 0;
53 size_t wtot = 0;
55 UNUSED(ctx);
57 while (wtot < len) {
58 if ((wout = write(1, buf + wtot, len - wtot)) < 0) {
60 /* An error that would be a pain in the neck to extract */
61 return;
64 wtot += wout;
68 /* Wrapper for "Just execute the damn command" */
69 static void do_unibi(unibi_term *ut, enum unibi_string which, unibi_var_t v1,
70 unibi_var_t v2, unibi_var_t v3, unibi_var_t v4, unibi_var_t
71 v5, unibi_var_t v6,
72 unibi_var_t v7, unibi_var_t v8, unibi_var_t v9)
74 unibi_var_t param[9] = { v1, v2, v3, v4, v5, v6, v7, v8, v9 };
75 unibi_var_t zero_dyn[26] = { 0 };
76 unibi_var_t zero_static[26] = { 0 };
77 const char *format_string = unibi_get_str(ut, which);
79 if (!format_string) {
80 return;
83 unibi_format(zero_dyn, zero_static, format_string, param, out_printf, 0,
84 0, 0);
87 /* Write hh:mm:ss / hh:mm:ss */
88 static void print_timing(struct playlist volatile *p)
90 int play = p->current_playtime;
91 int length = p->current_length;
92 int cur_s = 0;
93 int cur_m = 0;
94 int cur_h = 0;
95 int len_s = 0;
96 int len_m = 0;
97 int len_h = 0;
99 if (p->paused) {
100 printf("═══════ paused ══════");
102 return;
105 cur_s = play % 60;
106 play /= 60;
107 cur_m = play % 60;
108 play /= 60;
109 cur_h = play % 100;
110 len_s = length % 60;
111 length /= 60;
112 len_m = length % 60;
113 length /= 60;
114 len_h = length % 100;
116 if (cur_h) {
117 printf(" %02d:", cur_h);
118 } else {
119 printf(" ");
122 printf("%02d:%02d / ", cur_m, cur_s);
124 if (len_h ||
125 cur_h) {
126 printf("%02d:", len_h);
127 } else {
128 printf(" ");
131 printf("%02d:%02d ", len_m, len_s);
134 /* Print out text, with left indentation. */
135 static void trickle_out(struct state volatile *s, int left_pad, const
136 char *text, int row)
138 mbstate_t mbs = { 0 };
139 size_t left = 0;
140 wchar_t wc = 0;
141 size_t mbret = 0;
142 int cur_pos = left_pad;
143 int max_width = s->term_width;
145 if (!text) {
146 return;
149 left = strlen(text);
151 while (*text) {
152 int print_num = 0;
153 int this_cells = 0;
155 mbret = mbrtowc(&wc, text, left, &mbs);
157 if (mbret >= (size_t) -2) {
158 mbs = (mbstate_t) { 0 };
159 print_num = 1;
160 this_cells = 2;
161 continue;
162 } else {
163 print_num = (int) mbret;
164 this_cells = wcwidth(wc);
167 if (this_cells + cur_pos > max_width) {
168 fflush(stdout);
169 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
171 fflush(stdout);
172 row++;
174 if (row >= (int) s->pos_mid_line) {
175 return;
178 do_unibi(s->ut, unibi_cursor_address,
179 unibi_var_from_num(row), Z, Z, Z, Z, Z, Z, Z,
181 printf("%*s%.*s", left_pad, "", print_num, text);
182 cur_pos = left_pad + this_cells;
183 } else {
184 printf("%.*s", print_num, text);
187 text += print_num;
188 cur_pos += this_cells;
191 fflush(stdout);
194 /* Blit the whole damn thing */
195 static void redraw(struct state volatile *s)
197 /* Come off it */
198 if (!s ||
199 s->term_height < 5) {
200 return;
203 /* First, the upper pane */
204 if (!s->dirty_top) {
205 goto top_drawn;
208 printf("\x1b[0m");
209 fflush(stdout);
211 if (s->server_error) {
212 size_t error_len = strlen(s->server_error);
213 int lines = (error_len / s->term_width) + 1;
214 int error_start_pos = xmax(0, ((intmax_t) s->pos_mid_line -
215 lines) / 2);
217 for (size_t k = 0; k < s->pos_mid_line; k++) {
218 do_unibi(s->ut, unibi_cursor_address,
219 unibi_var_from_num(k), Z, Z, Z, Z, Z, Z, Z, Z);
220 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
224 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
225 error_start_pos), Z, Z, Z, Z, Z, Z, Z, Z);
226 printf("%s", s->server_error);
227 fflush(stdout);
228 } else if (s->uri_or_chat) {
229 const char *u = 0;
230 size_t idx = s->playlist_hover_idx;
232 if (idx < s->playlist.entries_len) {
233 u = s->playlist.entries[idx].uri;
234 } else {
235 u = " [ No media currently playing ]";
238 size_t uri_len = u ? strlen(u) : 0;
239 int lines = (uri_len / s->term_width) + 1;
240 int uri_start_pos = xmax(0, ((intmax_t) s->pos_mid_line -
241 lines) / 2);
243 for (size_t k = 0; k < s->pos_mid_line; k++) {
244 do_unibi(s->ut, unibi_cursor_address,
245 unibi_var_from_num(k), Z, Z, Z, Z, Z, Z, Z, Z);
246 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
250 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
251 uri_start_pos), Z, Z, Z, Z, Z, Z, Z, Z);
252 printf("%s", u);
253 fflush(stdout);
254 } else {
255 if (s->term_width < 20) {
256 goto top_drawn;
259 intmax_t k = s->chat_log.msgs_len - 1;
260 intmax_t pos = s->pos_mid_line - 1;
261 size_t un_len = 0;
262 int lines = 0;
263 size_t text_width = 0;
264 struct chat_msg *m = 0;
266 another_chat_msg:
268 if (pos < 0 || pos > s->term_height) {
269 /* We probably wrapped to -1 somehow */
270 goto top_drawn;
273 if (k < 0 ||
274 k >= (int) s->chat_log.msgs_len) {
275 /* We wrapped to -1 */
276 do_unibi(s->ut, unibi_cursor_address,
277 unibi_var_from_num(pos), Z, Z, Z, Z, Z, Z, Z,
279 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
281 fflush(stdout);
282 pos--;
283 goto another_chat_msg;
286 m = &s->chat_log.msgs[k];
287 text_width = strwidth(m->text);
288 un_len = strlen(m->user);
289 un_len = xmax(un_len, 14) + 2;
290 lines = 1 + (xmax(text_width - 1, 0) / (s->term_width -
291 un_len));
293 if (lines - 1 > pos) {
294 for (int j = 0; j <= pos; ++j) {
295 do_unibi(s->ut, unibi_cursor_address,
296 unibi_var_from_num(j), Z, Z, Z, Z, Z,
297 Z, Z, Z);
298 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z,
299 Z, Z, Z);
300 fflush(stdout);
303 goto top_drawn;
304 } else {
305 do_unibi(s->ut, unibi_cursor_address,
306 unibi_var_from_num(pos - lines + 1), Z, Z, Z,
307 Z, Z, Z,
308 Z, Z);
309 printf("%*s: ", 14, m->user);
310 fflush(stdout);
311 trickle_out(s, un_len, m->text, pos - lines +
313 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
315 fflush(stdout);
318 k--;
319 pos -= lines;
320 goto another_chat_msg;
323 top_drawn:
325 /* The middle line */
326 if (s->dirty_line[s->pos_mid_line]) {
327 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
328 s->pos_mid_line), Z, Z, Z, Z, Z, Z, Z, Z);
330 for (int k = 0; k < s->term_width; ++k) {
331 if (k == 10) {
332 printf("╤");
333 } else {
334 printf("═");
339 s->dirty_top = 0;
340 fflush(stdout);
342 if (s->dirty_playlist) {
343 for (size_t k = s->pos_mid_line + 1; k <
344 (size_t) s->term_height; ++k) {
345 s->dirty_line[k] = 1;
348 s->dirty_playlist = 0;
351 /* Now the bottom part */
352 for (size_t k = 0; s->pos_mid_line + 1 + k < (size_t) s->pos_hints_top;
353 ++k) {
354 int pos = s->pos_mid_line + 1 + k;
355 size_t idx = s->playlist_offset + k;
357 if (!s->dirty_line[pos]) {
358 continue;
361 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(pos),
362 Z, Z, Z, Z, Z, Z, Z, Z);
363 const char *divider = " │ ";
364 const char *runtime = "";
365 const char *title = "";
366 int title_width = 0;
367 int r = 0;
369 if (idx < s->playlist.entries_len) {
370 struct playlist_entry *e = &s->playlist.entries[idx];
372 runtime = e->runtime ? e->runtime : "";
373 title = e->title ? e->title : "";
374 title_width = e->title_width;
376 if (s->playlist.current_playing_uid ==
377 s->playlist.entries[idx].uid) {
378 /* divider = "━┿━"; */
379 divider = " ┿ ";
383 if (s->playlist_hover_idx == idx) {
384 printf("\x1b[48;2;%d;%d;%dm", 34, 54, 69);
385 } else {
386 printf("\x1b[48;2;%d;%d;%dm", 40, 40, 40);
389 if (s->term_width > 1 + 8 + 3) {
390 printf(" %*s%s%s", 8, runtime, divider, title);
391 r = 1 + 8 + 3 + title_width;
394 while (r < s->term_width) {
395 putchar(' ');
396 r++;
399 fflush(stdout);
402 printf("\x1b[0m");
403 fflush(stdout);
405 /* The bottom line */
406 if (s->dirty_line[s->term_height - 3]) {
407 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
408 s->term_height - 3), Z, Z, Z, Z, Z, Z, Z, Z);
409 printf("\x1b[48;2;%d;%d;%dm", 40, 40, 40);
411 for (int k = 0; k < s->term_width; ++k) {
412 if (k == 10) {
413 printf("╧");
414 } else if (k == 15 &&
415 s->term_width > 35 &&
416 s->playlist.current_playing_uid >= 0) {
417 print_timing(&s->playlist);
418 k = 35;
419 } else {
420 printf("═");
424 fflush(stdout);
427 /* Bottom text */
428 if (!s->dirty_line[s->term_height - 2] &&
429 !s->dirty_line[s->term_height - 1]) {
430 goto all_done;
433 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
434 s->term_height - 2), Z, Z, Z, Z, Z, Z, Z, Z);
435 printf(BG " C " BG ": view chat ", 34, 54, 69, 40, 40, 40);
436 printf(BG " K " BG ": scroll up ", 34, 54, 69, 40, 40, 40);
437 printf(BG " V " BG ": mpv {selected}", 34, 54, 69, 40, 40, 40);
439 for (int r = 71; r < s->term_width; ++r) {
440 putchar(' ');
443 printf("\x1b[0m");
444 fflush(stdout);
445 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
446 s->term_height - 1), Z, Z, Z, Z, Z, Z, Z, Z);
447 printf(BG " U " BG ": view URIs ", 34, 54, 69, 40, 40, 40);
448 printf(BG " J " BG ": scroll down ", 34, 54, 69, 40, 40, 40);
449 printf(BG " Q " BG ": quit", 34, 54, 69, 40, 40, 40);
451 for (int r = 61; r < s->term_width; ++r) {
452 putchar(' ');
455 printf("\x1b[0m");
456 fflush(stdout);
457 all_done:
459 for (int k = 0; k < s->term_height; ++k) {
460 s->dirty_line[k] = 0;
464 /* Move the hovered line up and down */
465 static void move_playlist_hover(struct state volatile *s, int delta)
467 size_t old_hover_pos = s->pos_mid_line + 1 + s->playlist_hover_idx -
468 s->playlist_offset;
469 size_t new_hover_idx = (size_t) xmax(0, xmin(
470 (intmax_t) s->
471 playlist_hover_idx + delta,
472 (intmax_t) s->playlist.
473 entries_len - 1));
474 size_t new_hover_pos = s->pos_mid_line + 1 + new_hover_idx -
475 s->playlist_offset;
476 size_t highest_cutoff = s->pos_mid_line + 4;
478 s->dirty_line[old_hover_pos] = 1;
479 s->dirty_line[new_hover_pos] = 1;
481 if (s->uri_or_chat) {
482 s->dirty_top = 1;
485 s->playlist_hover_idx = new_hover_idx;
487 while (highest_cutoff > new_hover_pos &&
488 s->playlist_offset > 0) {
489 s->playlist_offset--;
490 new_hover_pos++;
491 s->dirty_playlist = 1;
494 while (new_hover_pos + 4 > s->pos_hints_top) {
495 s->playlist_offset++;
496 new_hover_pos--;
497 s->dirty_playlist = 1;
501 /* All the input things we can do */
502 static void handle_keypress(struct state volatile *s, TermKeyKey *k)
504 switch (k->type) {
505 case TERMKEY_TYPE_KEYSYM:
507 switch (k->code.sym) {
508 case TERMKEY_SYM_UP:
509 move_playlist_hover(s, -1);
510 break;
511 case TERMKEY_SYM_DOWN:
512 move_playlist_hover(s, 1);
513 break;
514 default:
515 break;
518 break;
519 case TERMKEY_TYPE_UNICODE:
521 switch (k->code.codepoint) {
522 case 0x0063: /* c */
523 case 0x0043:
524 s->uri_or_chat = 0;
525 s->dirty_top = 1;
526 break;
527 case 0x0075: /* u */
528 case 0x0055:
529 s->uri_or_chat = 1;
530 s->dirty_top = 1;
531 break;
532 case 0x006a: /* j */
533 case 0x004a:
534 move_playlist_hover(s, 1);
535 break;
536 case 0x006b: /* k */
537 case 0x004b:
538 move_playlist_hover(s, -1);
539 break;
540 case 0x006c: /* l */
541 case 0x004c:
542 s->dirty_top = 1;
543 s->dirty_playlist = 1;
545 for (int q = 0; q < s->term_height; ++q) {
546 s->dirty_line[q] = 1;
549 break;
550 case 0x0071: /* q */
551 case 0x0051:
552 s->please_die = 1;
553 break;
554 case 0x0076: /* v */
555 case 0x0056:
557 if (s->playlist_hover_idx < s->playlist.entries_len) {
558 struct playlist_entry *e =
559 &s->playlist.entries[s->
560 playlist_hover_idx];
562 if (e->uri) {
563 launch_only_once(e->uri);
567 break;
568 default:
569 break;
572 break;
573 default:
574 break;
578 /* Make sure we don't waste too much space on empty playlists */
579 static void shrinkwrap_playlist(struct state volatile *s)
581 size_t hypothetical_pos_mid_line = 1 + (s->term_height - 2) / 3;
582 size_t new_pos_mid_line = s->pos_mid_line;
585 * Jumps around a bit too much for now, so the core of the
586 * function is commented out.
590 if (s->pos_hints_top > s->pos_mid_line + s->playlist.entries_len + 2) {
591 new_pos_mid_line = s->pos_hints_top - s->playlist.entries_len -
595 new_pos_mid_line = xmax(hypothetical_pos_mid_line, new_pos_mid_line);
597 if (new_pos_mid_line != s->pos_mid_line) {
598 s->pos_mid_line = new_pos_mid_line;
600 for (size_t k = 0; k < (size_t) s->term_height; ++k) {
601 s->dirty_line[k] = 1;
604 s->dirty_top = 1;
608 /* Terminal has changed under us */
609 static void handle_resize(struct state volatile *s)
611 struct winsize w = { 0 };
612 int *newmem = 0;
614 ioctl(1, TIOCGWINSZ, &w);
616 if (!(newmem = malloc(w.ws_row * sizeof(*s->dirty_line)))) {
617 PERROR_MESSAGE("malloc");
618 s->please_die = 1;
620 return;
623 s->term_width = w.ws_col;
624 s->term_height = w.ws_row;
625 free(s->dirty_line);
626 s->dirty_line = newmem;
627 memset(s->dirty_line, 1, s->term_height * sizeof(*s->dirty_line));
628 s->dirty_top = 1;
629 s->pos_mid_line = 1 + (s->term_height - 2) / 3;
630 s->pos_mid_line = (size_t) xmax(0, xmin((intmax_t) s->pos_mid_line,
631 s->term_height - 1));
632 s->pos_hints_top = (size_t) xmax(0, s->term_height - 3);
633 shrinkwrap_playlist(s);
634 move_playlist_hover(s, 0);
637 /* See a SIGWINCH */
638 static void sighandle_winch(int sig)
640 UNUSED(sig);
641 sigwinch_seen = 1;
644 /* Clean out our part of s, prepare to surrender terminal. */
645 static void ui_teardown(struct state volatile *s)
647 free(s->dirty_line);
648 s->dirty_line = 0;
650 /* termkey */
651 termkey_destroy(s->tk);
652 s->tk = 0;
654 /* unibilium */
655 do_unibi(s->ut, unibi_cursor_normal, Z, Z, Z, Z, Z, Z, Z, Z, Z);
656 do_unibi(s->ut, unibi_exit_attribute_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
657 do_unibi(s->ut, unibi_exit_ca_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
658 unibi_destroy(s->ut);
659 s->ut = 0;
662 /* Acquire terminal, save some data */
663 int ui_init(struct state volatile *s)
665 if (!isatty(fileno(stdin)) ||
666 !isatty(fileno(stdout))) {
667 ERROR_MESSAGE("stdin and/or stdout are not terminals");
668 ERROR_MESSAGE("[ If you really, really want to go ]");
669 ERROR_MESSAGE("[ ahead, this check is the only place ]");
670 ERROR_MESSAGE("[ in the program that cares. ]");
671 s->please_die = 1;
673 return -1;
676 /* Resize handling */
677 signal(SIGWINCH, sighandle_winch);
679 /* unibilium */
680 s->ut = unibi_from_env();
681 do_unibi(s->ut, unibi_enter_ca_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
682 do_unibi(s->ut, unibi_keypad_xmit, Z, Z, Z, Z, Z, Z, Z, Z, Z);
683 do_unibi(s->ut, unibi_cursor_invisible, Z, Z, Z, Z, Z, Z, Z, Z, Z);
684 do_unibi(s->ut, unibi_clear_screen, Z, Z, Z, Z, Z, Z, Z, Z, Z);
686 /* termkey */
687 s->tk = termkey_new(fileno(stdin), 0);
688 termkey_set_waittime(s->tk, 100);
690 return 0;
693 void ui_loop(struct state volatile *s)
695 int sv_errno = 0;
696 TermKeyResult ret;
697 TermKeyKey key;
698 int new_playtime = 0;
699 struct pollfd pfd = { .fd = fileno(stdin), .events = POLLIN };
701 sigwinch_seen = 1;
703 while (!s->please_die) {
704 if (sigwinch_seen) {
705 sigwinch_seen = 0;
706 handle_resize(s);
709 if (!s->playlist.paused) {
710 new_playtime = s->playlist.zero_playtime + (time(0) -
711 s->playlist.
712 zero_clocktime);
714 if (new_playtime != s->playlist.current_playtime) {
715 s->dirty_line[s->pos_hints_top] = 1;
718 if (new_playtime > s->playlist.current_length + 2) {
719 s->playlist.current_playing_uid = -1;
720 s->playlist.current_playtime = 0;
721 s->playlist.current_length = 0;
724 s->playlist.current_playtime = new_playtime;
727 redraw(s);
729 if (poll(&pfd, 1, 100) == 0) {
730 ret = termkey_getkey_force(s->tk, &key);
731 goto check_key;
734 if (pfd.revents & (POLLIN | POLLHUP | POLLERR)) {
735 termkey_advisereadable(s->tk);
738 ret = termkey_getkey(s->tk, &key);
739 check_key:
741 switch (ret) {
742 case TERMKEY_RES_NONE:
743 case TERMKEY_RES_AGAIN:
744 break;
745 case TERMKEY_RES_ERROR:
746 sv_errno = errno;
747 ui_teardown(s);
748 errno = sv_errno;
749 PERROR_MESSAGE("termkey_waitkey");
751 return;
752 case TERMKEY_RES_EOF:
753 goto done;
754 case TERMKEY_RES_KEY:
755 handle_keypress(s, &key);
756 break;
760 done:
761 ui_teardown(s);