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
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.
28 #include <sys/ioctl.h>
32 #include <unibilium.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"
44 static volatile sig_atomic_t sigwinch_seen
;
46 /* (unibi_var_t) { 0 } is too long */
49 /* Printf in format suitable for unibi_format() */
51 out_printf(void *ctx
, const char *buf
, size_t len
)
59 if ((wout
= write(1, buf
+ wtot
, len
- wtot
)) < 0) {
61 /* An error that would be a pain in the neck to extract */
69 /* Wrapper for "Just execute the damn command" */
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
,
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
);
85 unibi_format(zero_dyn
, zero_static
, format_string
, param
, out_printf
, 0,
89 /* Write hh:mm:ss / hh:mm:ss */
91 print_timing(struct playlist
volatile *p
)
93 int play
= p
->current_playtime
;
94 int length
= p
->current_length
;
103 printf("═══════ paused ══════");
117 len_h
= length
% 100;
120 printf(" %02d:", cur_h
);
125 printf("%02d:%02d / ", cur_m
, cur_s
);
129 printf("%02d:", len_h
);
134 printf("%02d:%02d ", len_m
, len_s
);
137 /* Print out text, with left indentation. */
139 trickle_out(struct state
volatile *s
, int left_pad
, const char *text
, int row
)
141 mbstate_t mbs
= { 0 };
145 int cur_pos
= left_pad
;
146 int max_width
= s
->term_width
;
158 mbret
= mbrtowc(&wc
, text
, left
, &mbs
);
160 if (mbret
>= (size_t) -2) {
161 mbs
= (mbstate_t) { 0 };
166 print_num
= (int) mbret
;
167 this_cells
= wcwidth(wc
);
170 if (this_cells
+ cur_pos
> max_width
) {
172 do_unibi(s
->ut
, unibi_clr_eol
, Z
, Z
, Z
, Z
, Z
, Z
, Z
, Z
,
177 if (row
>= (int) s
->pos_mid_line
) {
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
;
187 printf("%.*s", print_num
, text
);
191 cur_pos
+= this_cells
;
197 /* Blit the whole damn thing */
199 redraw(struct state
volatile *s
)
203 s
->term_height
< 5) {
207 /* First, the upper pane */
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
-
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
);
232 } else if (s
->uri_or_chat
) {
234 size_t idx
= s
->playlist_hover_idx
;
236 if (idx
< s
->playlist
.entries_len
) {
237 u
= s
->playlist
.entries
[idx
].uri
;
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
-
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
);
259 if (s
->term_width
< 20) {
263 intmax_t k
= s
->chat_log
.msgs_len
- 1;
264 intmax_t pos
= s
->pos_mid_line
- 1;
267 size_t text_width
= 0;
268 struct chat_msg
*m
= 0;
273 pos
> s
->term_height
) {
274 /* We probably wrapped to -1 somehow */
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
,
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
-
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
,
303 do_unibi(s
->ut
, unibi_clr_eol
, Z
, Z
, Z
, Z
, Z
, Z
,
310 do_unibi(s
->ut
, unibi_cursor_address
,
311 unibi_var_from_num(pos
- lines
+ 1), Z
, Z
, Z
,
314 printf("%*s: ", 14, m
->user
);
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
,
324 goto another_chat_msg
;
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
) {
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
;
358 int pos
= s
->pos_mid_line
+ 1 + k
;
359 size_t idx
= s
->playlist_offset
+ k
;
361 if (!s
->dirty_line
[pos
]) {
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
= "";
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 = "━┿━"; */
387 if (s
->playlist_hover_idx
== idx
) {
388 printf("\x1b[48;2;%d;%d;%dm", 34, 54, 69);
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
) {
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
) {
418 } else if (k
== 15 &&
419 s
->term_width
> 35 &&
420 s
->playlist
.current_playing_uid
>= 0) {
421 print_timing(&s
->playlist
);
432 if (!s
->dirty_line
[s
->term_height
- 2] &&
433 !s
->dirty_line
[s
->term_height
- 1]) {
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);
445 printf(BG
" U " BG
": view URIs", 34, 54, 69, 40, 40, 40);
448 for (int r
= 66; r
< s
->term_width
; ++r
) {
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
) {
468 for (int k
= 0; k
< s
->term_height
; ++k
) {
469 s
->dirty_line
[k
] = 0;
473 /* Move the hovered line up and down */
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
-
479 size_t new_hover_idx
= (size_t) xmax(0, xmin(
481 playlist_hover_idx
+ delta
,
482 (intmax_t) s
->playlist
.
484 size_t new_hover_pos
= s
->pos_mid_line
+ 1 + new_hover_idx
-
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
) {
503 s
->playlist_hover_idx
= new_hover_idx
;
505 while (highest_cutoff
> new_hover_pos
&&
506 s
->playlist_offset
> 0) {
507 s
->playlist_offset
--;
509 s
->dirty_playlist
= 1;
512 while (new_hover_pos
+ 4 > s
->pos_hints_top
) {
513 s
->playlist_offset
++;
515 s
->dirty_playlist
= 1;
519 /* All the input things we can do */
521 handle_keypress(struct state
volatile *s
, TermKeyKey
*k
)
524 case TERMKEY_TYPE_KEYSYM
:
526 switch (k
->code
.sym
) {
528 move_playlist_hover(s
, -1);
530 case TERMKEY_SYM_DOWN
:
531 move_playlist_hover(s
, 1);
538 case TERMKEY_TYPE_UNICODE
:
540 switch (k
->code
.codepoint
) {
545 s
->dirty_playlist
= 1;
551 s
->dirty_playlist
= 1;
555 move_playlist_hover(s
, 1);
559 move_playlist_hover(s
, -1);
564 s
->dirty_playlist
= 1;
566 for (int q
= 0; q
< s
->term_height
; ++q
) {
567 s
->dirty_line
[q
] = 1;
578 if (s
->playlist_hover_idx
< s
->playlist
.entries_len
) {
579 struct playlist_entry
*e
=
580 &s
->playlist
.entries
[s
->
584 launch_downloader(e
->uri
);
592 if (s
->playlist_hover_idx
< s
->playlist
.entries_len
) {
593 struct playlist_entry
*e
=
594 &s
->playlist
.entries
[s
->
598 launch_player(e
->uri
);
613 /* Make sure we don't waste too much space on empty playlists */
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;
644 /* Terminal has changed under us */
646 handle_resize(struct state
volatile *s
)
648 struct winsize w
= { 0 };
652 ioctl(1, TIOCGWINSZ
, &w
);
659 if (!(newmem
= malloc(n_rows
* sizeof(*s
->dirty_line
)))) {
660 PERROR_MESSAGE("malloc");
666 s
->term_width
= w
.ws_col
;
667 s
->term_height
= w
.ws_row
;
669 s
->dirty_line
= newmem
;
670 memset(s
->dirty_line
, 1, s
->term_height
* sizeof(*s
->dirty_line
));
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);
682 sighandle_winch(int sig
)
688 /* Clean out our part of s, prepare to surrender terminal. */
690 ui_teardown(struct state
volatile *s
)
696 termkey_destroy(s
->tk
);
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
);
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. ]");
722 /* Resize handling */
723 signal(SIGWINCH
, sighandle_winch
);
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
);
733 s
->tk
= termkey_new(fileno(stdin
), 0);
734 termkey_set_waittime(s
->tk
, 100);
740 ui_loop(struct state
volatile *s
)
745 int new_playtime
= 0;
746 struct pollfd pfd
= { .fd
= fileno(stdin
), .events
= POLLIN
};
750 while (!s
->please_die
) {
756 if (!s
->playlist
.paused
) {
757 new_playtime
= s
->playlist
.zero_playtime
+ (time(0) -
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
;
776 if (poll(&pfd
, 1, 100) == 0) {
777 ret
= termkey_getkey_force(s
->tk
, &key
);
781 if (pfd
.revents
& (POLLIN
| POLLHUP
| POLLERR
)) {
782 termkey_advisereadable(s
->tk
);
785 ret
= termkey_getkey(s
->tk
, &key
);
789 case TERMKEY_RES_NONE
:
790 case TERMKEY_RES_AGAIN
:
792 case TERMKEY_RES_ERROR
:
796 PERROR_MESSAGE("termkey_waitkey");
799 case TERMKEY_RES_EOF
:
801 case TERMKEY_RES_KEY
:
802 handle_keypress(s
, &key
);