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
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() */
50 static void out_printf(void *ctx
, const char *buf
, size_t len
)
58 if ((wout
= write(1, buf
+ wtot
, len
- wtot
)) < 0) {
60 /* An error that would be a pain in the neck to extract */
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
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
);
83 unibi_format(zero_dyn
, zero_static
, format_string
, param
, out_printf
, 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
;
100 printf("═══════ paused ══════");
114 len_h
= length
% 100;
117 printf(" %02d:", cur_h
);
122 printf("%02d:%02d / ", cur_m
, cur_s
);
126 printf("%02d:", len_h
);
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
138 mbstate_t mbs
= { 0 };
142 int cur_pos
= left_pad
;
143 int max_width
= s
->term_width
;
155 mbret
= mbrtowc(&wc
, text
, left
, &mbs
);
157 if (mbret
>= (size_t) -2) {
158 mbs
= (mbstate_t) { 0 };
163 print_num
= (int) mbret
;
164 this_cells
= wcwidth(wc
);
167 if (this_cells
+ cur_pos
> max_width
) {
169 do_unibi(s
->ut
, unibi_clr_eol
, Z
, Z
, Z
, Z
, Z
, Z
, Z
, Z
,
174 if (row
>= (int) s
->pos_mid_line
) {
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
;
184 printf("%.*s", print_num
, text
);
188 cur_pos
+= this_cells
;
194 /* Blit the whole damn thing */
195 static void redraw(struct state
volatile *s
)
199 s
->term_height
< 5) {
203 /* First, the upper pane */
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
-
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
);
228 } else if (s
->uri_or_chat
) {
230 size_t idx
= s
->playlist_hover_idx
;
232 if (idx
< s
->playlist
.entries_len
) {
233 u
= s
->playlist
.entries
[idx
].uri
;
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
-
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
);
255 if (s
->term_width
< 20) {
259 intmax_t k
= s
->chat_log
.msgs_len
- 1;
260 intmax_t pos
= s
->pos_mid_line
- 1;
263 size_t text_width
= 0;
264 struct chat_msg
*m
= 0;
268 if (pos
< 0 || pos
> s
->term_height
) {
269 /* We probably wrapped to -1 somehow */
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
,
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
-
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
,
298 do_unibi(s
->ut
, unibi_clr_eol
, Z
, Z
, Z
, Z
, Z
, Z
,
305 do_unibi(s
->ut
, unibi_cursor_address
,
306 unibi_var_from_num(pos
- lines
+ 1), Z
, Z
, Z
,
309 printf("%*s: ", 14, m
->user
);
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
,
320 goto another_chat_msg
;
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
) {
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
;
354 int pos
= s
->pos_mid_line
+ 1 + k
;
355 size_t idx
= s
->playlist_offset
+ k
;
357 if (!s
->dirty_line
[pos
]) {
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
= "";
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 = "━┿━"; */
383 if (s
->playlist_hover_idx
== idx
) {
384 printf("\x1b[48;2;%d;%d;%dm", 34, 54, 69);
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
) {
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
) {
414 } else if (k
== 15 &&
415 s
->term_width
> 35 &&
416 s
->playlist
.current_playing_uid
>= 0) {
417 print_timing(&s
->playlist
);
428 if (!s
->dirty_line
[s
->term_height
- 2] &&
429 !s
->dirty_line
[s
->term_height
- 1]) {
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
) {
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
) {
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
-
469 size_t new_hover_idx
= (size_t) xmax(0, xmin(
471 playlist_hover_idx
+ delta
,
472 (intmax_t) s
->playlist
.
474 size_t new_hover_pos
= s
->pos_mid_line
+ 1 + new_hover_idx
-
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
) {
485 s
->playlist_hover_idx
= new_hover_idx
;
487 while (highest_cutoff
> new_hover_pos
&&
488 s
->playlist_offset
> 0) {
489 s
->playlist_offset
--;
491 s
->dirty_playlist
= 1;
494 while (new_hover_pos
+ 4 > s
->pos_hints_top
) {
495 s
->playlist_offset
++;
497 s
->dirty_playlist
= 1;
501 /* All the input things we can do */
502 static void handle_keypress(struct state
volatile *s
, TermKeyKey
*k
)
505 case TERMKEY_TYPE_KEYSYM
:
507 switch (k
->code
.sym
) {
509 move_playlist_hover(s
, -1);
511 case TERMKEY_SYM_DOWN
:
512 move_playlist_hover(s
, 1);
519 case TERMKEY_TYPE_UNICODE
:
521 switch (k
->code
.codepoint
) {
534 move_playlist_hover(s
, 1);
538 move_playlist_hover(s
, -1);
543 s
->dirty_playlist
= 1;
545 for (int q
= 0; q
< s
->term_height
; ++q
) {
546 s
->dirty_line
[q
] = 1;
557 if (s
->playlist_hover_idx
< s
->playlist
.entries_len
) {
558 struct playlist_entry
*e
=
559 &s
->playlist
.entries
[s
->
563 launch_only_once(e
->uri
);
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;
608 /* Terminal has changed under us */
609 static void handle_resize(struct state
volatile *s
)
611 struct winsize w
= { 0 };
614 ioctl(1, TIOCGWINSZ
, &w
);
616 if (!(newmem
= malloc(w
.ws_row
* sizeof(*s
->dirty_line
)))) {
617 PERROR_MESSAGE("malloc");
623 s
->term_width
= w
.ws_col
;
624 s
->term_height
= w
.ws_row
;
626 s
->dirty_line
= newmem
;
627 memset(s
->dirty_line
, 1, s
->term_height
* sizeof(*s
->dirty_line
));
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);
638 static void sighandle_winch(int sig
)
644 /* Clean out our part of s, prepare to surrender terminal. */
645 static void ui_teardown(struct state
volatile *s
)
651 termkey_destroy(s
->tk
);
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
);
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. ]");
676 /* Resize handling */
677 signal(SIGWINCH
, sighandle_winch
);
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
);
687 s
->tk
= termkey_new(fileno(stdin
), 0);
688 termkey_set_waittime(s
->tk
, 100);
693 void ui_loop(struct state
volatile *s
)
698 int new_playtime
= 0;
699 struct pollfd pfd
= { .fd
= fileno(stdin
), .events
= POLLIN
};
703 while (!s
->please_die
) {
709 if (!s
->playlist
.paused
) {
710 new_playtime
= s
->playlist
.zero_playtime
+ (time(0) -
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
;
729 if (poll(&pfd
, 1, 100) == 0) {
730 ret
= termkey_getkey_force(s
->tk
, &key
);
734 if (pfd
.revents
& (POLLIN
| POLLHUP
| POLLERR
)) {
735 termkey_advisereadable(s
->tk
);
738 ret
= termkey_getkey(s
->tk
, &key
);
742 case TERMKEY_RES_NONE
:
743 case TERMKEY_RES_AGAIN
:
745 case TERMKEY_RES_ERROR
:
749 PERROR_MESSAGE("termkey_waitkey");
752 case TERMKEY_RES_EOF
:
754 case TERMKEY_RES_KEY
:
755 handle_keypress(s
, &key
);