make UI tips context-dependent
[cycon.git] / ui.c
blob26b4dafa23164490000f6e5d0d93ac615cb5d56b
1 /*
2 * Copyright (c) 2019, 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
51 out_printf(void *ctx, const char *buf, size_t len)
53 ssize_t wout = 0;
54 size_t wtot = 0;
56 UNUSED(ctx);
58 while (wtot < len) {
59 if ((wout = write(1, buf + wtot, len - wtot)) < 0) {
61 /* An error that would be a pain in the neck to extract */
62 return;
65 wtot += wout;
69 /* Wrapper for "Just execute the damn command" */
70 static void
71 do_unibi(unibi_term *ut, enum unibi_string which, unibi_var_t v1, unibi_var_t
72 v2, unibi_var_t v3, unibi_var_t v4, unibi_var_t v5, unibi_var_t v6,
73 unibi_var_t
74 v7, unibi_var_t v8, unibi_var_t v9)
76 unibi_var_t param[9] = { v1, v2, v3, v4, v5, v6, v7, v8, v9 };
77 unibi_var_t zero_dyn[26] = { 0 };
78 unibi_var_t zero_static[26] = { 0 };
79 const char *format_string = unibi_get_str(ut, which);
81 if (!format_string) {
82 return;
85 unibi_format(zero_dyn, zero_static, format_string, param, out_printf, 0,
86 0, 0);
89 /* Write hh:mm:ss / hh:mm:ss */
90 static void
91 print_timing(struct playlist volatile *p)
93 int play = p->current_playtime;
94 int length = p->current_length;
95 int cur_s = 0;
96 int cur_m = 0;
97 int cur_h = 0;
98 int len_s = 0;
99 int len_m = 0;
100 int len_h = 0;
102 if (p->paused) {
103 printf("═══════ paused ══════");
105 return;
108 cur_s = play % 60;
109 play /= 60;
110 cur_m = play % 60;
111 play /= 60;
112 cur_h = play % 100;
113 len_s = length % 60;
114 length /= 60;
115 len_m = length % 60;
116 length /= 60;
117 len_h = length % 100;
119 if (cur_h) {
120 printf(" %02d:", cur_h);
121 } else {
122 printf(" ");
125 printf("%02d:%02d / ", cur_m, cur_s);
127 if (len_h ||
128 cur_h) {
129 printf("%02d:", len_h);
130 } else {
131 printf(" ");
134 printf("%02d:%02d ", len_m, len_s);
137 /* Print out text, with left indentation. */
138 static void
139 trickle_out(struct state volatile *s, int left_pad, const char *text, int row)
141 mbstate_t mbs = { 0 };
142 size_t left = 0;
143 wchar_t wc = 0;
144 size_t mbret = 0;
145 int cur_pos = left_pad;
146 int max_width = s->term_width;
148 if (!text) {
149 return;
152 left = strlen(text);
154 while (*text) {
155 int print_num = 0;
156 int this_cells = 0;
158 mbret = mbrtowc(&wc, text, left, &mbs);
160 if (mbret >= (size_t) -2) {
161 mbs = (mbstate_t) { 0 };
162 print_num = 1;
163 this_cells = 2;
164 continue;
165 } else {
166 print_num = (int) mbret;
167 this_cells = wcwidth(wc);
170 if (this_cells + cur_pos > max_width) {
171 fflush(stdout);
172 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
174 fflush(stdout);
175 row++;
177 if (row >= (int) s->pos_mid_line) {
178 return;
181 do_unibi(s->ut, unibi_cursor_address,
182 unibi_var_from_num(row), Z, Z, Z, Z, Z, Z, Z,
184 printf("%*s%.*s", left_pad, "", print_num, text);
185 cur_pos = left_pad + this_cells;
186 } else {
187 printf("%.*s", print_num, text);
190 text += print_num;
191 cur_pos += this_cells;
194 fflush(stdout);
197 /* Blit the whole damn thing */
198 static void
199 redraw(struct state volatile *s)
201 /* Come off it */
202 if (!s ||
203 s->term_height < 5) {
204 return;
207 /* First, the upper pane */
208 if (!s->dirty_top) {
209 goto top_drawn;
212 printf("\x1b[0m");
213 fflush(stdout);
215 if (s->server_error) {
216 size_t error_len = strlen(s->server_error);
217 int lines = (error_len / s->term_width) + 1;
218 int error_start_pos = xmax(0, ((intmax_t) s->pos_mid_line -
219 lines) / 2);
221 for (size_t k = 0; k < s->pos_mid_line; k++) {
222 do_unibi(s->ut, unibi_cursor_address,
223 unibi_var_from_num(k), Z, Z, Z, Z, Z, Z, Z, Z);
224 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
228 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
229 error_start_pos), Z, Z, Z, Z, Z, Z, Z, Z);
230 printf("%s", s->server_error);
231 fflush(stdout);
232 } else if (s->uri_or_chat) {
233 const char *u = 0;
234 size_t idx = s->playlist_hover_idx;
236 if (idx < s->playlist.entries_len) {
237 u = s->playlist.entries[idx].uri;
238 } else {
239 u = " [ No media currently selected ]";
242 size_t uri_len = u ? strlen(u) : 0;
243 int lines = (uri_len / s->term_width) + 1;
244 int uri_start_pos = xmax(0, ((intmax_t) s->pos_mid_line -
245 lines) / 2);
247 for (size_t k = 0; k < s->pos_mid_line; k++) {
248 do_unibi(s->ut, unibi_cursor_address,
249 unibi_var_from_num(k), Z, Z, Z, Z, Z, Z, Z, Z);
250 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
254 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
255 uri_start_pos), Z, Z, Z, Z, Z, Z, Z, Z);
256 printf("%s", u);
257 fflush(stdout);
258 } else {
259 if (s->term_width < 20) {
260 goto top_drawn;
263 intmax_t k = s->chat_log.msgs_len - 1;
264 intmax_t pos = s->pos_mid_line - 1;
265 size_t un_len = 0;
266 int lines = 0;
267 size_t text_width = 0;
268 struct chat_msg *m = 0;
270 another_chat_msg:
272 if (pos < 0 ||
273 pos > s->term_height) {
274 /* We probably wrapped to -1 somehow */
275 goto top_drawn;
278 if (k < 0 ||
279 k >= (int) s->chat_log.msgs_len) {
280 /* We wrapped to -1 */
281 do_unibi(s->ut, unibi_cursor_address,
282 unibi_var_from_num(pos), Z, Z, Z, Z, Z, Z, Z,
284 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
286 fflush(stdout);
287 pos--;
288 goto another_chat_msg;
291 m = &s->chat_log.msgs[k];
292 text_width = strwidth(m->text);
293 un_len = strlen(m->user);
294 un_len = xmax(un_len, 14) + 2;
295 lines = 1 + (xmax(text_width - 1, 0) / (s->term_width -
296 un_len));
298 if (lines - 1 > pos) {
299 for (int j = 0; j <= pos; ++j) {
300 do_unibi(s->ut, unibi_cursor_address,
301 unibi_var_from_num(j), Z, Z, Z, Z, Z,
302 Z, Z, Z);
303 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z,
304 Z, Z, Z);
305 fflush(stdout);
308 goto top_drawn;
309 } else {
310 do_unibi(s->ut, unibi_cursor_address,
311 unibi_var_from_num(pos - lines + 1), Z, Z, Z,
312 Z, Z, Z,
313 Z, Z);
314 printf("%*s: ", 14, m->user);
315 fflush(stdout);
316 trickle_out(s, un_len, m->text, pos - lines + 1);
317 do_unibi(s->ut, unibi_clr_eol, Z, Z, Z, Z, Z, Z, Z, Z,
319 fflush(stdout);
322 k--;
323 pos -= lines;
324 goto another_chat_msg;
327 top_drawn:
329 /* The middle line */
330 if (s->dirty_line[s->pos_mid_line]) {
331 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
332 s->pos_mid_line), Z, Z, Z, Z, Z, Z, Z, Z);
334 for (int k = 0; k < s->term_width; ++k) {
335 if (k == 10) {
336 printf("╤");
337 } else {
338 printf("═");
343 s->dirty_top = 0;
344 fflush(stdout);
346 if (s->dirty_playlist) {
347 for (size_t k = s->pos_mid_line + 1; k <
348 (size_t) s->term_height; ++k) {
349 s->dirty_line[k] = 1;
352 s->dirty_playlist = 0;
355 /* Now the bottom part */
356 for (size_t k = 0; s->pos_mid_line + 1 + k < (size_t) s->pos_hints_top;
357 ++k) {
358 int pos = s->pos_mid_line + 1 + k;
359 size_t idx = s->playlist_offset + k;
361 if (!s->dirty_line[pos]) {
362 continue;
365 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(pos),
366 Z, Z, Z, Z, Z, Z, Z, Z);
367 const char *divider = " │ ";
368 const char *runtime = "";
369 const char *title = "";
370 int title_width = 0;
371 int r = 0;
373 if (idx < s->playlist.entries_len) {
374 struct playlist_entry *e = &s->playlist.entries[idx];
376 runtime = e->runtime ? e->runtime : "";
377 title = e->title ? e->title : "";
378 title_width = e->title_width;
380 if (s->playlist.current_playing_uid ==
381 s->playlist.entries[idx].uid) {
382 /* divider = "━┿━"; */
383 divider = " ┿ ";
387 if (s->playlist_hover_idx == idx) {
388 printf("\x1b[48;2;%d;%d;%dm", 34, 54, 69);
389 } else {
390 printf("\x1b[48;2;%d;%d;%dm", 40, 40, 40);
393 if (s->term_width > 1 + 8 + 3) {
394 printf(" %*s%s%s", 8, runtime, divider, title);
395 r = 1 + 8 + 3 + title_width;
398 while (r < s->term_width) {
399 putchar(' ');
400 r++;
403 fflush(stdout);
406 printf("\x1b[0m");
407 fflush(stdout);
409 /* The bottom line */
410 if (s->dirty_line[s->term_height - 3]) {
411 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
412 s->term_height - 3), Z, Z, Z, Z, Z, Z, Z, Z);
413 printf("\x1b[48;2;%d;%d;%dm", 40, 40, 40);
415 for (int k = 0; k < s->term_width; ++k) {
416 if (k == 10) {
417 printf("╧");
418 } else if (k == 15 &&
419 s->term_width > 35 &&
420 s->playlist.current_playing_uid >= 0) {
421 print_timing(&s->playlist);
422 k = 35;
423 } else {
424 printf("═");
428 fflush(stdout);
431 /* Bottom text */
432 if (!s->dirty_line[s->term_height - 2] &&
433 !s->dirty_line[s->term_height - 1]) {
434 goto all_done;
437 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
438 s->term_height - 2), Z, Z, Z, Z, Z, Z, Z, Z);
439 printf(BG " J " BG ": scroll down ", 34, 54, 69, 40, 40, 40);
440 printf(BG " V " BG ": mpv {selected} ", 34, 54, 69, 40, 40, 40);
442 if (s->uri_or_chat) {
443 printf(BG " C " BG ": view chat", 34, 54, 69, 40, 40, 40);
444 } else {
445 printf(BG " U " BG ": view URIs", 34, 54, 69, 40, 40, 40);
448 for (int r = 66; r < s->term_width; ++r) {
449 putchar(' ');
452 printf("\x1b[0m");
453 fflush(stdout);
454 do_unibi(s->ut, unibi_cursor_address, unibi_var_from_num(
455 s->term_height - 1), Z, Z, Z, Z, Z, Z, Z, Z);
456 printf(BG " K " BG ": scroll up ", 34, 54, 69, 40, 40, 40);
457 printf(BG " D " BG ": download {selected} ", 34, 54, 69, 40, 40, 40);
458 printf(BG " Q " BG ": quit", 34, 54, 69, 40, 40, 40);
460 for (int r = 61; r < s->term_width; ++r) {
461 putchar(' ');
464 printf("\x1b[0m");
465 fflush(stdout);
466 all_done:
468 for (int k = 0; k < s->term_height; ++k) {
469 s->dirty_line[k] = 0;
473 /* Move the hovered line up and down */
474 static void
475 move_playlist_hover(struct state volatile *s, int delta)
477 size_t old_hover_pos = s->pos_mid_line + 1 + s->playlist_hover_idx -
478 s->playlist_offset;
479 size_t new_hover_idx = (size_t) xmax(0, xmin(
480 (intmax_t) s->
481 playlist_hover_idx + delta,
482 (intmax_t) s->playlist.
483 entries_len - 1));
484 size_t new_hover_pos = s->pos_mid_line + 1 + new_hover_idx -
485 s->playlist_offset;
486 size_t highest_cutoff = s->pos_mid_line + 4;
488 if (old_hover_pos >= (size_t) s->term_height) {
489 old_hover_pos = xmax(0, s->term_height - 1);
492 if (new_hover_pos >= (size_t) s->term_height) {
493 new_hover_pos = xmax(0, s->term_height - 1);
496 s->dirty_line[old_hover_pos] = 1;
497 s->dirty_line[new_hover_pos] = 1;
499 if (s->uri_or_chat) {
500 s->dirty_top = 1;
503 s->playlist_hover_idx = new_hover_idx;
505 while (highest_cutoff > new_hover_pos &&
506 s->playlist_offset > 0) {
507 s->playlist_offset--;
508 new_hover_pos++;
509 s->dirty_playlist = 1;
512 while (new_hover_pos + 4 > s->pos_hints_top) {
513 s->playlist_offset++;
514 new_hover_pos--;
515 s->dirty_playlist = 1;
519 /* All the input things we can do */
520 static void
521 handle_keypress(struct state volatile *s, TermKeyKey *k)
523 switch (k->type) {
524 case TERMKEY_TYPE_KEYSYM:
526 switch (k->code.sym) {
527 case TERMKEY_SYM_UP:
528 move_playlist_hover(s, -1);
529 break;
530 case TERMKEY_SYM_DOWN:
531 move_playlist_hover(s, 1);
532 break;
533 default:
534 break;
537 break;
538 case TERMKEY_TYPE_UNICODE:
540 switch (k->code.codepoint) {
541 case 0x0063: /* c */
542 case 0x0043:
543 s->uri_or_chat = 0;
544 s->dirty_top = 1;
545 s->dirty_playlist = 1;
546 break;
547 case 0x0075: /* u */
548 case 0x0055:
549 s->uri_or_chat = 1;
550 s->dirty_top = 1;
551 s->dirty_playlist = 1;
552 break;
553 case 0x006a: /* j */
554 case 0x004a:
555 move_playlist_hover(s, 1);
556 break;
557 case 0x006b: /* k */
558 case 0x004b:
559 move_playlist_hover(s, -1);
560 break;
561 case 0x006c: /* l */
562 case 0x004c:
563 s->dirty_top = 1;
564 s->dirty_playlist = 1;
566 for (int q = 0; q < s->term_height; ++q) {
567 s->dirty_line[q] = 1;
570 break;
571 case 0x0071: /* q */
572 case 0x0051:
573 s->please_die = 1;
574 break;
575 case 0x0064: /* d */
576 case 0x0044:
578 if (s->playlist_hover_idx < s->playlist.entries_len) {
579 struct playlist_entry *e =
580 &s->playlist.entries[s->
581 playlist_hover_idx];
583 if (e->uri) {
584 launch_downloader(e->uri);
588 break;
589 case 0x0076: /* v */
590 case 0x0056:
592 if (s->playlist_hover_idx < s->playlist.entries_len) {
593 struct playlist_entry *e =
594 &s->playlist.entries[s->
595 playlist_hover_idx];
597 if (e->uri) {
598 launch_player(e->uri);
602 break;
603 default:
604 break;
607 break;
608 default:
609 break;
613 /* Make sure we don't waste too much space on empty playlists */
614 static void
615 shrinkwrap_playlist(struct state volatile *s)
617 size_t hypothetical_pos_mid_line = 1 + (s->term_height - 2) / 3;
618 size_t new_pos_mid_line = s->pos_mid_line;
621 * Jumps around a bit too much for now, so the core of the
622 * function is commented out.
626 if (s->pos_hints_top > s->pos_mid_line + s->playlist.entries_len + 2) {
627 new_pos_mid_line = s->pos_hints_top - s->playlist.entries_len -
631 new_pos_mid_line = xmax(hypothetical_pos_mid_line, new_pos_mid_line);
633 if (new_pos_mid_line != s->pos_mid_line) {
634 s->pos_mid_line = new_pos_mid_line;
636 for (size_t k = 0; k < (size_t) s->term_height; ++k) {
637 s->dirty_line[k] = 1;
640 s->dirty_top = 1;
644 /* Terminal has changed under us */
645 static void
646 handle_resize(struct state volatile *s)
648 struct winsize w = { 0 };
649 int *newmem = 0;
650 size_t n_rows = 0;
652 ioctl(1, TIOCGWINSZ, &w);
653 n_rows = w.ws_row;
655 if (n_rows < 8) {
656 n_rows = 8;
659 if (!(newmem = malloc(n_rows * sizeof(*s->dirty_line)))) {
660 PERROR_MESSAGE("malloc");
661 s->please_die = 1;
663 return;
666 s->term_width = w.ws_col;
667 s->term_height = w.ws_row;
668 free(s->dirty_line);
669 s->dirty_line = newmem;
670 memset(s->dirty_line, 1, s->term_height * sizeof(*s->dirty_line));
671 s->dirty_top = 1;
672 s->pos_mid_line = 1 + (s->term_height - 2) / 3;
673 s->pos_mid_line = (size_t) xmax(0, xmin((intmax_t) s->pos_mid_line,
674 s->term_height - 1));
675 s->pos_hints_top = (size_t) xmax(0, s->term_height - 3);
676 shrinkwrap_playlist(s);
677 move_playlist_hover(s, 0);
680 /* See a SIGWINCH */
681 static void
682 sighandle_winch(int sig)
684 UNUSED(sig);
685 sigwinch_seen = 1;
688 /* Clean out our part of s, prepare to surrender terminal. */
689 static void
690 ui_teardown(struct state volatile *s)
692 free(s->dirty_line);
693 s->dirty_line = 0;
695 /* termkey */
696 termkey_destroy(s->tk);
697 s->tk = 0;
699 /* unibilium */
700 do_unibi(s->ut, unibi_cursor_normal, Z, Z, Z, Z, Z, Z, Z, Z, Z);
701 do_unibi(s->ut, unibi_exit_attribute_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
702 do_unibi(s->ut, unibi_exit_ca_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
703 unibi_destroy(s->ut);
704 s->ut = 0;
707 /* Acquire terminal, save some data */
709 ui_init(struct state volatile *s)
711 if (!isatty(fileno(stdin)) ||
712 !isatty(fileno(stdout))) {
713 ERROR_MESSAGE("stdin and/or stdout are not terminals");
714 ERROR_MESSAGE("[ If you really, really want to go ]");
715 ERROR_MESSAGE("[ ahead, this check is the only place ]");
716 ERROR_MESSAGE("[ in the program that cares. ]");
717 s->please_die = 1;
719 return -1;
722 /* Resize handling */
723 signal(SIGWINCH, sighandle_winch);
725 /* unibilium */
726 s->ut = unibi_from_env();
727 do_unibi(s->ut, unibi_enter_ca_mode, Z, Z, Z, Z, Z, Z, Z, Z, Z);
728 do_unibi(s->ut, unibi_keypad_xmit, Z, Z, Z, Z, Z, Z, Z, Z, Z);
729 do_unibi(s->ut, unibi_cursor_invisible, Z, Z, Z, Z, Z, Z, Z, Z, Z);
730 do_unibi(s->ut, unibi_clear_screen, Z, Z, Z, Z, Z, Z, Z, Z, Z);
732 /* termkey */
733 s->tk = termkey_new(fileno(stdin), 0);
734 termkey_set_waittime(s->tk, 100);
736 return 0;
739 void
740 ui_loop(struct state volatile *s)
742 int sv_errno = 0;
743 TermKeyResult ret;
744 TermKeyKey key;
745 int new_playtime = 0;
746 struct pollfd pfd = { .fd = fileno(stdin), .events = POLLIN };
748 sigwinch_seen = 1;
750 while (!s->please_die) {
751 if (sigwinch_seen) {
752 sigwinch_seen = 0;
753 handle_resize(s);
756 if (!s->playlist.paused) {
757 new_playtime = s->playlist.zero_playtime + (time(0) -
758 s->playlist.
759 zero_clocktime);
761 if (new_playtime != s->playlist.current_playtime) {
762 s->dirty_line[s->pos_hints_top] = 1;
765 if (new_playtime > s->playlist.current_length + 2) {
766 s->playlist.current_playing_uid = -1;
767 s->playlist.current_playtime = 0;
768 s->playlist.current_length = 0;
771 s->playlist.current_playtime = new_playtime;
774 redraw(s);
776 if (poll(&pfd, 1, 100) == 0) {
777 ret = termkey_getkey_force(s->tk, &key);
778 goto check_key;
781 if (pfd.revents & (POLLIN | POLLHUP | POLLERR)) {
782 termkey_advisereadable(s->tk);
785 ret = termkey_getkey(s->tk, &key);
786 check_key:
788 switch (ret) {
789 case TERMKEY_RES_NONE:
790 case TERMKEY_RES_AGAIN:
791 break;
792 case TERMKEY_RES_ERROR:
793 sv_errno = errno;
794 ui_teardown(s);
795 errno = sv_errno;
796 PERROR_MESSAGE("termkey_waitkey");
798 return;
799 case TERMKEY_RES_EOF:
800 goto done;
801 case TERMKEY_RES_KEY:
802 handle_keypress(s, &key);
803 break;
807 done:
808 ui_teardown(s);