tmus

TUI Music Player
git clone https://git.sinitax.com/sinitax/tmus
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

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}