summaryrefslogtreecommitdiffstats
path: root/src/tui.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui.c')
-rw-r--r--src/tui.c937
1 files changed, 937 insertions, 0 deletions
diff --git a/src/tui.c b/src/tui.c
new file mode 100644
index 0000000..69e933f
--- /dev/null
+++ b/src/tui.c
@@ -0,0 +1,937 @@
+#define NCURSES_WIDECHAR 1
+
+#include "tui.h"
+
+#include "cmd.h"
+#include "data.h"
+#include "history.h"
+#include "pane.h"
+#include "player.h"
+#include "list.h"
+#include "listnav.h"
+#include "ref.h"
+#include "style.h"
+#include "util.h"
+
+#include <ncurses.h>
+
+#include <unistd.h>
+
+#include <wchar.h>
+#include <wctype.h>
+
+#undef KEY_ENTER
+#define KEY_ENTER '\n'
+#define KEY_SPACE ' '
+#define KEY_ESC '\x1b'
+#define KEY_TAB '\t'
+#define KEY_CTRL(c) ((c) & ~0x60)
+
+enum {
+ IMODE_EXECUTE,
+ IMODE_TRACK_SEARCH,
+ IMODE_TAG_SEARCH,
+ IMODE_COUNT
+};
+
+typedef wchar_t *(*completion_gen)(const wchar_t *text, int fwd, int state);
+
+static void pane_title(struct pane *pane, const char *title, int highlight);
+
+static wchar_t *command_name_gen(const wchar_t *text, int fwd, int state);
+static wchar_t *track_name_gen(const wchar_t *text, int fwd, int state);
+static wchar_t *tag_name_gen(const wchar_t *text, int fwd, int state);
+
+static void toggle_current_tag(void);
+static int tag_pane_input(wint_t c);
+static void tag_pane_vis(struct pane *pane, int sel);
+
+static int track_pane_input(wint_t c);
+static void track_pane_vis(struct pane *pane, int sel);
+
+static int play_track(const wchar_t *name);
+static int select_tag(const wchar_t *name);
+
+static int cmd_pane_input(wint_t c);
+static void cmd_pane_vis(struct pane *pane, int sel);
+
+static void queue_hover(void);
+static void update_track_playlist(void);
+static void main_input(wint_t c);
+static void main_vis(void);
+
+static void tui_curses_init(void);
+static void tui_resize(void);
+
+static int scrw, scrh;
+static int quit;
+
+static struct pane pane_left, pane_right, pane_bot;
+static struct pane *const panes[] = {
+ &pane_left,
+ &pane_right,
+ &pane_bot
+};
+
+static struct history command_history;
+static struct history track_search_history;
+static struct history tag_search_history;
+static struct history *history;
+
+static int cmd_input_mode;
+static int cmd_show;
+
+static struct inputln completion_query;
+static int completion_reset;
+static completion_gen completion;
+
+struct pane *cmd_pane, *tag_pane, *track_pane;
+struct pane *pane_sel, *pane_top_sel;
+
+struct list *tracks_vis;
+int track_show_playlist;
+struct listnav tag_nav;
+struct listnav track_nav;
+
+const char imode_prefix[IMODE_COUNT] = {
+ [IMODE_EXECUTE] = ':',
+ [IMODE_TRACK_SEARCH] = '/',
+ [IMODE_TAG_SEARCH] = '?',
+};
+
+static const char player_state_chars[] = {
+ [PLAYER_STATE_PAUSED] = '|',
+ [PLAYER_STATE_PLAYING] = '>',
+ [PLAYER_STATE_STOPPED] = '#'
+};
+
+void
+pane_title(struct pane *pane, const char *title, int highlight)
+{
+ wmove(pane->win, 0, 0);
+
+ style_on(pane->win, STYLE_TITLE);
+ if (highlight) ATTR_ON(pane->win, A_STANDOUT);
+
+ wprintw(pane->win, " %-*.*s", pane->w - 1, pane->w - 1, title);
+
+ if (highlight) ATTR_OFF(pane->win, A_STANDOUT);
+ style_off(pane->win, STYLE_TITLE);
+}
+
+wchar_t *
+command_name_gen(const wchar_t *text, int fwd, int reset)
+{
+ static int index, len;
+ int dir;
+
+ dir = fwd ? 1 : -1;
+
+ if (reset) {
+ index = 0;
+ len = wcslen(text);
+ } else if (index >= -1 && index <= command_count) {
+ index += dir;
+ }
+
+ while (index >= 0 && index < command_count) {
+ if (!wcsncmp(commands[index].name, text, len))
+ return wcsdup(commands[index].name);
+ index += dir;
+ }
+
+ return NULL;
+}
+
+wchar_t *
+track_name_gen(const wchar_t *text, int fwd, int reset)
+{
+ static struct link *cur;
+ struct link *iter;
+ struct track *track;
+
+ if (reset) {
+ cur = tracks.head.next;
+ iter = cur;
+ } else {
+ iter = fwd ? cur->next : cur->prev;
+ }
+
+ while (iter && LIST_INNER(&tracks, iter)) {
+ track = UPCAST(iter, struct ref)->data;
+ if (wcsstr(track->name, text)) {
+ cur = iter;
+ return wcsdup(track->name);
+ }
+ iter = fwd ? iter->next : iter->prev;
+ }
+
+ return NULL;
+}
+
+wchar_t *
+tag_name_gen(const wchar_t *text, int fwd, int reset)
+{
+ static struct link *cur;
+ struct link *iter;
+ struct tag *tag;
+
+ if (reset) {
+ cur = tags.head.next;
+ iter = cur;
+ } else {
+ iter = fwd ? cur->next : cur->prev;
+ }
+
+ while (iter && LIST_INNER(&tags, iter)) {
+ tag = UPCAST(iter, struct tag);
+ if (wcsstr(tag->name, text)) {
+ cur = iter;
+ return wcsdup(tag->name);
+ }
+ iter = fwd ? iter->next : iter->prev;
+ }
+
+ return NULL;
+}
+
+void
+toggle_current_tag(void)
+{
+ struct link *link, *iter;
+ struct track *track;
+ struct tag *tag;
+ struct ref *ref;
+ int in_tags, in_playlist;
+
+ if (list_empty(&tags)) return;
+
+ link = list_at(&tags, tag_nav.sel);
+ ASSERT(link != NULL);
+ tag = UPCAST(link, struct tag);
+
+ /* toggle tag in tags_sel */
+ if (refs_incl(&tags_sel, tag)) {
+ refs_rm(&tags_sel, tag);
+ } else {
+ ref = ref_init(tag);
+ list_push_back(&tags_sel, LINK(ref));
+ }
+
+ /* rebuild the full playlist */
+ refs_free(&player->playlist);
+ for (LIST_ITER(&tags_sel, link)) {
+ tag = UPCAST(link, struct ref)->data;
+ for (LIST_ITER(&tag->tracks, iter)) {
+ ref = ref_init(UPCAST(iter, struct ref)->data);
+ ASSERT(ref != NULL);
+ list_push_back(&player->playlist, LINK(ref));
+ }
+ }
+}
+
+int
+tag_pane_input(wint_t c)
+{
+ switch (c) {
+ case KEY_UP:
+ listnav_update_sel(&tag_nav, tag_nav.sel - 1);
+ return 1;
+ case KEY_DOWN:
+ listnav_update_sel(&tag_nav, tag_nav.sel + 1);
+ return 1;
+ case KEY_SPACE:
+ toggle_current_tag();
+ return 1;
+ case KEY_ENTER:
+ refs_free(&tags_sel);
+ toggle_current_tag();
+ return 1;
+ case KEY_PPAGE:
+ listnav_update_sel(&tag_nav, tag_nav.sel - tag_nav.wlen / 2);
+ return 1;
+ case KEY_NPAGE:
+ listnav_update_sel(&tag_nav, tag_nav.sel + tag_nav.wlen / 2);
+ return 1;
+ }
+
+ return 0;
+}
+
+void
+tag_pane_vis(struct pane *pane, int sel)
+{
+ struct tag *tag;
+ struct link *iter;
+ int index, tagsel;
+
+ werase(pane->win);
+ pane_title(pane, "Tags", sel);
+
+ listnav_update_bounds(&tag_nav, 0, list_len(&tags));
+ listnav_update_wlen(&tag_nav, pane->h - 1);
+
+ index = -1;
+ for (LIST_ITER(&tags, iter)) {
+ tag = UPCAST(iter, struct tag);
+ tagsel = refs_incl(&tags_sel, tag);
+
+ index += 1;
+ if (index < tag_nav.wmin) continue;
+ if (index >= tag_nav.wmax) break;
+
+ if (sel && tagsel && index == tag_nav.sel)
+ style_on(pane->win, STYLE_ITEM_HOVER_SEL);
+ else if (sel && index == tag_nav.sel)
+ style_on(pane->win, STYLE_ITEM_HOVER);
+ else if (tagsel)
+ style_on(pane->win, STYLE_ITEM_SEL);
+ else if (index == tag_nav.sel)
+ style_on(pane->win, STYLE_PREV);
+
+ wmove(pane->win, 1 + index - tag_nav.wmin, 0);
+ wprintw(pane->win, "%-*.*ls", pane->w, pane->w, tag->name);
+
+ if (sel && tagsel && index == tag_nav.sel)
+ style_off(pane->win, STYLE_ITEM_HOVER_SEL);
+ else if (sel && index == tag_nav.sel)
+ style_off(pane->win, STYLE_ITEM_HOVER);
+ else if (tagsel)
+ style_off(pane->win, STYLE_ITEM_SEL);
+ else if (index == tag_nav.sel)
+ style_off(pane->win, STYLE_PREV);
+ }
+}
+
+int
+track_pane_input(wint_t c)
+{
+ struct link *link;
+ struct track *track;
+
+ switch (c) {
+ case KEY_UP:
+ listnav_update_sel(&track_nav, track_nav.sel - 1);
+ return 1;
+ case KEY_DOWN:
+ listnav_update_sel(&track_nav, track_nav.sel + 1);
+ return 1;
+ case KEY_ENTER:
+ link = list_at(tracks_vis, track_nav.sel);
+ if (!link) return 1;
+ track = UPCAST(link, struct ref)->data;
+ player_play_track(track);
+ return 1;
+ case KEY_PPAGE:
+ listnav_update_sel(&track_nav,
+ track_nav.sel - track_nav.wlen / 2);
+ return 1;
+ case KEY_NPAGE:
+ listnav_update_sel(&track_nav,
+ track_nav.sel + track_nav.wlen / 2);
+ return 1;
+ }
+
+ return 0;
+}
+
+void
+track_pane_vis(struct pane *pane, int sel)
+{
+ struct track *track;
+ struct link *iter;
+ struct tag *tag;
+ int index;
+
+ werase(pane->win);
+ pane_title(pane, "Tracks", sel);
+
+ listnav_update_bounds(&track_nav, 0, list_len(tracks_vis));
+ listnav_update_wlen(&track_nav, pane->h - 1);
+
+ index = -1;
+ for (LIST_ITER(tracks_vis, iter)) {
+ track = UPCAST(iter, struct ref)->data;
+
+ index += 1;
+ if (index < track_nav.wmin) continue;
+ if (index >= track_nav.wmax) break;
+
+ if (sel && index == track_nav.sel && track == player->track)
+ style_on(pane->win, STYLE_ITEM_HOVER_SEL);
+ else if (sel && index == track_nav.sel)
+ style_on(pane->win, STYLE_ITEM_HOVER);
+ else if (track == player->track)
+ style_on(pane->win, STYLE_ITEM_SEL);
+ else if (index == track_nav.sel)
+ style_on(pane->win, STYLE_PREV);
+
+ wmove(pane->win, 1 + index - track_nav.wmin, 0);
+ wprintw(pane->win, "%-*.*ls", pane->w, pane->w, track->name);
+
+ if (sel && index == track_nav.sel && track == player->track)
+ style_off(pane->win, STYLE_ITEM_HOVER_SEL);
+ else if (sel && index == track_nav.sel)
+ style_off(pane->win, STYLE_ITEM_HOVER);
+ else if (track == player->track)
+ style_off(pane->win, STYLE_ITEM_SEL);
+ else if (index == track_nav.sel)
+ style_off(pane->win, STYLE_PREV);
+ }
+}
+
+int
+run_cmd(const wchar_t *query)
+{
+ const wchar_t *sep;
+ int i, cmdlen;
+ bool success;
+
+ sep = wcschr(query, L' ');
+ cmdlen = sep ? sep - query : wcslen(query);
+ for (i = 0; i < command_count; i++) {
+ if (!wcsncmp(commands[i].name, query, cmdlen)) {
+ success = commands[i].func(sep ? sep + 1 : NULL);
+ if (!success && !cmd_status) {
+ free(cmd_status);
+ cmd_status = wcsdup(L"Command Failed!\n");
+ ASSERT(cmd_status != NULL);
+ }
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+play_track(const wchar_t *query)
+{
+ struct track *track;
+ struct link *iter;
+
+ for (LIST_ITER(&tracks, iter)) {
+ track = UPCAST(iter, struct ref)->data;
+ if (wcsstr(track->name, query)) {
+ player_play_track(track);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+select_tag(const wchar_t *query)
+{
+ struct tag *tag;
+ struct link *iter;
+ int index;
+
+ index = -1;
+ for (LIST_ITER(&tags, iter)) {
+ index += 1;
+ tag = UPCAST(iter, struct tag);
+ if (wcsstr(tag->name, query)) {
+ listnav_update_sel(&tag_nav, index);
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+int
+cmd_pane_input(wint_t c)
+{
+ wchar_t *res;
+ int match;
+
+ switch (c) {
+ case KEY_ESC:
+ match = wcscmp(completion_query.buf, history->input->buf);
+ if (!completion_reset && match)
+ inputln_copy(history->input, &completion_query);
+ else if (history->sel == history->input)
+ pane_sel = pane_top_sel;
+ else
+ history->sel = history->input;
+ break;
+ case KEY_LEFT:
+ inputln_left(history->sel);
+ break;
+ case KEY_RIGHT:
+ inputln_right(history->sel);
+ break;
+ case KEY_CTRL('w'):
+ inputln_del(history->sel, history->sel->cur);
+ break;
+ case KEY_UP:
+ history_next(history);
+ break;
+ case KEY_DOWN:
+ history_prev(history);
+ break;
+ case KEY_ENTER:
+ if (!*history->sel->buf) {
+ pane_sel = pane_top_sel;
+ break;
+ }
+
+ if (cmd_input_mode == IMODE_EXECUTE) {
+ run_cmd(history->sel->buf);
+ } else if (cmd_input_mode == IMODE_TRACK_SEARCH) {
+ play_track(history->sel->buf);
+ } else if (cmd_input_mode == IMODE_TAG_SEARCH) {
+ select_tag(history->sel->buf);
+ }
+
+ history_submit(history);
+ pane_sel = pane_top_sel;
+ break;
+ case KEY_TAB:
+ case KEY_BTAB:
+ if (history->sel != history->input) {
+ inputln_copy(history->input, history->sel);
+ history->sel = history->input;
+ }
+
+ if (completion_reset)
+ inputln_copy(&completion_query, history->input);
+
+ res = completion(completion_query.buf,
+ c == KEY_TAB, completion_reset);
+ if (res) inputln_replace(history->input, res);
+ free(res);
+
+ completion_reset = 0;
+ break;
+ case KEY_BACKSPACE:
+ if (history->sel->cur == 0) {
+ pane_sel = pane_top_sel;
+ break;
+ }
+ inputln_del(history->sel, 1);
+ completion_reset = 1;
+ break;
+ default:
+ if (!iswprint(c)) return 0;
+ inputln_addch(history->sel, c);
+ completion_reset = 1;
+ break;
+ }
+
+ return 1; /* grab everything */
+}
+
+void
+cmd_pane_vis(struct pane *pane, int sel)
+{
+ static wchar_t *linebuf = NULL;
+ static int linecap = 0;
+ struct inputln *cmd;
+ struct link *iter;
+ wchar_t *line, *end;
+ int index, offset;
+
+ werase(pane->win);
+
+ /* static line buffer for perf */
+ if (pane->w > linecap) {
+ linecap = MAX(linecap, pane->w);
+ linebuf = realloc(linebuf, linecap * sizeof(wchar_t));
+ }
+ end = linebuf + linecap;
+
+ /* track name */
+ style_on(pane->win, STYLE_TITLE);
+ pane_clearln(pane, 0);
+ if (player->track) {
+ swprintf(linebuf, linecap, L"%ls", player->track->name);
+ mvwaddwstr(pane->win, 0, 1, linebuf);
+ }
+ style_off(pane->win, STYLE_TITLE);
+
+ if (player->loaded) {
+ /* status line */
+ line = linebuf;
+ line += swprintf(line, end - line, L"%c ",
+ player_state_chars[player->state]);
+ line += swprintf(line, end - line, L"%s / ",
+ timestr(player->time_pos));
+ line += swprintf(line, end - line, L"%s",
+ timestr(player->time_end));
+
+ if (player->volume >= 0) {
+ line += swprintf(line, end - line, L" - vol: %u%%",
+ player->volume);
+ }
+
+ if (player->msg) {
+ line += swprintf(line, end - line, L" | [PLAYER] %s",
+ player->msg);
+ }
+
+ if (!list_empty(&player->queue)) {
+ line += swprintf(line, end - line,
+ L" | [QUEUE] %i tracks",
+ list_len(&player->queue));
+ }
+
+ ATTR_ON(pane->win, A_REVERSE);
+ pane_clearln(pane, 1);
+ mvwaddwstr(pane->win, 1, 0, linebuf);
+ ATTR_OFF(pane->win, A_REVERSE);
+ } else if (player->msg) {
+ /* player message */
+ line = linebuf;
+ line += swprintf(line, linecap, L"[PLAYER] %s", player->msg);
+ line += swprintf(line, end - line, L"%*.*s",
+ pane->w, pane->w, L" ");
+
+ pane_clearln(pane, 1);
+ mvwaddwstr(pane->win, 1, 0, linebuf);
+ }
+
+ /* status bits on right of status line */
+ if (player->loaded) ATTR_ON(pane->win, A_REVERSE);
+ mvwaddstr(pane->win, 1, pane->w - 5, "[ ]");
+ if (track_show_playlist) mvwaddstr(pane->win, 1, pane->w - 4, "P");
+ if (player->autoplay) mvwaddstr(pane->win, 1, pane->w - 3, "A");
+ if (player->shuffle) mvwaddstr(pane->win, 1, pane->w - 2, "S");
+ if (player->loaded) ATTR_OFF(pane->win, A_REVERSE);
+
+ if (sel || cmd_show) {
+ /* cmd and search input */
+ line = linebuf;
+
+ free(cmd_status);
+ cmd_status = NULL;
+
+ cmd = history->sel;
+ if (cmd != history->input) {
+ index = 0;
+ for (LIST_ITER(&history->list, iter)) {
+ if (UPCAST(iter, struct inputln) == cmd)
+ break;
+ }
+ line += swprintf(line, end - line, L"[%i] ",
+ iter ? index : -1);
+ } else {
+ line += swprintf(line, end - line, L"%c",
+ imode_prefix[cmd_input_mode]);
+ }
+ offset = wcslen(linebuf);
+
+ line += swprintf(line, end - line, L"%ls", cmd->buf);
+
+ pane_clearln(pane, 2);
+ mvwaddwstr(pane->win, 2, 0, linebuf);
+
+ if (sel) { /* show cursor in text */
+ ATTR_ON(pane->win, A_REVERSE);
+ wmove(pane->win, 2, offset + cmd->cur);
+ waddch(pane->win, cmd->cur < cmd->len
+ ? cmd->buf[cmd->cur] : L' ');
+ ATTR_OFF(pane->win, A_REVERSE);
+ }
+ } else if (cmd_status) {
+ pane_clearln(pane, 2);
+ style_on(pane->win, STYLE_ERROR);
+ mvwaddwstr(pane->win, 2, 1, cmd_status);
+ style_off(pane->win, STYLE_ERROR);
+ }
+}
+
+void
+queue_hover(void)
+{
+ struct link *link;
+
+ link = list_at(&player->playlist, track_nav.sel);
+ if (!link) return;
+ player_queue_append(UPCAST(link, struct ref)->data);
+}
+
+void
+update_track_playlist(void)
+{
+ struct link *link;
+ struct tag *tag;
+
+ if (track_show_playlist) {
+ tracks_vis = &player->playlist;
+ } else {
+ link = list_at(&tags, tag_nav.sel);
+ if (!link) return;
+ tag = UPCAST(link, struct tag);
+ tracks_vis = &tag->tracks;
+ }
+}
+
+void
+main_input(wint_t c)
+{
+ switch (c) {
+ case KEY_CTRL('r'):
+ redrawwin(stdscr);
+ break;
+ case KEY_TAB:
+ pane_sel = pane_sel == &pane_left
+ ? &pane_right : &pane_left;
+ pane_top_sel = pane_sel;
+ break;
+ case KEY_ESC:
+ pane_sel = pane_top_sel;
+ break;
+ case KEY_LEFT:
+ if (!player->loaded) break;
+ player_seek(MAX(player->time_pos - 10, 0));
+ break;
+ case KEY_RIGHT:
+ if (!player->loaded) break;
+ player_seek(MIN(player->time_pos + 10, player->time_end));
+ break;
+ case L'y':
+ queue_hover();
+ break;
+ case L'o':
+ player_queue_clear();
+ break;
+ case L'c':
+ player_toggle_pause();
+ break;
+ case L'n':
+ case L'>':
+ player_next();
+ break;
+ case L'p':
+ case L'<':
+ player_prev();
+ break;
+ case L'P':
+ if (track_show_playlist) {
+ pane_sel = tag_pane;
+ track_show_playlist = 0;
+ } else {
+ pane_sel = track_pane;
+ track_show_playlist = 1;
+ }
+ break;
+ case L'A':
+ player->autoplay ^= 1;
+ break;
+ case L'S':
+ player->shuffle ^= 1;
+ break;
+ case L'b':
+ player_seek(0);
+ break;
+ case L'x':
+ if (player->state == PLAYER_STATE_PLAYING) {
+ player_stop();
+ } else {
+ player_play();
+ }
+ break;
+ case L':':
+ cmd_input_mode = IMODE_EXECUTE;
+ pane_sel = &pane_bot;
+ completion_reset = 1;
+ history = &command_history;
+ completion = command_name_gen;
+ break;
+ case L'/':
+ cmd_input_mode = IMODE_TRACK_SEARCH;
+ pane_sel = &pane_bot;
+ completion_reset = 1;
+ history = &track_search_history;
+ completion = track_name_gen;
+ break;
+ case L'?':
+ cmd_input_mode = IMODE_TAG_SEARCH;
+ pane_sel = &pane_bot;
+ completion_reset = 1;
+ history = &tag_search_history;
+ completion = tag_name_gen;
+ break;
+ case L'+':
+ player_set_volume(MIN(100, player->volume + 5));
+ break;
+ case L'-':
+ player_set_volume(MAX(0, player->volume - 5));
+ break;
+ case L'q':
+ quit = 1;
+ break;
+ }
+}
+
+void
+main_vis(void)
+{
+ int i;
+
+ /* add missing title tile at the top */
+ style_on(stdscr, STYLE_TITLE);
+ move(0, pane_left.ex);
+ addch(' ');
+ style_off(stdscr, STYLE_TITLE);
+
+ /* draw left-right separator line */
+ style_on(stdscr, STYLE_PANE_SEP);
+ for (i = pane_left.sy + 1; i < pane_left.ey; i++) {
+ move(i, pane_left.ex);
+ addch(ACS_VLINE);
+ }
+ style_off(stdscr, STYLE_PANE_SEP);
+}
+
+void
+tui_curses_init(void)
+{
+ initscr();
+
+ /* do most of the handling ourselves,
+ * enable special keys */
+ raw();
+ noecho();
+ keypad(stdscr, TRUE);
+
+ /* update screen occasionally for things like
+ * time even when no input was received */
+ halfdelay(1);
+
+ /* inits COLOR and COLOR_PAIRS used by styles */
+ start_color();
+
+ /* dont show cursor */
+ curs_set(0);
+
+ /* we use ESC deselecting the current pane
+ * and not for escape sequences, so dont wait */
+ ESCDELAY = 0;
+}
+
+void
+tui_resize(void)
+{
+ struct link *iter;
+ struct tag *tag;
+ int i, leftw;
+
+ getmaxyx(stdscr, scrh, scrw);
+
+ /* guarantee a minimum terminal size */
+ while (scrw < 10 || scrh < 4) {
+ clear();
+ refresh();
+ usleep(10000);
+ getmaxyx(stdscr, scrh, scrw);
+ }
+
+ /* adjust tag pane width to name lengths */
+ leftw = 0;
+ for (LIST_ITER(&tags, iter)) {
+ tag = UPCAST(iter, struct tag);
+ leftw = MAX(leftw, wcslen(tag->name));
+ }
+ leftw = MAX(leftw + 1, 0.2f * scrw);
+
+ pane_resize(&pane_left, 0, 0, leftw, scrh - 3);
+ pane_resize(&pane_right, pane_left.ex + 1, 0, scrw, scrh - 3);
+ pane_resize(&pane_bot, 0, scrh - 3, scrw, scrh);
+}
+void
+tui_init(void)
+{
+ quit = 0;
+ cmd_input_mode = IMODE_TRACK_SEARCH;
+
+ inputln_init(&completion_query);
+ completion_reset = 1;
+
+ history_init(&track_search_history);
+ history_init(&tag_search_history);
+ history_init(&command_history);
+ history = &command_history;
+
+ tui_curses_init();
+
+ style_init();
+
+ pane_init((tag_pane = &pane_left), tag_pane_input, tag_pane_vis);
+ pane_init((track_pane = &pane_right), track_pane_input, track_pane_vis);
+ pane_init((cmd_pane = &pane_bot), cmd_pane_input, cmd_pane_vis);
+
+ pane_sel = &pane_left;
+ pane_top_sel = pane_sel;
+
+ listnav_init(&tag_nav);
+ listnav_init(&track_nav);
+
+ track_show_playlist = 0;
+ update_track_playlist();
+
+ tui_resize();
+}
+
+void
+tui_deinit(void)
+{
+ pane_free(&pane_left);
+ pane_free(&pane_right);
+ pane_free(&pane_bot);
+
+ history_free(&track_search_history);
+ history_free(&tag_search_history);
+ history_free(&command_history);
+
+ if (!isendwin()) endwin();
+}
+
+bool
+tui_update(void)
+{
+ bool handled;
+ wint_t c;
+ int i;
+
+ get_wch(&c);
+
+ if (c == KEY_RESIZE) {
+ tui_resize();
+ } else if (c != ERR) {
+ handled = 0;
+ if (pane_sel && pane_sel->active)
+ handled = pane_sel->handle(c);
+
+ /* fallback if char not handled by pane */
+ if (!handled) main_input(c);
+ }
+
+ update_track_playlist();
+
+ refresh();
+ for (i = 0; i < ARRLEN(panes); i++) {
+ /* only update ui for panes that are visible */
+ if (!panes[i]->active) continue;
+
+ panes[i]->update(panes[i], pane_sel == panes[i]);
+ wnoutrefresh(panes[i]->win);
+ }
+
+ main_vis();
+ doupdate();
+
+ return !quit;
+}
+
+void
+tui_restore(void)
+{
+ if (!isendwin())
+ endwin();
+}
+
+