tui.c (31244B)
1#define NCURSES_WIDECHAR 1 2#define _GNU_SOURCE 3 4#include "tui.h" 5 6#include "cmd.h" 7#include "data.h" 8#include "history.h" 9#include "pane.h" 10#include "player.h" 11#include "list.h" 12#include "listnav.h" 13#include "log.h" 14#include "style.h" 15#include "strbuf.h" 16#include "util.h" 17 18#include "grapheme.h" 19#include <ncurses.h> 20 21#include <unistd.h> 22#include <errno.h> 23#include <ctype.h> 24#include <string.h> 25 26#undef KEY_ENTER 27#define KEY_ENTER '\n' 28#define KEY_SPACE ' ' 29#define KEY_ESC '\x1b' 30#define KEY_TAB '\t' 31#define KEY_CTRL(c) ((c) & ~0x60) 32 33enum { 34 IMODE_EXECUTE, 35 IMODE_TRACK_PLAY, 36 IMODE_TRACK_VIS_SELECT, 37 IMODE_TRACK_SELECT, 38 IMODE_TAG_SELECT, 39 IMODE_COUNT 40}; 41 42typedef char *(*completion_gen)(const char *text, int fwd, int state); 43 44static void pane_title(struct pane *pane, bool highlight, const char *fmtstr, ...); 45static bool confirm_popup(const char *prompt); 46 47static char *command_name_gen(const char *text, int fwd, int state); 48static char *track_vis_name_gen(const char *text, int fwd, int state); 49static char *track_name_gen(const char *text, int fwd, int state); 50static char *tag_name_gen(const char *text, int fwd, int state); 51 52static bool rename_current_tag(void); 53static bool toggle_current_tag(void); 54static void select_only_current_tag(void); 55static void seek_next_selected_tag(void); 56static void delete_current_tag(void); 57static bool tag_name_cmp(const void *p1, const void *p2, void *user); 58static void sort_visible_tags(void); 59static void select_all_tags(void); 60static bool tag_pane_input(wint_t c); 61static void tag_pane_vis(struct pane *pane, int sel); 62 63static bool play_current_track(void); 64static bool nav_to_track_tag(struct track *target); 65static bool nav_to_track(struct track *target); 66static bool rename_current_track(void); 67static bool delete_current_track(void); 68static void queue_current_track(void); 69static void unqueue_last_track(void); 70static bool track_vis_name_cmp(const void *p1, const void *p2, void *user); 71static void sort_visible_tracks(void); 72static bool track_pane_input(wint_t c); 73static void track_pane_vis(struct pane *pane, int sel); 74 75static void select_cmd_pane(int mode); 76static bool run_cmd(const char *name); 77static bool play_track(const char *name); 78static bool nav_to_track_by_name(const char *name); 79static bool nav_to_track_vis_by_name(const char *name); 80static bool seek_tag(const char *name); 81static bool cmd_pane_input(wint_t c); 82static void cmd_pane_vis(struct pane *pane, int sel); 83 84static void update_tracks_vis(void); 85static void reindex_selected_tags(void); 86static void main_input(wint_t c); 87static void main_vis(void); 88 89static void tui_curses_init(void); 90static void tui_resize(void); 91 92static const char imode_prefix[IMODE_COUNT] = { 93 [IMODE_EXECUTE] = ':', 94 [IMODE_TRACK_PLAY] = '!', 95 [IMODE_TRACK_SELECT] = '/', 96 [IMODE_TRACK_VIS_SELECT] = '~', 97 [IMODE_TAG_SELECT] = '?', 98}; 99 100static const char player_state_chars[] = { 101 [PLAYER_STATE_PAUSED] = '|', 102 [PLAYER_STATE_PLAYING] = '>', 103 [PLAYER_STATE_STOPPED] = '#' 104}; 105 106static int scrw, scrh; 107static int quit; 108 109static struct pane pane_left, pane_right, pane_bot; 110static struct pane *const panes[] = { 111 &pane_left, 112 &pane_right, 113 &pane_bot 114}; 115 116static struct history command_history; 117static struct history track_play_history; 118static struct history track_select_history; 119static struct history track_vis_select_history; 120static struct history tag_select_history; 121static struct history *history; 122 123static int cmd_input_mode; 124static int cmd_show; 125 126static struct inputln completion_query; 127static int completion_reset; 128static completion_gen completion; 129 130struct pane *cmd_pane, *tag_pane, *track_pane; 131struct pane *pane_sel, *pane_after_cmd; 132 133struct list *tracks_vis; 134int track_show_playlist; 135struct listnav tag_nav; 136struct listnav track_nav; 137 138char *user_status; 139int user_status_uptime; 140 141void 142pane_title(struct pane *pane, bool highlight, const char *fmtstr, ...) 143{ 144 va_list ap; 145 146 style_on(pane->win, STYLE_TITLE); 147 if (highlight) ATTR_ON(pane->win, A_STANDOUT); 148 149 pane_clearln(pane, 0); 150 wmove(pane->win, 0, 0); 151 va_start(ap, fmtstr); 152 vw_printw(pane->win, fmtstr, ap); 153 va_end(ap); 154 155 if (highlight) ATTR_OFF(pane->win, A_STANDOUT); 156 style_off(pane->win, STYLE_TITLE); 157} 158 159bool 160confirm_popup(const char *prompt) 161{ 162 WINDOW *win; 163 int maxx, maxy; 164 int sx, sy, w, h; 165 int tx, ty; 166 unsigned int c; 167 168 getmaxyx(stdscr, maxy, maxx); 169 170 w = 30; 171 h = 5; 172 sx = (maxx - w) / 2; 173 sy = (maxy - h) / 2; 174 win = newwin(h, w, sy, sx); 175 176 tx = (w - strlen(prompt)) / 2; 177 ty = h / 2; 178 wborder(win, 0, 0, 0, 0, 0, 0, 0, 0); 179 mvwprintw(win, ty, tx, "%s", prompt); 180 wrefresh(win); 181 182 while ((c = wgetch(win)) && c >= 128); 183 184 delwin(win); 185 186 return c == 'y'; 187} 188 189char * 190command_name_gen(const char *text, int fwd, int reset) 191{ 192 static int index, len; 193 char *dup; 194 int dir; 195 196 dir = fwd ? 1 : -1; 197 198 if (reset) { 199 index = 0; 200 len = strlen(text); 201 } else if (index >= -1 && index <= command_count) { 202 index += dir; 203 } 204 205 while (index >= 0 && index < command_count) { 206 if (!strncmp(commands[index].name, text, len)) { 207 dup = astrdup(commands[index].name); 208 return dup; 209 } 210 index += dir; 211 } 212 213 return NULL; 214} 215 216char * 217track_vis_name_gen(const char *text, int fwd, int reset) 218{ 219 static struct list_link *cur; 220 struct list_link *link; 221 struct track *track; 222 const char *prevname; 223 char *dup; 224 225 if (reset) { 226 prevname = NULL; 227 cur = tracks_vis->head.next; 228 link = cur; 229 } else { 230 link = fwd ? cur->next : cur->prev; 231 prevname = NULL; 232 if (LIST_INNER(cur)) { 233 track = tracks_vis_track(cur); 234 prevname = track->name; 235 } 236 } 237 while (LIST_INNER(link)) { 238 track = tracks_vis_track(link); 239 if (prevname && !strcmp(prevname, track->name)) 240 goto next; 241 242 if (strcasestr(track->name, text)) { 243 cur = link; 244 dup = astrdup(track->name); 245 return dup; 246 } 247 248next: 249 prevname = track->name; 250 link = fwd ? link->next : link->prev; 251 } 252 253 return NULL; 254 255} 256 257char * 258track_name_gen(const char *text, int fwd, int reset) 259{ 260 static struct list_link *cur; 261 struct list_link *link; 262 struct track *track; 263 const char *prevname; 264 char *dup; 265 266 if (reset) { 267 prevname = NULL; 268 cur = tracks.head.next; 269 link = cur; 270 } else { 271 link = fwd ? cur->next : cur->prev; 272 prevname = NULL; 273 if (LIST_INNER(cur)) { 274 track = LIST_UPCAST(cur, struct track, link); 275 prevname = track->name; 276 } 277 } 278 279 while (LIST_INNER(link)) { 280 track = LIST_UPCAST(link, struct track, link); 281 if (prevname && !strcmp(prevname, track->name)) 282 goto next; 283 284 if (strcasestr(track->name, text)) { 285 cur = link; 286 dup = aprintf("%s/%s", track->tag->name, track->name); 287 return dup; 288 } 289 290next: 291 prevname = track->name; 292 link = fwd ? link->next : link->prev; 293 } 294 295 return NULL; 296} 297 298char * 299tag_name_gen(const char *text, int fwd, int reset) 300{ 301 static struct list_link *cur; 302 struct list_link *link; 303 struct tag *tag; 304 char *dup; 305 306 if (reset) { 307 cur = tags.head.next; 308 link = cur; 309 } else { 310 link = fwd ? cur->next : cur->prev; 311 } 312 313 while (LIST_INNER(link)) { 314 tag = LIST_UPCAST(link, struct tag, link); 315 if (strcasestr(tag->name, text)) { 316 cur = link; 317 dup = astrdup(tag->name); 318 return dup; 319 } 320 link = fwd ? link->next : link->prev; 321 } 322 323 return NULL; 324} 325 326bool 327rename_current_tag(void) 328{ 329 struct list_link *link; 330 struct tag *tag; 331 char *cmd; 332 333 link = list_at(&tags, tag_nav.sel); 334 if (!link) return false; 335 tag = LIST_UPCAST(link, struct tag, link); 336 337 cmd = aprintf("rename %s", tag->name); 338 select_cmd_pane(IMODE_EXECUTE); 339 inputln_replace(history->input, cmd); 340 free(cmd); 341 342 return true; 343} 344 345bool 346toggle_current_tag(void) 347{ 348 struct list_link *link; 349 struct tag *tag; 350 351 if (list_empty(&tags)) return false; 352 353 link = list_at(&tags, tag_nav.sel); 354 if (!link) return false; 355 tag = LIST_UPCAST(link, struct tag, link); 356 357 /* toggle tag in tags_sel */ 358 if (list_link_inuse(&tag->link_sel)) { 359 list_link_pop(&tag->link_sel); 360 } else { 361 list_insert_back(&tags_sel, &tag->link_sel); 362 } 363 364 playlist_outdated = true; 365 366 return true; 367} 368 369void 370select_only_current_tag(void) 371{ 372 list_clear(&tags_sel); 373 toggle_current_tag(); 374} 375 376void 377seek_next_selected_tag(void) 378{ 379 struct list_link *link; 380 struct tag *tag; 381 int index; 382 383 if (list_empty(&tags_sel)) 384 return; 385 386 if (list_empty(&tags)) 387 return; 388 389 link = list_at(&tags, tag_nav.sel); 390 if (!link) return; 391 392 index = tag_nav.sel; 393 tag = LIST_UPCAST(link, struct tag, link); 394 do { 395 index += 1; 396 link = tag->link.next; 397 if (!LIST_INNER(link)) { 398 link = list_at(&tags, 0); 399 index = 0; 400 } 401 tag = LIST_UPCAST(link, struct tag, link); 402 } while (!list_link_inuse(&tag->link_sel)); 403 404 listnav_update_sel(&tag_nav, index); 405} 406 407void 408delete_current_tag(void) 409{ 410 struct list_link *link; 411 struct tag *tag; 412 413 if (!confirm_popup("Delete tag?")) 414 return; 415 416 link = list_at(&tags, tag_nav.sel); 417 if (!link) return; 418 tag = LIST_UPCAST(link, struct tag, link); 419 if (list_link_inuse(&tag->link_sel)) 420 playlist_outdated = true; 421 tag_rm(tag, true); 422} 423 424bool 425tag_name_cmp(const void *p1, const void *p2, void *user) 426{ 427 const struct tag *t1 = p1, *t2 = p2; 428 429 return strcasecmp(t1->name, t2->name) <= 0; 430} 431 432void 433sort_visible_tags(void) 434{ 435 list_insertion_sort(&tags, false, tag_name_cmp, 436 LIST_OFFSET(struct tag, link), NULL); 437} 438 439void 440select_all_tags(void) 441{ 442 struct list_link *link; 443 struct tag *tag; 444 445 list_clear(&tags_sel); 446 for (LIST_ITER(&tags, link)) { 447 tag = LIST_UPCAST(link, struct tag, link); 448 list_insert_back(&tags_sel, &tag->link_sel); 449 } 450 playlist_outdated = true; 451} 452 453bool 454tag_pane_input(wint_t c) 455{ 456 switch (c) { 457 case KEY_UP: /* nav up */ 458 listnav_update_sel(&tag_nav, tag_nav.sel - 1); 459 break; 460 case KEY_DOWN: /* nav down */ 461 listnav_update_sel(&tag_nav, tag_nav.sel + 1); 462 break; 463 case KEY_SPACE: /* toggle tag */ 464 toggle_current_tag(); 465 break; 466 case KEY_ENTER: /* select only current tag */ 467 select_only_current_tag(); 468 break; 469 case KEY_PPAGE: /* seek half a page up */ 470 listnav_update_sel(&tag_nav, tag_nav.sel - tag_nav.wlen / 2); 471 break; 472 case KEY_NPAGE: /* seek half a page down */ 473 listnav_update_sel(&tag_nav, tag_nav.sel + tag_nav.wlen / 2); 474 break; 475 case L'r': /* rename tag */ 476 rename_current_tag(); 477 break; 478 case L'g': /* seek start of list */ 479 listnav_update_sel(&tag_nav, 0); 480 break; 481 case L'G': /* seek end of list */ 482 listnav_update_sel(&tag_nav, tag_nav.max - 1); 483 break; 484 case L'n': /* nav through selected tags */ 485 seek_next_selected_tag(); 486 break; 487 case L'D': /* delete tag */ 488 delete_current_tag(); 489 break; 490 case KEY_CTRL(L's'): 491 sort_visible_tags(); 492 break; 493 case KEY_CTRL(L'a'): 494 select_all_tags(); 495 break; 496 default: 497 return false; 498 } 499 500 return true; 501} 502 503void 504tag_pane_vis(struct pane *pane, int sel) 505{ 506 struct tag *tag; 507 struct list_link *link; 508 int index, tagsel; 509 510 werase(pane->win); 511 pane_title(pane, sel, "Tags"); 512 513 listnav_update_bounds(&tag_nav, 0, list_len(&tags)); 514 listnav_update_wlen(&tag_nav, pane->h - 1); 515 516 index = -1; 517 for (LIST_ITER(&tags, link)) { 518 tag = LIST_UPCAST(link, struct tag, link); 519 tagsel = list_link_inuse(&tag->link_sel); 520 521 index += 1; 522 if (index < tag_nav.wmin) continue; 523 if (index >= tag_nav.wmax) break; 524 525 if (sel && tagsel && index == tag_nav.sel) 526 style_on(pane->win, STYLE_ITEM_HOVER_SEL); 527 else if (sel && index == tag_nav.sel) 528 style_on(pane->win, STYLE_ITEM_HOVER); 529 else if (tagsel) 530 style_on(pane->win, STYLE_ITEM_SEL); 531 else if (index == tag_nav.sel) 532 style_on(pane->win, STYLE_PREV); 533 534 pane_writeln(pane, 1 + index - tag_nav.wmin, tag->name); 535 536 if (sel && tagsel && index == tag_nav.sel) 537 style_off(pane->win, STYLE_ITEM_HOVER_SEL); 538 else if (sel && index == tag_nav.sel) 539 style_off(pane->win, STYLE_ITEM_HOVER); 540 else if (tagsel) 541 style_off(pane->win, STYLE_ITEM_SEL); 542 else if (index == tag_nav.sel) 543 style_off(pane->win, STYLE_PREV); 544 } 545} 546 547bool 548play_current_track(void) 549{ 550 struct list_link *link; 551 struct track *track; 552 553 link = list_at(tracks_vis, track_nav.sel); 554 if (!link) return false; 555 track = tracks_vis_track(link); 556 player_play_track(track, true); 557 558 return true; 559} 560 561bool 562nav_to_track_tag(struct track *target) 563{ 564 int index; 565 566 if (!target) return false; 567 568 index = list_index(&tags, &target->tag->link); 569 if (index < 0) return false; 570 571 listnav_update_sel(&tag_nav, index); 572 update_tracks_vis(); 573 574 return true; 575} 576 577bool 578nav_to_track(struct track *target) 579{ 580 struct list_link *link; 581 struct track *track; 582 int index; 583 584 if (!target) return false; 585 586 index = 0; 587 for (LIST_ITER(tracks_vis, link)) { 588 track = tracks_vis_track(link); 589 if (track == target) { 590 listnav_update_sel(&track_nav, index); 591 break; 592 } 593 index += 1; 594 } 595 596 return true; 597} 598 599bool 600rename_current_track(void) 601{ 602 struct list_link *link; 603 struct track *track; 604 char *cmd; 605 606 link = list_at(tracks_vis, track_nav.sel); 607 if (!link) return false; 608 track = tracks_vis_track(link); 609 610 cmd = aprintf("rename %s", track->name); 611 select_cmd_pane(IMODE_EXECUTE); 612 inputln_replace(history->input, cmd); 613 free(cmd); 614 615 return true; 616} 617 618bool 619delete_current_track(void) 620{ 621 struct list_link *link; 622 struct track *track; 623 624 link = list_at(tracks_vis, track_nav.sel); 625 if (!link) return false; 626 627 track = tracks_vis_track(link); 628 629 if (!trash_tag || !strcmp(track->tag->name, "trash")) { 630 if (!track_rm(track, true)) 631 USER_STATUS("Failed to remove track"); 632 } else { 633 if (!track_move(track, trash_tag) && errno != EEXIST) 634 USER_STATUS("Failed to trash track: %s", strerror(errno)); 635 if (errno == EEXIST && !track_rm(track, true)) 636 USER_STATUS("Failed to remove track"); 637 } 638 639 return true; 640} 641 642void 643queue_current_track(void) 644{ 645 struct list_link *link; 646 struct track *track; 647 648 link = list_at(tracks_vis, track_nav.sel); 649 if (!link) return; 650 651 track = tracks_vis_track(link); 652 list_insert_back(&player.queue, &track->link_pq); 653} 654 655void 656unqueue_last_track(void) 657{ 658 struct list_link *link; 659 660 link = list_back(&player.queue); 661 if (!link) return; 662 list_link_pop(link); 663} 664 665bool 666track_vis_name_cmp(const void *p1, const void *p2, void *user) 667{ 668 const struct track *t1 = p1, *t2 = p2; 669 670 return strcasecmp(t1->name, t2->name) <= 0; 671} 672 673void 674sort_visible_tracks(void) 675{ 676 struct list_link *link; 677 struct tag *tag; 678 679 if (tracks_vis == &player.playlist) { 680 list_insertion_sort(tracks_vis, false, track_vis_name_cmp, 681 LIST_OFFSET(struct track, link_pl), NULL); 682 } else { 683 list_insertion_sort(tracks_vis, false, track_vis_name_cmp, 684 LIST_OFFSET(struct track, link_tt), NULL); 685 } 686 687 if (!track_show_playlist) { 688 link = list_at(&tags, tag_nav.sel); 689 if (!link) return; 690 tag = LIST_UPCAST(link, struct tag, link); 691 tag->reordered = true; 692 } 693} 694 695bool 696track_pane_input(wint_t c) 697{ 698 switch (c) { 699 case KEY_UP: /* nav up */ 700 listnav_update_sel(&track_nav, track_nav.sel - 1); 701 break; 702 case KEY_DOWN: /* nav down */ 703 listnav_update_sel(&track_nav, track_nav.sel + 1); 704 break; 705 case KEY_ENTER: /* play track */ 706 play_current_track(); 707 break; 708 case KEY_PPAGE: /* seek half page up */ 709 listnav_update_sel(&track_nav, 710 track_nav.sel - track_nav.wlen / 2); 711 break; 712 case KEY_NPAGE: /* seek half page down */ 713 listnav_update_sel(&track_nav, 714 track_nav.sel + track_nav.wlen / 2); 715 break; 716 case L'r': /* rename track */ 717 rename_current_track(); 718 break; 719 case L'g': /* seek start of list */ 720 listnav_update_sel(&track_nav, 0); 721 break; 722 case L'G': /* seek end of list */ 723 listnav_update_sel(&track_nav, track_nav.max - 1); 724 break; 725 case L'y': /* push queue track */ 726 queue_current_track(); 727 break; 728 case L'Y': /* pop queue track */ 729 unqueue_last_track(); 730 break; 731 case L'n': /* seek playing */ 732 nav_to_track(player.track); 733 break; 734 case L'D': /* delete track */ 735 delete_current_track(); 736 break; 737 case KEY_CTRL(L's'): /* sort track in view */ 738 sort_visible_tracks(); 739 break; 740 default: 741 return false; 742 } 743 744 return true; 745} 746 747void 748track_pane_vis(struct pane *pane, int sel) 749{ 750 struct track *track; 751 struct list_link *link; 752 struct tag *tag; 753 int index; 754 755 werase(pane->win); 756 if (tracks_vis == &player.playlist) { 757 pane_title(pane, sel, "Tracks (playlist)"); 758 } else { 759 link = list_at(&tags, tag_nav.sel); 760 if (!link) { 761 pane_title(pane, sel, "Tracks"); 762 } else { 763 tag = LIST_UPCAST(link, struct tag, link); 764 pane_title(pane, sel, "Tracks (%s)", tag->name); 765 } 766 } 767 768 listnav_update_wlen(&track_nav, pane->h - 1); 769 770 index = -1; 771 for (LIST_ITER(tracks_vis, link)) { 772 track = tracks_vis_track(link); 773 774 index += 1; 775 if (index < track_nav.wmin) continue; 776 if (index >= track_nav.wmax) break; 777 778 if (sel && index == track_nav.sel && track == player.track) 779 style_on(pane->win, STYLE_ITEM_HOVER_SEL); 780 else if (sel && index == track_nav.sel) 781 style_on(pane->win, STYLE_ITEM_HOVER); 782 else if (track == player.track) 783 style_on(pane->win, STYLE_ITEM_SEL); 784 else if (index == track_nav.sel) 785 style_on(pane->win, STYLE_PREV); 786 787 pane_writeln(pane, 1 + index - track_nav.wmin, track->name); 788 789 if (sel && index == track_nav.sel && track == player.track) 790 style_off(pane->win, STYLE_ITEM_HOVER_SEL); 791 else if (sel && index == track_nav.sel) 792 style_off(pane->win, STYLE_ITEM_HOVER); 793 else if (track == player.track) 794 style_off(pane->win, STYLE_ITEM_SEL); 795 else if (index == track_nav.sel) 796 style_off(pane->win, STYLE_PREV); 797 } 798} 799 800void 801select_cmd_pane(int mode) 802{ 803 switch (mode) { 804 case IMODE_EXECUTE: 805 cmd_input_mode = IMODE_EXECUTE; 806 pane_after_cmd = pane_sel; 807 history = &command_history; 808 completion = command_name_gen; 809 break; 810 case IMODE_TAG_SELECT: 811 cmd_input_mode = IMODE_TAG_SELECT; 812 pane_after_cmd = pane_sel; 813 history = &tag_select_history; 814 completion = tag_name_gen; 815 break; 816 case IMODE_TRACK_PLAY: 817 cmd_input_mode = IMODE_TRACK_PLAY; 818 pane_after_cmd = pane_sel; 819 history = &track_play_history; 820 completion = track_name_gen; 821 break; 822 case IMODE_TRACK_SELECT: 823 cmd_input_mode = IMODE_TRACK_SELECT; 824 pane_after_cmd = pane_sel; 825 history = &track_select_history; 826 completion = track_name_gen; 827 break; 828 case IMODE_TRACK_VIS_SELECT: 829 cmd_input_mode = IMODE_TRACK_VIS_SELECT; 830 pane_after_cmd = pane_sel; 831 history = &track_vis_select_history; 832 completion = track_vis_name_gen; 833 break; 834 default: 835 ASSERT(0); 836 } 837 838 pane_sel = cmd_pane; 839 completion_reset = 1; 840} 841 842bool 843run_cmd(const char *query) 844{ 845 bool success, found; 846 847 success = cmd_run(query, &found); 848 if (!success && !user_status) 849 USER_STATUS("FAIL"); 850 else if (success && !user_status) 851 USER_STATUS("OK"); 852 853 return found; 854} 855 856bool 857play_track(const char *query) 858{ 859 struct track *track; 860 struct list_link *link; 861 862 for (LIST_ITER(&tracks, link)) { 863 track = LIST_UPCAST(link, struct track, link); 864 if (!strcmp(track->name, query)) { 865 player_play_track(track, true); 866 return true; 867 } 868 } 869 870 return false; 871} 872 873bool 874nav_to_track_by_name(const char *query) 875{ 876 struct track *track; 877 struct tag *tag; 878 struct list_link *link; 879 const char *qtrack; 880 const char *qtag; 881 882 qtag = query; 883 qtrack = strchr(query, '/'); 884 if (!qtrack) return false; 885 qtrack += 1; 886 887 for (LIST_ITER(&tags, link)) { 888 tag = LIST_UPCAST(link, struct tag, link); 889 if (!strncmp(tag->name, qtag, qtrack - qtag - 1)) 890 break; 891 } 892 if (!LIST_INNER(link)) 893 return false; 894 895 for (LIST_ITER(&tag->tracks, link)) { 896 track = LIST_UPCAST(link, struct track, link_tt); 897 if (!strcmp(track->name, qtrack)) { 898 nav_to_track_tag(track); 899 nav_to_track(track); 900 pane_after_cmd = track_pane; 901 return true; 902 } 903 } 904 905 return false; 906} 907 908bool 909nav_to_track_vis_by_name(const char *query) 910{ 911 struct track *track; 912 struct list_link *link; 913 914 for (LIST_ITER(tracks_vis, link)) { 915 track = tracks_vis_track(link); 916 if (!strcmp(track->name, query)) { 917 nav_to_track(track); 918 pane_after_cmd = track_pane; 919 return true; 920 } 921 } 922 923 return false; 924} 925 926bool 927seek_tag(const char *query) 928{ 929 struct tag *tag; 930 struct list_link *link; 931 int index; 932 933 index = -1; 934 for (LIST_ITER(&tags, link)) { 935 index += 1; 936 tag = LIST_UPCAST(link, struct tag, link); 937 if (!strcmp(tag->name, query)) { 938 listnav_update_sel(&tag_nav, index); 939 pane_after_cmd = tag_pane; 940 return true; 941 } 942 } 943 944 return false; 945} 946 947bool 948cmd_pane_input(wint_t c) 949{ 950 char *res; 951 int match; 952 953 switch (c) { 954 case KEY_ESC: 955 match = strcmp(completion_query.buf, history->input->buf); 956 if (!completion_reset && match) { 957 inputln_copy(history->input, &completion_query); 958 } else if (history->sel == history->input) { 959 inputln_replace(history->input, ""); 960 pane_sel = pane_after_cmd; 961 } else { 962 history->sel = history->input; 963 } 964 break; 965 case KEY_LEFT: 966 inputln_left(history->sel); 967 break; 968 case KEY_RIGHT: 969 inputln_right(history->sel); 970 break; 971 case KEY_CTRL(L'w'): 972 inputln_del(history->sel, history->sel->cur); 973 break; 974 case KEY_UP: 975 history_next(history); 976 break; 977 case KEY_DOWN: 978 history_prev(history); 979 break; 980 case KEY_ENTER: 981 if (!*history->sel->buf) { 982 pane_sel = pane_after_cmd; 983 break; 984 } 985 986 if (cmd_input_mode == IMODE_EXECUTE) { 987 if (!run_cmd(history->sel->buf)) 988 USER_STATUS("No such command"); 989 } else if (cmd_input_mode == IMODE_TRACK_PLAY) { 990 if (!play_track(history->sel->buf)) 991 USER_STATUS("Failed to find track"); 992 } else if (cmd_input_mode == IMODE_TRACK_SELECT) { 993 if (!nav_to_track_by_name(history->sel->buf)) 994 USER_STATUS("Failed to find track"); 995 } else if (cmd_input_mode == IMODE_TRACK_VIS_SELECT) { 996 if (!nav_to_track_vis_by_name(history->sel->buf)) 997 USER_STATUS("Failed to find track in view"); 998 } else if (cmd_input_mode == IMODE_TAG_SELECT) { 999 if (!seek_tag(history->sel->buf)) 1000 USER_STATUS("Failed to find tag"); 1001 } 1002 1003 history_submit(history); 1004 pane_sel = pane_after_cmd; 1005 break; 1006 case KEY_TAB: 1007 case KEY_BTAB: 1008 if (history->sel != history->input) { 1009 inputln_copy(history->input, history->sel); 1010 history->sel = history->input; 1011 } 1012 1013 if (completion_reset) 1014 inputln_copy(&completion_query, history->input); 1015 1016 res = completion(completion_query.buf, 1017 c == KEY_TAB, completion_reset); 1018 if (res) inputln_replace(history->input, res); 1019 free(res); 1020 1021 completion_reset = 0; 1022 break; 1023 case KEY_BACKSPACE: 1024 if (history->sel->cur == 0) { 1025 pane_sel = pane_after_cmd; 1026 break; 1027 } 1028 inputln_del(history->sel, 1); 1029 completion_reset = 1; 1030 break; 1031 default: 1032 /* TODO: wide char input support */ 1033 if (c <= 0) break; 1034 inputln_addch(history->sel, c); 1035 completion_reset = 1; 1036 break; 1037 } 1038 1039 return true; /* grab everything */ 1040} 1041 1042void 1043cmd_pane_vis(struct pane *pane, int sel) 1044{ 1045 static struct strbuf line = { 0 }; 1046 struct inputln *cmd; 1047 struct list_link *link; 1048 int index, offset; 1049 1050 werase(pane->win); 1051 1052 /* track name */ 1053 style_on(pane->win, STYLE_TITLE); 1054 pane_clearln(pane, 0); 1055 if (player.loaded) { 1056 strbuf_clear(&line); 1057 if (player.track) 1058 strbuf_append(&line, " %s", player.track->name); 1059 else if (player.track_name) 1060 strbuf_append(&line, " (*) %s", player.track_name); 1061 else 1062 strbuf_append(&line, " <UNKNOWN>"); 1063 pane_writeln(pane, 0, line.buf); 1064 } 1065 style_off(pane->win, STYLE_TITLE); 1066 1067 if (player.loaded) { 1068 /* status line */ 1069 strbuf_clear(&line); 1070 strbuf_append(&line, "%c ", player_state_chars[player.state]); 1071 strbuf_append(&line, "%s / ", timestr(player.time_pos)); 1072 strbuf_append(&line, "%s", timestr(player.time_end)); 1073 1074 if (player.volume >= 0) 1075 strbuf_append(&line, " - vol: %u%%", player.volume); 1076 1077 if (player.status) 1078 strbuf_append(&line, " | [PLAYER] %s", player.status); 1079 1080 if (list_len(&player.queue) > 0) 1081 strbuf_append(&line, " | [QUEUE] %i tracks", 1082 list_len(&player.queue)); 1083 1084 ATTR_ON(pane->win, A_REVERSE); 1085 pane_writeln(pane, 1, line.buf); 1086 ATTR_OFF(pane->win, A_REVERSE); 1087 } else if (player.status) { 1088 /* player message */ 1089 strbuf_clear(&line); 1090 strbuf_append(&line, "[PLAYER] %s", player.status); 1091 pane_writeln(pane, 1, line.buf); 1092 } 1093 1094 /* status bits on right of status line */ 1095 if (player.loaded) 1096 ATTR_ON(pane->win, A_REVERSE); 1097 1098 mvwaddstr(pane->win, 1, pane->w - 6, "[ ]"); 1099 if (list_empty(&player.history)) 1100 mvwaddstr(pane->win, 1, pane->w - 5, "H"); 1101 if (track_show_playlist) 1102 mvwaddstr(pane->win, 1, pane->w - 4, "P"); 1103 if (player.autoplay) 1104 mvwaddstr(pane->win, 1, pane->w - 3, "A"); 1105 if (player.shuffle) 1106 mvwaddstr(pane->win, 1, pane->w - 2, "S"); 1107 1108 if (player.loaded) 1109 ATTR_OFF(pane->win, A_REVERSE); 1110 1111 if (sel || cmd_show) { 1112 /* cmd and search input */ 1113 strbuf_clear(&line); 1114 1115 free(user_status); 1116 user_status = NULL; 1117 1118 cmd = history->sel; 1119 if (cmd != history->input) { 1120 index = 0; 1121 for (LIST_ITER(&history->list, link)) { 1122 if (LIST_UPCAST(link, struct inputln, link) == cmd) 1123 break; 1124 index += 1; 1125 } 1126 strbuf_append(&line, "[%i] ", link ? index : -1); 1127 } else { 1128 strbuf_append(&line, "%c", imode_prefix[cmd_input_mode]); 1129 } 1130 offset = strlen(line.buf); 1131 1132 strbuf_append(&line, "%s", cmd->buf); 1133 1134 pane_writeln(pane, 2, line.buf); 1135 1136 if (sel) { /* show cursor in text */ 1137 ATTR_ON(pane->win, A_REVERSE); 1138 wmove(pane->win, 2, offset + cmd->curpos); 1139 if (cmd->cur >= cmd->len) { 1140 waddch(pane->win, ' '); 1141 } else { 1142 size_t n = grapheme_next_character_break_utf8( 1143 cmd->buf + cmd->cur, cmd->len - cmd->cur); 1144 waddnstr(pane->win, cmd->buf + cmd->cur, n); 1145 } 1146 ATTR_OFF(pane->win, A_REVERSE); 1147 } 1148 } else if (user_status && user_status_uptime) { 1149 user_status_uptime--; 1150 strbuf_clear(&line); 1151 strbuf_append(&line, " %s", user_status); 1152 pane_writeln(pane, 2, line.buf); 1153 } else { 1154 free(user_status); 1155 user_status = NULL; 1156 } 1157} 1158 1159void 1160update_tracks_vis(void) 1161{ 1162 struct list_link *link; 1163 struct tag *tag; 1164 1165 if (track_show_playlist) { 1166 tracks_vis = &player.playlist; 1167 } else { 1168 link = list_at(&tags, tag_nav.sel); 1169 if (!link) return; 1170 tag = LIST_UPCAST(link, struct tag, link); 1171 tracks_vis = &tag->tracks; 1172 } 1173 1174 listnav_update_bounds(&track_nav, 0, list_len(tracks_vis)); 1175} 1176 1177void 1178reindex_selected_tags(void) 1179{ 1180 struct list_link *link; 1181 struct tag *tag; 1182 struct track *track; 1183 struct tag *playing_tag; 1184 char *playing_name; 1185 1186 playing_tag = NULL; 1187 playing_name = NULL; 1188 1189 if (player.track) { 1190 playing_tag = player.track->tag; 1191 playing_name = astrdup(player.track->name); 1192 } 1193 1194 if (track_show_playlist) { 1195 for (LIST_ITER(&tags_sel, link)) { 1196 tag = LIST_UPCAST(link, struct tag, link_sel); 1197 tag_reindex_tracks(tag); 1198 } 1199 } else { 1200 link = list_at(&tags, tag_nav.sel); 1201 if (!link) return; 1202 tag = LIST_UPCAST(link, struct tag, link); 1203 tag_reindex_tracks(tag); 1204 } 1205 1206 if (playing_tag) { 1207 for (LIST_ITER(&playing_tag->tracks, link)) { 1208 track = LIST_UPCAST(link, struct track, link_tt); 1209 if (!strcmp(track->name, playing_name)) { 1210 player.track = track; 1211 break; 1212 } 1213 } 1214 } 1215} 1216 1217void 1218main_input(wint_t c) 1219{ 1220 switch (c) { 1221 case KEY_TAB: 1222 pane_sel = pane_sel == &pane_left 1223 ? &pane_right : &pane_left; 1224 break; 1225 case KEY_ESC: 1226 if (pane_sel == cmd_pane) 1227 pane_sel = pane_after_cmd; 1228 break; 1229 case KEY_LEFT: 1230 player_seek(player.time_pos - 10); 1231 break; 1232 case KEY_RIGHT: 1233 player_seek(player.time_pos + 10); 1234 break; 1235 case L'o': 1236 list_clear(&player.queue); 1237 break; 1238 case L'h': 1239 list_clear(&player.history); 1240 break; 1241 case L'c': 1242 player_toggle_pause(); 1243 break; 1244 case L'>': 1245 player_next(); 1246 break; 1247 case L'<': 1248 player_prev(); 1249 break; 1250 case L'P': 1251 track_show_playlist ^= 1; 1252 break; 1253 case L'A': 1254 player.autoplay ^= 1; 1255 break; 1256 case L'S': 1257 player.shuffle ^= 1; 1258 break; 1259 case L'b': 1260 player_seek(0); 1261 break; 1262 case L'x': 1263 if (player.state == PLAYER_STATE_PLAYING) 1264 player_stop(); 1265 else 1266 player_play(); 1267 break; 1268 case L'.': 1269 cmd_rerun(); 1270 break; 1271 case L':': 1272 select_cmd_pane(IMODE_EXECUTE); 1273 break; 1274 case L'/': 1275 select_cmd_pane(IMODE_TRACK_SELECT); 1276 break; 1277 case L'~': 1278 select_cmd_pane(IMODE_TRACK_VIS_SELECT); 1279 break; 1280 case L'!': 1281 select_cmd_pane(IMODE_TRACK_PLAY); 1282 break; 1283 case L'?': 1284 select_cmd_pane(IMODE_TAG_SELECT); 1285 break; 1286 case L'+': 1287 player_set_volume(MIN(100, player.volume + 5)); 1288 break; 1289 case L'-': 1290 player_set_volume(MAX(0, player.volume - 5)); 1291 break; 1292 case KEY_CTRL('l'): 1293 clear(); 1294 refresh(); 1295 break; 1296 case KEY_CTRL(L'r'): 1297 reindex_selected_tags(); 1298 break; 1299 case L'q': 1300 quit = 1; 1301 break; 1302 case L'N': 1303 if (!nav_to_track_tag(player.track)) 1304 return; 1305 track_show_playlist = false; 1306 update_tracks_vis(); 1307 nav_to_track(player.track); 1308 pane_sel = track_pane; 1309 break; 1310 } 1311} 1312 1313void 1314main_vis(void) 1315{ 1316 int i; 1317 1318 /* add missing title tile at the top */ 1319 style_on(stdscr, STYLE_TITLE); 1320 move(0, pane_left.ex); 1321 addch(' '); 1322 style_off(stdscr, STYLE_TITLE); 1323 1324 /* draw left-right separator line */ 1325 style_on(stdscr, STYLE_PANE_SEP); 1326 for (i = pane_left.sy + 1; i < pane_left.ey; i++) { 1327 move(i, pane_left.ex); 1328 addch(ACS_VLINE); 1329 } 1330 style_off(stdscr, STYLE_PANE_SEP); 1331} 1332 1333void 1334tui_curses_init(void) 1335{ 1336 initscr(); 1337 1338 /* do most of the handling ourselves, 1339 * enable special keys */ 1340 raw(); 1341 noecho(); 1342 keypad(stdscr, TRUE); 1343 1344 /* update screen occasionally for things like 1345 * time even when no input was received */ 1346 halfdelay(1); 1347 1348 /* inits COLOR and COLOR_PAIRS used by styles */ 1349 start_color(); 1350 1351 /* dont show cursor */ 1352 curs_set(0); 1353 1354 /* we use ESC deselecting the current pane 1355 * and not for escape sequences, so dont wait */ 1356 ESCDELAY = 0; 1357} 1358 1359void 1360tui_resize(void) 1361{ 1362 struct list_link *link; 1363 struct tag *tag; 1364 int leftw; 1365 1366 getmaxyx(stdscr, scrh, scrw); 1367 1368 /* guarantee a minimum terminal size */ 1369 while (scrw < 10 || scrh < 4) { 1370 clear(); 1371 refresh(); 1372 usleep(10000); 1373 getmaxyx(stdscr, scrh, scrw); 1374 } 1375 1376 /* adjust tag pane width to name lengths */ 1377 leftw = 0; 1378 for (LIST_ITER(&tags, link)) { 1379 tag = LIST_UPCAST(link, struct tag, link); 1380 leftw = MAX(leftw, text_width(tag->name, strlen(tag->name))); 1381 } 1382 leftw = MAX(leftw + 1, 0.2f * scrw); 1383 1384 pane_resize(&pane_left, 0, 0, leftw, scrh - 3); 1385 pane_resize(&pane_right, pane_left.ex + 1, 0, scrw, scrh - 3); 1386 pane_resize(&pane_bot, 0, scrh - 3, scrw, scrh); 1387} 1388 1389void 1390tui_init(void) 1391{ 1392 quit = 0; 1393 cmd_input_mode = IMODE_TRACK_SELECT; 1394 1395 user_status = NULL; 1396 user_status_uptime = 0; 1397 1398 inputln_init(&completion_query); 1399 completion_reset = 1; 1400 1401 history_init(&track_play_history); 1402 history_init(&track_select_history); 1403 history_init(&track_vis_select_history); 1404 history_init(&tag_select_history); 1405 history_init(&command_history); 1406 history = &command_history; 1407 1408 tui_curses_init(); 1409 1410 style_init(); 1411 1412 pane_init((tag_pane = &pane_left), tag_pane_input, tag_pane_vis); 1413 pane_init((track_pane = &pane_right), track_pane_input, track_pane_vis); 1414 pane_init((cmd_pane = &pane_bot), cmd_pane_input, cmd_pane_vis); 1415 1416 pane_sel = &pane_left; 1417 pane_after_cmd = pane_sel; 1418 1419 listnav_init(&tag_nav); 1420 listnav_init(&track_nav); 1421 1422 track_show_playlist = 0; 1423 update_tracks_vis(); 1424 1425 tui_resize(); 1426} 1427 1428void 1429tui_deinit(void) 1430{ 1431 free(user_status); 1432 1433 inputln_deinit(&completion_query); 1434 1435 pane_deinit(&pane_left); 1436 pane_deinit(&pane_right); 1437 pane_deinit(&pane_bot); 1438 1439 history_deinit(&track_play_history); 1440 history_deinit(&track_select_history); 1441 history_deinit(&track_vis_select_history); 1442 history_deinit(&tag_select_history); 1443 history_deinit(&command_history); 1444 1445 if (!isendwin()) endwin(); 1446} 1447 1448bool 1449tui_update(void) 1450{ 1451 bool handled; 1452 wint_t c; 1453 int i; 1454 1455 get_wch(&c); 1456 1457 if (c == KEY_RESIZE) { 1458 tui_resize(); 1459 } else if (c != ERR) { 1460 handled = 0; 1461 if (pane_sel && pane_sel->active) 1462 handled = pane_sel->handle(c); 1463 1464 /* fallback if char not handled by pane */ 1465 if (!handled) main_input(c); 1466 } 1467 1468 playlist_update(); 1469 update_tracks_vis(); 1470 1471 refresh(); 1472 for (i = 0; i < ARRLEN(panes); i++) { 1473 /* only update ui for panes that are visible */ 1474 if (!panes[i]->active) continue; 1475 1476 panes[i]->update(panes[i], pane_sel == panes[i]); 1477 wnoutrefresh(panes[i]->win); 1478 } 1479 1480 main_vis(); 1481 doupdate(); 1482 1483 return !quit; 1484}