/* * nncmpp -- the MPD client you never knew you needed * * Copyright (c) 2016, Přemysl Janouch * All rights reserved. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * */ #include "config.h" // My battle-tested C framework acting as a GLib replacement. Its one big // disadvantage is missing support for i18n but that can eventually be added // as an optional feature. Localised applications look super awkward, though. #define LIBERTY_WANT_POLLER #define LIBERTY_WANT_ASYNC #include "liberty/liberty.c" #include #include "mpd.c" #include #include #ifndef TIOCGWINSZ #include #endif // ! TIOCGWINSZ #include // ncurses is notoriously retarded for input handling, we need something // different if only to receive mouse events reliably. #include "termo.h" // It is surprisingly hard to find a good library to handle Unicode shenanigans, // and there's enough of those for it to be impractical to reimplement them. // // GLib ICU libunistring utf8proc // Decently sized . . x x // Grapheme breaks . x . x // Character width x . x x // Locale handling . . x . // Liberal license . x . x // // Also note that the ICU API is icky and uses UTF-16 for its primary encoding. // // Currently we're chugging along with libunistring but utf8proc seems viable. // Non-Unicode locales can mostly be handled with simple iconv like in sdtui. // Similarly grapheme breaks can be guessed at using character width (a basic // test here is Zalgo text). // // None of this is ever going to work too reliably anyway because terminals // and Unicode don't go awfully well together. In particular, character cell // devices have some problems with double-wide characters. #include #include #include #define CTRL_KEY(x) ((x) - 'A' + 1) #define APP_TITLE PROGRAM_NAME " " ///< Left top corner // --- Utilities --------------------------------------------------------------- // The standard endwin/refresh sequence makes the terminal flicker static void update_curses_terminal_size (void) { #if defined (HAVE_RESIZETERM) && defined (TIOCGWINSZ) struct winsize size; if (!ioctl (STDOUT_FILENO, TIOCGWINSZ, (char *) &size)) { char *row = getenv ("LINES"); char *col = getenv ("COLUMNS"); unsigned long tmp; resizeterm ( (row && xstrtoul (&tmp, row, 10)) ? tmp : size.ws_row, (col && xstrtoul (&tmp, col, 10)) ? tmp : size.ws_col); } #else // HAVE_RESIZETERM && TIOCGWINSZ endwin (); refresh (); #endif // HAVE_RESIZETERM && TIOCGWINSZ } // --- Application ------------------------------------------------------------- // Function names are prefixed mostly because of curses which clutters the // global namespace and makes it harder to distinguish what functions relate to. // Avoiding colours in the defaults here in order to support dumb terminals #define ATTRIBUTE_TABLE(XX) \ XX( HEADER, "header", -1, -1, A_REVERSE ) \ XX( ACTIVE, "header_active", -1, -1, A_UNDERLINE ) \ XX( EVEN, "even", -1, -1, 0 ) \ XX( ODD, "odd", -1, -1, 0 ) enum { #define XX(name, config, fg_, bg_, attrs_) ATTRIBUTE_ ## name, ATTRIBUTE_TABLE (XX) #undef XX ATTRIBUTE_COUNT }; struct attrs { short fg; ///< Foreground colour index short bg; ///< Background colour index chtype attrs; ///< Other attributes }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // The user interface is focused on conceptual simplicity. That is important // since we're not using any TUI framework (which are mostly a lost cause to me // in the post-Unicode era and not worth pursuing), and the code would get // bloated and incomprehensible fast. We mostly rely on app_add_utf8_string() // to write text from left to right row after row while keeping track of cells. // // There is an independent top pane displaying general status information, // followed by a tab bar and a listview served by a per-tab event handler. // // For simplicity, the listview can only work with items that are one row high. struct tab; struct row_buffer; /// Try to handle an event in the tab typedef bool (*tab_event_fn) (struct tab *self, termo_key_t *event); /// Draw an item to the screen using the row buffer API typedef void (*tab_item_draw_fn) (struct tab *self, unsigned item_index, struct row_buffer *buffer); struct tab { LIST_HEADER (struct tab) char *name; ///< Visible identifier size_t name_width; ///< Visible width of the name // Implementation: // TODO: free() callback? tab_event_fn on_event; ///< Event handler callback tab_item_draw_fn on_item_draw; ///< Item draw callback // Provided by tab owner: bool can_multiselect; ///< Multiple items can be selected size_t item_count; ///< Total item count // Managed by the common handler: int item_top; ///< Index of the topmost item int item_selected; ///< Index of the selected item }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - enum player_state { PLAYER_STOPPED, PLAYER_PLAYING, PLAYER_PAUSED }; // Basically a container for most of the globals; no big sense in handing // around a pointer to this, hence it is a simple global variable as well. // There is enough global state as it is. static struct app_context { // Event loop: struct poller poller; ///< Poller bool quitting; ///< Quit signal for the event loop bool polling; ///< The event loop is running struct poller_fd tty_event; ///< Terminal input event struct poller_fd signal_event; ///< Signal FD event // Connection: struct mpd_client client; ///< MPD client interface struct poller_timer reconnect_event;///< MPD reconnect timer enum player_state state; ///< Player state // TODO: probably save the full info reply char *song; ///< Currently playing song // Data: struct config config; ///< Program configuration struct tab *tabs; ///< All tabs struct tab *active_tab; ///< Active tab // Terminal: termo_t *tk; ///< termo handle struct poller_timer tk_timer; ///< termo timeout timer bool locale_is_utf8; ///< The locale is Unicode int list_offset; ///< Height of the top part struct attrs attrs[ATTRIBUTE_COUNT]; } g_ctx; /// Shortcut to retrieve named terminal attributes #define APP_ATTR(name) g_ctx.attrs[ATTRIBUTE_ ## name].attrs // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void tab_init (struct tab *self, const char *name) { memset (self, 0, sizeof *self); // Add some padding for decorative purposes self->name = xstrdup_printf (" %s ", name); // Assuming tab names are pure ASCII, otherwise this would be inaccurate // and we'd need to filter it first to replace invalid chars with '?' self->name_width = u8_strwidth ((uint8_t *) self->name, locale_charset ()); self->item_selected = -1; } static void tab_free (struct tab *self) { free (self->name); } // --- Configuration ----------------------------------------------------------- static struct config_schema g_config_settings[] = { { .name = "address", .comment = "Address to connect to the MPD server", .type = CONFIG_ITEM_STRING, .default_ = "localhost" }, { .name = "password", .comment = "Password to use for MPD authentication", .type = CONFIG_ITEM_STRING }, { .name = "root", .comment = "Where all the files MPD is playing are located", .type = CONFIG_ITEM_STRING }, {} }; static struct config_schema g_config_colors[] = { #define XX(name_, config, fg_, bg_, attrs_) \ { .name = config, .type = CONFIG_ITEM_STRING }, ATTRIBUTE_TABLE (XX) #undef XX {} }; static const char * get_config_string (struct config_item *root, const char *key) { struct config_item *item = config_item_get (root, key, NULL); hard_assert (item); if (item->type == CONFIG_ITEM_NULL) return NULL; hard_assert (config_item_type_is_string (item->type)); return item->value.string.str; } /// Load configuration for a color using a subset of git config colors static void app_load_color (struct config_item *subtree, const char *name, int id) { const char *value = get_config_string (subtree, name); if (!value) return; struct str_vector v; str_vector_init (&v); cstr_split_ignore_empty (value, ' ', &v); int colors = 0; struct attrs attrs = { -1, -1, 0 }; for (char **it = v.vector; *it; it++) { char *end = NULL; long n = strtol (*it, &end, 10); if (*it != end && !*end && n >= SHRT_MIN && n <= SHRT_MAX) { if (colors == 0) attrs.fg = n; if (colors == 1) attrs.bg = n; colors++; } else if (!strcmp (*it, "bold")) attrs.attrs |= A_BOLD; else if (!strcmp (*it, "dim")) attrs.attrs |= A_DIM; else if (!strcmp (*it, "ul")) attrs.attrs |= A_UNDERLINE; else if (!strcmp (*it, "blink")) attrs.attrs |= A_BLINK; else if (!strcmp (*it, "reverse")) attrs.attrs |= A_REVERSE; #ifdef A_ITALIC else if (!strcmp (*it, "italic")) attrs.attrs |= A_ITALIC; #endif // A_ITALIC } str_vector_free (&v); g_ctx.attrs[id] = attrs; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void load_config_settings (struct config_item *subtree, void *user_data) { config_schema_apply_to_object (g_config_settings, subtree, user_data); } static void load_config_colors (struct config_item *subtree, void *user_data) { config_schema_apply_to_object (g_config_colors, subtree, user_data); // The attributes cannot be changed dynamically right now, so it doesn't // make much sense to make use of "on_change" callbacks either. // For simplicity, we should reload the entire table on each change anyway. #define XX(name, config, fg_, bg_, attrs_) \ app_load_color (subtree, config, ATTRIBUTE_ ## name); ATTRIBUTE_TABLE (XX) #undef XX } static void app_load_configuration (void) { struct config *config = &g_ctx.config; config_register_module (config, "settings", load_config_settings, NULL); config_register_module (config, "colors", load_config_colors, NULL); char *filename = resolve_filename (PROGRAM_NAME ".conf", resolve_relative_config_filename); if (!filename) return; struct error *e = NULL; struct config_item *root = config_read_from_file (filename, &e); free (filename); if (e) { print_error ("error loading configuration: %s", e->message); error_free (e); exit (EXIT_FAILURE); } if (root) { config_load (&g_ctx.config, root); config_schema_call_changed (g_ctx.config.root); } } // --- Application ------------------------------------------------------------- static void app_init_attributes (void) { #define XX(name, config, fg_, bg_, attrs_) \ g_ctx.attrs[ATTRIBUTE_ ## name].fg = fg_; \ g_ctx.attrs[ATTRIBUTE_ ## name].bg = bg_; \ g_ctx.attrs[ATTRIBUTE_ ## name].attrs = attrs_; ATTRIBUTE_TABLE (XX) #undef XX } static void app_init_context (void) { memset (&g_ctx, 0, sizeof g_ctx); poller_init (&g_ctx.poller); mpd_client_init (&g_ctx.client, &g_ctx.poller); config_init (&g_ctx.config); // This is also approximately what libunistring does internally, // since the locale name is canonicalized by locale_charset(). // Note that non-Unicode locales are handled pretty inefficiently. g_ctx.locale_is_utf8 = !strcasecmp_ascii (locale_charset (), "UTF-8"); app_init_attributes (); } static void app_init_terminal (void) { TERMO_CHECK_VERSION; if (!(g_ctx.tk = termo_new (STDIN_FILENO, NULL, 0))) abort (); if (!initscr () || nonl () == ERR) abort (); // Disable cursor, we're not going to use it most of the time curs_set (0); // By default we don't use any colors so they're not required... if (start_color () == ERR || use_default_colors () == ERR || COLOR_PAIRS <= ATTRIBUTE_COUNT) return; for (int a = 0; a < ATTRIBUTE_COUNT; a++) { // ...thus we can reset back to defaults even after initializing some if (g_ctx.attrs[a].fg >= COLORS || g_ctx.attrs[a].fg < -1 || g_ctx.attrs[a].bg >= COLORS || g_ctx.attrs[a].bg < -1) { app_init_attributes (); return; } init_pair (a + 1, g_ctx.attrs[a].fg, g_ctx.attrs[a].bg); g_ctx.attrs[a].attrs |= COLOR_PAIR (a + 1); } } static void app_free_context (void) { mpd_client_free (&g_ctx.client); free (g_ctx.song); config_free (&g_ctx.config); poller_free (&g_ctx.poller); if (g_ctx.tk) termo_destroy (g_ctx.tk); } static void app_quit (void) { g_ctx.quitting = true; // TODO: bring down the MPD interface (if that's needed at all); // so far there's nothing for us to wait on, so let's just stop looping g_ctx.polling = false; } static bool app_is_character_in_locale (ucs4_t ch) { // Avoid the overhead joined with calling iconv() for all characters. if (g_ctx.locale_is_utf8) return true; // The library really creates a new conversion object every single time // and doesn't provide any smarter APIs. Luckily, most users use UTF-8. size_t len; char *tmp = u32_conv_to_encoding (locale_charset (), iconveh_error, &ch, 1, NULL, NULL, &len); if (!tmp) return false; free (tmp); return true; } // --- Terminal output --------------------------------------------------------- // Necessary abstraction to simplify aligned, formatted character output struct row_char { LIST_HEADER (struct row_char) ucs4_t c; ///< Unicode codepoint chtype attrs; ///< Special attributes int width; ///< How many cells this takes }; struct row_buffer { struct row_char *chars; ///< Characters struct row_char *chars_tail; ///< Tail of characters size_t chars_len; ///< Character count int total_width; ///< Total width of all characters }; static void row_buffer_init (struct row_buffer *self) { memset (self, 0, sizeof *self); } static void row_buffer_free (struct row_buffer *self) { LIST_FOR_EACH (struct row_char, it, self->chars) free (it); } /// Replace invalid chars and push all codepoints to the array w/ attributes. static void row_buffer_append (struct row_buffer *self, const char *str, chtype attrs) { // The encoding is only really used internally for some corner cases const char *encoding = locale_charset (); ucs4_t c; const uint8_t *start = (const uint8_t *) str, *next = start; while ((next = u8_next (&c, next))) { if (uc_width (c, encoding) < 0 || !app_is_character_in_locale (c)) c = '?'; struct row_char *rc = xmalloc (sizeof *rc); *rc = (struct row_char) { .c = c, .attrs = attrs, .width = uc_width (c, encoding) }; LIST_APPEND_WITH_TAIL (self->chars, self->chars_tail, rc); self->chars_len++; self->total_width += rc->width; } } /// Pop as many codepoints as needed to free up "space" character cells. /// Given the suffix nature of combining marks, this should work pretty fine. static int row_buffer_pop_cells (struct row_buffer *self, int space) { int made = 0; while (self->chars && made < space) { struct row_char *tail = self->chars_tail; LIST_UNLINK_WITH_TAIL (self->chars, self->chars_tail, tail); self->chars_len--; made += tail->width; free (tail); } self->total_width -= made; return made; } static void row_buffer_ellipsis (struct row_buffer *self, int target, chtype attrs) { row_buffer_pop_cells (self, self->total_width - target); ucs4_t ellipsis = L'…'; if (app_is_character_in_locale (ellipsis)) { if (self->total_width >= target) row_buffer_pop_cells (self, 1); if (self->total_width + 1 <= target) row_buffer_append (self, "…", attrs); } else if (target >= 3) { if (self->total_width >= target) row_buffer_pop_cells (self, 3); if (self->total_width + 3 <= target) row_buffer_append (self, "...", attrs); } } static void row_buffer_print (uint32_t *ucs4, chtype attrs) { // Cannot afford to convert negative numbers to the unsigned chtype. uint8_t *str = (uint8_t *) u32_strconv_to_locale (ucs4); if (str) { for (uint8_t *p = str; *p; p++) addch (*p | attrs); free (str); } } static void row_buffer_flush (struct row_buffer *self) { if (!self->chars) return; // We only NUL-terminate the chunks because of the libunistring API uint32_t chunk[self->chars_len + 1], *insertion_point = chunk; LIST_FOR_EACH (struct row_char, it, self->chars) { if (it->prev && it->attrs != it->prev->attrs) { row_buffer_print (chunk, it->prev->attrs); insertion_point = chunk; } *insertion_point++ = it->c; *insertion_point = 0; } row_buffer_print (chunk, self->chars_tail->attrs); } // --- Help tab ---------------------------------------------------------------- // TODO: either find something else to put in here or remove the wrapper struct static struct { struct tab super; ///< Parent class } g_help_tab; static struct help_tab_item { const char *text; ///< Item text } g_help_items[] = { { "First entry on the list" }, { "Something different" }, { "Yet another item" }, }; static void help_tab_on_item_draw (struct tab *self, unsigned item_index, struct row_buffer *buffer) { (void) self; hard_assert (item_index <= N_ELEMENTS (g_help_items)); row_buffer_append (buffer, g_help_items[item_index].text, 0); } static struct tab * help_tab_create () { struct tab *super = &g_help_tab.super; tab_init (super, "Help"); super->on_item_draw = help_tab_on_item_draw; super->item_count = N_ELEMENTS (g_help_items); super->item_selected = 0; return super; } // --- Application ------------------------------------------------------------- /// Write the given UTF-8 string padded with spaces. /// @param[in] n The number of characters to write, or -1 for the whole string. /// @param[in] attrs Text attributes for the text, without padding. /// To change the attributes of all output, use attrset(). /// @return The number of characters output. static size_t app_write_utf8 (const char *str, chtype attrs, int n) { if (!n) return 0; struct row_buffer buf; row_buffer_init (&buf); row_buffer_append (&buf, str, attrs); if (n < 0) n = buf.total_width; if (buf.total_width > n) row_buffer_ellipsis (&buf, n, attrs); row_buffer_flush (&buf); for (int i = buf.total_width; i < n; i++) addch (' '); row_buffer_free (&buf); return n; } static void app_redraw_top (void) { // TODO: this will eventually be dynamically computed depending on contents g_ctx.list_offset = 2; attrset (0); mvwhline (stdscr, 0, 0, 0, COLS); switch (g_ctx.client.state) { case MPD_CONNECTED: switch (g_ctx.state) { case PLAYER_PLAYING: case PLAYER_PAUSED: app_write_utf8 (g_ctx.song, 0, COLS); break; case PLAYER_STOPPED: app_write_utf8 ("Stopped", 0, COLS); } break; case MPD_CONNECTING: app_write_utf8 ("Connecting to MPD...", 0, COLS); break; case MPD_DISCONNECTED: app_write_utf8 ("Disconnected", 0, COLS); } attrset (APP_ATTR (HEADER)); mvwhline (stdscr, 1, 0, APP_ATTR (HEADER), COLS); // TODO: render this with APP_ATTR (ACTIVE) when the help tab is selected; // ...maybe the help tab should not even be on the list? size_t indent = app_write_utf8 (APP_TITLE, A_BOLD, -1); attrset (0); LIST_FOR_EACH (struct tab, it, g_ctx.tabs) { indent += app_write_utf8 (it->name, it == g_ctx.active_tab ? APP_ATTR (ACTIVE) : APP_ATTR (HEADER), MIN (COLS - indent, it->name_width)); } refresh (); } static void app_redraw_view (void) { move (g_ctx.list_offset, 0); clrtobot (); // TODO: display a scrollbar on the right side struct tab *tab = g_ctx.active_tab; int to_show = MIN (LINES - g_ctx.list_offset, (int) tab->item_count - tab->item_top); for (int row_index = 0; row_index < to_show; row_index++) { unsigned item_index = tab->item_top + row_index; int row_attrs = (item_index & 1) ? APP_ATTR (ODD) : APP_ATTR (EVEN); if ((int) item_index == tab->item_selected) row_attrs |= A_REVERSE; attrset (row_attrs); struct row_buffer buf; row_buffer_init (&buf); tab->on_item_draw (tab, item_index, &buf); if (buf.total_width > COLS) row_buffer_ellipsis (&buf, COLS, row_attrs); row_buffer_flush (&buf); for (int i = buf.total_width; i < COLS; i++) addch (' '); row_buffer_free (&buf); } attrset (0); refresh (); } static void app_redraw (void) { app_redraw_top (); app_redraw_view (); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /// Scroll up @a n items. Doesn't redraw. static bool app_scroll_up (int n) { struct tab *tab = g_ctx.active_tab; if (tab->item_top < n) { tab->item_top = 0; return false; } tab->item_top -= n; return true; } /// Scroll down @a n items. Doesn't redraw. static bool app_scroll_down (int n) { struct tab *tab = g_ctx.active_tab; // TODO: if (n_items >= lines), don't allow to scroll off past the end if ((tab->item_top += n) >= (int) tab->item_count) { if (tab->item_count) tab->item_top = tab->item_count - 1; else tab->item_top = 0; return false; } return true; } /// Moves the selection one item up. static bool app_one_item_up (void) { struct tab *tab = g_ctx.active_tab; if (tab->item_selected < 1) return false; if (--tab->item_selected < tab->item_top) app_scroll_up (tab->item_top - tab->item_selected); app_redraw_view (); return true; } /// Moves the selection one item down. static bool app_one_item_down (void) { struct tab *tab = g_ctx.active_tab; if (tab->item_selected + 1 >= (int) tab->item_count) return false; int n_visible = LINES - g_ctx.list_offset; if (++tab->item_selected >= tab->item_top + n_visible) app_scroll_down (1); app_redraw_view (); return true; } static bool app_goto_tab (unsigned n) { // TODO: go to tab n, return false if out of range return false; app_redraw (); return true; } static void app_process_resize (void) { struct tab *tab = g_ctx.active_tab; if (tab->item_selected < 0) return; int n_visible = LINES - g_ctx.list_offset; if (n_visible < 0) return; // Scroll up as needed to keep the selection visible int selected_offset = tab->item_selected - tab->item_top; if (selected_offset >= n_visible) app_scroll_up (selected_offset - n_visible + 1); app_redraw (); } // --- User input handling ----------------------------------------------------- enum user_action { USER_ACTION_NONE, USER_ACTION_QUIT, USER_ACTION_REDRAW, USER_ACTION_GOTO_ITEM_PREVIOUS, USER_ACTION_GOTO_ITEM_NEXT, USER_ACTION_GOTO_PAGE_PREVIOUS, USER_ACTION_GOTO_PAGE_NEXT, USER_ACTION_COUNT }; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool app_process_user_action (enum user_action action) { switch (action) { case USER_ACTION_QUIT: return false; case USER_ACTION_REDRAW: clear (); app_redraw (); return true; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case USER_ACTION_GOTO_ITEM_PREVIOUS: app_one_item_up (); return true; case USER_ACTION_GOTO_ITEM_NEXT: app_one_item_down (); return true; case USER_ACTION_GOTO_PAGE_PREVIOUS: app_scroll_up (LINES - (int) g_ctx.list_offset); app_redraw_view (); return true; case USER_ACTION_GOTO_PAGE_NEXT: app_scroll_down (LINES - (int) g_ctx.list_offset); app_redraw_view (); return true; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - case USER_ACTION_NONE: return true; default: hard_assert (!"unhandled user action"); } return true; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool app_process_keysym (termo_key_t *event) { enum user_action action = USER_ACTION_NONE; typedef const enum user_action ActionMap[TERMO_N_SYMS]; static ActionMap actions = { [TERMO_SYM_ESCAPE] = USER_ACTION_QUIT, [TERMO_SYM_UP] = USER_ACTION_GOTO_ITEM_PREVIOUS, [TERMO_SYM_DOWN] = USER_ACTION_GOTO_ITEM_NEXT, [TERMO_SYM_PAGEUP] = USER_ACTION_GOTO_PAGE_PREVIOUS, [TERMO_SYM_PAGEDOWN] = USER_ACTION_GOTO_PAGE_NEXT, }; static ActionMap actions_alt = { }; static ActionMap actions_ctrl = { }; if (!event->modifiers) action = actions[event->code.sym]; else if (event->modifiers == TERMO_KEYMOD_ALT) action = actions_alt[event->code.sym]; else if (event->modifiers == TERMO_KEYMOD_CTRL) action = actions_ctrl[event->code.sym]; return app_process_user_action (action); } static bool app_process_ctrl_key (termo_key_t *event) { static const enum user_action actions[32] = { [CTRL_KEY ('L')] = USER_ACTION_REDRAW, [CTRL_KEY ('P')] = USER_ACTION_GOTO_ITEM_PREVIOUS, [CTRL_KEY ('N')] = USER_ACTION_GOTO_ITEM_NEXT, [CTRL_KEY ('B')] = USER_ACTION_GOTO_PAGE_PREVIOUS, [CTRL_KEY ('F')] = USER_ACTION_GOTO_PAGE_NEXT, }; int64_t i = (int64_t) event->code.codepoint - 'a' + 1; if (i > 0 && i < (int64_t) N_ELEMENTS (actions)) return app_process_user_action (actions[i]); return true; } static bool app_process_alt_key (termo_key_t *event) { if (event->code.codepoint >= '0' && event->code.codepoint <= '9') { int n = event->code.codepoint - '0'; if (!app_goto_tab ((n == 0 ? 10 : n) - 1)) beep (); } return true; } static bool app_process_key (termo_key_t *event) { if (event->modifiers == TERMO_KEYMOD_CTRL) return app_process_ctrl_key (event); if (event->modifiers == TERMO_KEYMOD_ALT) return app_process_alt_key (event); if (event->modifiers) return true; // TODO: normal unmodified keys will have functions as well ucs4_t c = event->code.codepoint; return true; } static void app_process_left_mouse_click (int line, int column) { if (line < g_ctx.list_offset - 1) { // TODO: emulate some GUI widgets; this is going to be wild } else if (line == g_ctx.list_offset - 1) { struct tab *winner = NULL; int indent = strlen (APP_TITLE); // TODO: set the winner to the special help tab in this case if (column < indent) return; for (struct tab *iter = g_ctx.tabs; !winner && iter; iter = iter->next) { if (column < (indent += iter->name_width)) winner = iter; } if (winner) { g_ctx.active_tab = winner; app_redraw (); } } else { struct tab *tab = g_ctx.active_tab; int row_index = line - g_ctx.list_offset; if (row_index >= (int) tab->item_count - tab->item_top) return; tab->item_selected = row_index + tab->item_top; app_redraw_view (); } } static bool app_process_mouse (termo_key_t *event) { int line, column, button; termo_mouse_event_t type; termo_interpret_mouse (g_ctx.tk, event, &type, &button, &line, &column); if (type != TERMO_MOUSE_PRESS) return true; if (button == 1) app_process_left_mouse_click (line, column); else if (button == 4) app_process_user_action (USER_ACTION_GOTO_ITEM_PREVIOUS); else if (button == 5) app_process_user_action (USER_ACTION_GOTO_ITEM_NEXT); return true; } static bool app_process_termo_event (termo_key_t *event) { switch (event->type) { case TERMO_TYPE_MOUSE: return app_process_mouse (event); case TERMO_TYPE_KEY: return app_process_key (event); case TERMO_TYPE_KEYSYM: return app_process_keysym (event); default: return true; } } // --- Signals ----------------------------------------------------------------- static int g_signal_pipe[2]; ///< A pipe used to signal... signals /// Program termination has been requested by a signal static volatile sig_atomic_t g_termination_requested; /// The window has changed in size static volatile sig_atomic_t g_winch_received; static void signals_postpone_handling (char id) { int original_errno = errno; if (write (g_signal_pipe[1], &id, 1) == -1) soft_assert (errno == EAGAIN); errno = original_errno; } static void signals_superhandler (int signum) { switch (signum) { case SIGWINCH: g_winch_received = true; signals_postpone_handling ('w'); break; case SIGINT: case SIGTERM: g_termination_requested = true; signals_postpone_handling ('t'); break; default: hard_assert (!"unhandled signal"); } } static void signals_setup_handlers (void) { if (pipe (g_signal_pipe) == -1) exit_fatal ("%s: %s", "pipe", strerror (errno)); set_cloexec (g_signal_pipe[0]); set_cloexec (g_signal_pipe[1]); // So that the pipe cannot overflow; it would make write() block within // the signal handler, which is something we really don't want to happen. // The same holds true for read(). set_blocking (g_signal_pipe[0], false); set_blocking (g_signal_pipe[1], false); signal (SIGPIPE, SIG_IGN); struct sigaction sa; sa.sa_flags = SA_RESTART; sa.sa_handler = signals_superhandler; sigemptyset (&sa.sa_mask); if (sigaction (SIGWINCH, &sa, NULL) == -1 || sigaction (SIGINT, &sa, NULL) == -1 || sigaction (SIGTERM, &sa, NULL) == -1) exit_fatal ("sigaction: %s", strerror (errno)); } // --- MPD interface ----------------------------------------------------------- // TODO: this entire thing has been slavishly copy-pasted from dwmstatus // TODO: try to move some of this code to mpd.c // Sometimes it's not that easy and there can be repeating entries static void mpd_vector_to_map (const struct str_vector *data, struct str_map *map) { str_map_init (map); map->key_xfrm = tolower_ascii_strxfrm; map->free = free; char *key, *value; for (size_t i = 0; i < data->len; i++) { if ((key = mpd_client_parse_kv (data->vector[i], &value))) str_map_set (map, key, xstrdup (value)); else print_debug ("%s: %s", "erroneous MPD output", data->vector[i]); } } static void mpd_on_info_response (const struct mpd_response *response, const struct str_vector *data, void *user_data) { (void) user_data; if (!response->success) { print_debug ("%s: %s", "retrieving MPD info failed", response->message_text); return; } struct str_map map; mpd_vector_to_map (data, &map); const char *value; g_ctx.state = PLAYER_PLAYING; if ((value = str_map_find (&map, "state"))) { if (!strcmp (value, "stop")) g_ctx.state = PLAYER_STOPPED; if (!strcmp (value, "pause")) g_ctx.state = PLAYER_PAUSED; } struct str s; str_init (&s); char *mpd_song = NULL; if ((value = str_map_find (&map, "title")) || (value = str_map_find (&map, "name")) || (value = str_map_find (&map, "file"))) str_append_printf (&s, "\"%s\"", value); if ((value = str_map_find (&map, "artist"))) str_append_printf (&s, " by \"%s\"", value); if ((value = str_map_find (&map, "album"))) str_append_printf (&s, " from \"%s\"", value); mpd_song = str_steal (&s); str_map_free (&map); free (g_ctx.song); g_ctx.song = mpd_song; app_redraw (); } static void mpd_request_info (void) { struct mpd_client *c = &g_ctx.client; mpd_client_list_begin (c); mpd_client_send_command (c, "currentsong", NULL); mpd_client_send_command (c, "status", NULL); mpd_client_list_end (c); mpd_client_add_task (c, mpd_on_info_response, NULL); mpd_client_idle (c, 0); } static void mpd_on_events (unsigned subsystems, void *user_data) { (void) user_data; struct mpd_client *c = &g_ctx.client; if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_PLAYLIST)) mpd_request_info (); else mpd_client_idle (c, 0); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static void mpd_queue_reconnect (void) { poller_timer_set (&g_ctx.reconnect_event, 5 * 1000); } static void mpd_on_password_response (const struct mpd_response *response, const struct str_vector *data, void *user_data) { (void) data; (void) user_data; struct mpd_client *c = &g_ctx.client; if (response->success) mpd_request_info (); else { print_error ("%s: %s", "couldn't authenticate to MPD", response->message_text); mpd_client_send_command (c, "close", NULL); } } static void mpd_on_connected (void *user_data) { (void) user_data; struct mpd_client *c = &g_ctx.client; const char *password = get_config_string (g_ctx.config.root, "settings.password"); if (password) { mpd_client_send_command (c, "password", password, NULL); mpd_client_add_task (c, mpd_on_password_response, NULL); } else mpd_request_info (); } static void mpd_on_failure (void *user_data) { (void) user_data; // This is also triggered both by a failed connect and a clean disconnect print_error ("connection to MPD failed"); mpd_queue_reconnect (); } static void app_on_reconnect (void *user_data) { (void) user_data; struct mpd_client *c = &g_ctx.client; c->on_failure = mpd_on_failure; c->on_connected = mpd_on_connected; c->on_event = mpd_on_events; // We accept hostname/IPv4/IPv6 in pseudo-URL format, as well as sockets char *address = xstrdup (get_config_string (g_ctx.config.root, "settings.address")), *p = address, *host = address, *port = "6600"; // Unwrap IPv6 addresses in format_host_port_pair() format char *right_bracket = strchr (p, ']'); if (p[0] == '[' && right_bracket) { *right_bracket = '\0'; host = p + 1; p = right_bracket + 1; } char *colon = strchr (p, ':'); if (colon) { *colon = '\0'; port = colon + 1; } struct error *e = NULL; if (!mpd_client_connect (c, host, port, &e)) { print_error ("%s: %s", "cannot connect to MPD", e->message); error_free (e); mpd_queue_reconnect (); } free (address); } // --- Initialisation, event handling ------------------------------------------ static void app_on_tty_readable (const struct pollfd *fd, void *user_data) { (void) user_data; if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); poller_timer_reset (&g_ctx.tk_timer); termo_advisereadable (g_ctx.tk); termo_key_t event; termo_result_t res; while ((res = termo_getkey (g_ctx.tk, &event)) == TERMO_RES_KEY) if (!app_process_termo_event (&event)) { app_quit (); return; } if (res == TERMO_RES_AGAIN) poller_timer_set (&g_ctx.tk_timer, termo_get_waittime (g_ctx.tk)); else if (res == TERMO_RES_ERROR || res == TERMO_RES_EOF) { app_quit (); return; } } static void app_on_key_timer (void *user_data) { (void) user_data; termo_key_t event; if (termo_getkey_force (g_ctx.tk, &event) == TERMO_RES_KEY) if (!app_process_termo_event (&event)) app_quit (); } static void app_on_signal_pipe_readable (const struct pollfd *fd, void *user_data) { (void) user_data; char id = 0; (void) read (fd->fd, &id, 1); if (g_termination_requested && !g_ctx.quitting) app_quit (); if (g_winch_received) { update_curses_terminal_size (); app_process_resize (); g_winch_received = false; } } static void app_log_handler (void *user_data, const char *quote, const char *fmt, va_list ap) { // TODO: we might want to make use of the user_data (attribute?) (void) user_data; // We certainly don't want to end up in a possibly infinite recursion static bool in_processing; if (in_processing) return; in_processing = true; struct str message; str_init (&message); str_append (&message, quote); str_append_vprintf (&message, fmt, ap); // If the standard error output isn't redirected, try our best at showing // the message to the user; it will probably get overdrawn soon // TODO: remember it somewhere so that it stays shown for a while if (isatty (STDERR_FILENO)) { // TODO: remember the position and attributes and restore them attrset (A_REVERSE); mvwhline (stdscr, LINES - 1, 0, A_REVERSE, COLS); app_write_utf8 (message.str, 0, COLS); } else fprintf (stderr, "%s\n", message.str); str_free (&message); in_processing = false; } static void app_init_poller_events (void) { poller_fd_init (&g_ctx.signal_event, &g_ctx.poller, g_signal_pipe[0]); g_ctx.signal_event.dispatcher = app_on_signal_pipe_readable; poller_fd_set (&g_ctx.signal_event, POLLIN); poller_fd_init (&g_ctx.tty_event, &g_ctx.poller, STDIN_FILENO); g_ctx.tty_event.dispatcher = app_on_tty_readable; poller_fd_set (&g_ctx.tty_event, POLLIN); poller_timer_init (&g_ctx.tk_timer, &g_ctx.poller); g_ctx.tk_timer.dispatcher = app_on_key_timer; poller_timer_init (&g_ctx.reconnect_event, &g_ctx.poller); g_ctx.reconnect_event.dispatcher = app_on_reconnect; poller_timer_set (&g_ctx.reconnect_event, 0); } int main (int argc, char *argv[]) { static const struct opt opts[] = { { 'd', "debug", NULL, 0, "run in debug mode" }, { 'h', "help", NULL, 0, "display this help and exit" }, { 'V', "version", NULL, 0, "output version information and exit" }, { 0, NULL, NULL, 0, NULL } }; struct opt_handler oh; opt_handler_init (&oh, argc, argv, opts, NULL, "MPD client."); int c; while ((c = opt_handler_get (&oh)) != -1) switch (c) { case 'd': g_debug_mode = true; break; case 'h': opt_handler_usage (&oh, stdout); exit (EXIT_SUCCESS); case 'V': printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); exit (EXIT_SUCCESS); default: print_error ("wrong options"); opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } argc -= optind; argv += optind; if (argc) { opt_handler_usage (&oh, stderr); exit (EXIT_FAILURE); } opt_handler_free (&oh); // We only need to convert to and from the terminal encoding if (!setlocale (LC_CTYPE, "")) print_warning ("failed to set the locale"); app_init_context (); app_load_configuration (); app_init_terminal (); g_log_message_real = app_log_handler; // TODO: create more tabs // TODO: in debug mode add a tab with all messages LIST_PREPEND (g_ctx.tabs, help_tab_create ()); g_ctx.active_tab = g_ctx.tabs; app_redraw (); signals_setup_handlers (); app_init_poller_events (); g_ctx.polling = true; while (g_ctx.polling) poller_run (&g_ctx.poller); endwin (); g_log_message_real = log_message_stdio; app_free_context (); return 0; }