summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--.gitmodules6
-rw-r--r--LICENSE15
-rw-r--r--README.adoc79
-rw-r--r--cmake/FindNcursesw.cmake17
-rw-r--r--cmake/FindUnistring.cmake10
-rw-r--r--config.h.in10
m---------liberty0
-rw-r--r--mpd.c645
-rw-r--r--nncmpp.c1506
m---------termo0
11 files changed, 2297 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6954c64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Build files
+/build
+
+# Qt Creator files
+/CMakeLists.txt.user*
+/nncmpp.config
+/nncmpp.files
+/nncmpp.creator*
+/nncmpp.includes
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4acc2dd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,6 @@
+[submodule "termo"]
+ path = termo
+ url = git://github.com/pjanouch/termo.git
+[submodule "liberty"]
+ path = liberty
+ url = git://github.com/pjanouch/liberty.git
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ce263ae
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+ Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
+ 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.
+
diff --git a/README.adoc b/README.adoc
new file mode 100644
index 0000000..e956c4e
--- /dev/null
+++ b/README.adoc
@@ -0,0 +1,79 @@
+nncmpp
+======
+
+'nncmpp' is yet another MPD client. It does exactly what I want it to, more
+specifically it's a simplified TUI version of Sonata so that I don't need to
+run an ugly undeveloped Python application.
+
+If it's not obvious enough, the name a pun on all those ridiculous client names,
+and should be pronounced as "nincompoop".
+
+Currently it's under development and doesn't work in any sense yet.
+
+Packages
+--------
+Regular releases are sporadic. git master should be stable enough. You can get
+a package with the latest development version from Archlinux's AUR, or from
+openSUSE Build Service for the rest of mainstream distributions. Consult the
+list of repositories and their respective links at:
+
+https://build.opensuse.org/project/repositories/home:pjanouch:git
+
+Building and Running
+--------------------
+Build dependencies: CMake, pkg-config, liberty (included), termo (included) +
+Runtime dependencies: ncursesw, libunistring
+
+ $ git clone --recursive https://github.com/pjanouch/nncmpp.git
+ $ mkdir nncmpp/build
+ $ cd nncmpp/build
+ $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug
+ $ make
+
+To install the application, you can do either the usual:
+
+ # make install
+
+Or you can try telling CMake to make a package for you. For Debian it is:
+
+ $ cpack -G DEB
+ # dpkg -i nncmpp-*.deb
+
+Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with
+`fakeroot` or file ownership will end up wrong.
+
+Having the program installed, create a configuration file and run it.
+
+Configuration
+-------------
+Create _~/.config/nncmpp/nncmpp.conf_ with contents like the following:
+
+....
+settings = {
+ address = "localhost"
+ password = "<your password>"
+ root = "~/Music"
+}
+colors = {
+ header = "reverse"
+ header_active = "underline"
+ even = "16 231"
+ odd = "16 255"
+}
+....
+
+Contributing and Support
+------------------------
+Use this project's GitHub to report any bugs, request features, or submit pull
+requests. If you want to discuss this project, or maybe just hang out with
+the developer, feel free to join me at irc://irc.janouch.name, channel #dev.
+
+License
+-------
+'nncmpp' is written by Přemysl Janouch <p.janouch@gmail.com>.
+
+You may use the software under the terms of the ISC license, the text of which
+is included within the package, or, at your option, you may relicense the work
+under the MIT or the Modified BSD License, as listed at the following site:
+
+http://www.gnu.org/licenses/license-list.html
diff --git a/cmake/FindNcursesw.cmake b/cmake/FindNcursesw.cmake
new file mode 100644
index 0000000..88c1d01
--- /dev/null
+++ b/cmake/FindNcursesw.cmake
@@ -0,0 +1,17 @@
+# Public Domain
+
+find_package (PkgConfig REQUIRED)
+pkg_check_modules (NCURSESW QUIET ncursesw)
+
+# OpenBSD doesn't provide a pkg-config file
+set (required_vars NCURSESW_LIBRARIES)
+if (NOT NCURSESW_FOUND)
+ find_library (NCURSESW_LIBRARIES NAMES ncursesw)
+ find_path (NCURSESW_INCLUDE_DIRS ncurses.h)
+ list (APPEND required_vars NCURSESW_INCLUDE_DIRS)
+endif (NOT NCURSESW_FOUND)
+
+include (FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS (NCURSESW DEFAULT_MSG ${required_vars})
+
+mark_as_advanced (NCURSESW_LIBRARIES NCURSESW_INCLUDE_DIRS)
diff --git a/cmake/FindUnistring.cmake b/cmake/FindUnistring.cmake
new file mode 100644
index 0000000..6b74efb
--- /dev/null
+++ b/cmake/FindUnistring.cmake
@@ -0,0 +1,10 @@
+# Public Domain
+
+find_path (UNISTRING_INCLUDE_DIRS unistr.h)
+find_library (UNISTRING_LIBRARIES NAMES unistring libunistring)
+
+include (FindPackageHandleStandardArgs)
+FIND_PACKAGE_HANDLE_STANDARD_ARGS (UNISTRING DEFAULT_MSG
+ UNISTRING_INCLUDE_DIRS UNISTRING_LIBRARIES)
+
+mark_as_advanced (UNISTRING_LIBRARIES UNISTRING_INCLUDE_DIRS)
diff --git a/config.h.in b/config.h.in
new file mode 100644
index 0000000..b61ed66
--- /dev/null
+++ b/config.h.in
@@ -0,0 +1,10 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define PROGRAM_NAME "${CMAKE_PROJECT_NAME}"
+#define PROGRAM_VERSION "${project_VERSION}"
+
+#cmakedefine HAVE_RESIZETERM
+
+#endif // ! CONFIG_H
+
diff --git a/liberty b/liberty
new file mode 160000
+Subproject 952cf985dca6a97ee662f3b189788089abd2ef5
diff --git a/mpd.c b/mpd.c
new file mode 100644
index 0000000..f093691
--- /dev/null
+++ b/mpd.c
@@ -0,0 +1,645 @@
+// Copied from desktop-tools, should go to liberty if it proves useful
+
+// --- MPD client interface ----------------------------------------------------
+
+// This is a rather thin MPD client interface intended for basic tasks
+
+#define MPD_SUBSYSTEM_TABLE(XX) \
+ XX (DATABASE, 0, "database") \
+ XX (UPDATE, 1, "update") \
+ XX (STORED_PLAYLIST, 2, "stored_playlist") \
+ XX (PLAYLIST, 3, "playlist") \
+ XX (PLAYER, 4, "player") \
+ XX (MIXER, 5, "mixer") \
+ XX (OUTPUT, 6, "output") \
+ XX (OPTIONS, 7, "options") \
+ XX (STICKER, 8, "sticker") \
+ XX (SUBSCRIPTION, 9, "subscription") \
+ XX (MESSAGE, 10, "message")
+
+enum mpd_subsystem
+{
+#define XX(a, b, c) MPD_SUBSYSTEM_ ## a = (1 << b),
+ MPD_SUBSYSTEM_TABLE (XX)
+#undef XX
+ MPD_SUBSYSTEM_MAX
+};
+
+static const char *mpd_subsystem_names[] =
+{
+#define XX(a, b, c) [b] = c,
+ MPD_SUBSYSTEM_TABLE (XX)
+#undef XX
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum mpd_client_state
+{
+ MPD_DISCONNECTED, ///< Not connected
+ MPD_CONNECTING, ///< Currently connecting
+ MPD_CONNECTED ///< Connected
+};
+
+struct mpd_response
+{
+ bool success; ///< OK or ACK
+
+ // ACK-only fields:
+
+ int error; ///< Numeric error value (ack.h)
+ int list_offset; ///< Offset of command in list
+ char *current_command; ///< Name of the erroring command
+ char *message_text; ///< Error message
+};
+
+/// Task completion callback
+typedef void (*mpd_client_task_cb) (const struct mpd_response *response,
+ const struct str_vector *data, void *user_data);
+
+struct mpd_client_task
+{
+ LIST_HEADER (struct mpd_client_task)
+
+ mpd_client_task_cb callback; ///< Callback on completion
+ void *user_data; ///< User data
+};
+
+struct mpd_client
+{
+ struct poller *poller; ///< Poller
+
+ // Connection:
+
+ enum mpd_client_state state; ///< Connection state
+ struct connector *connector; ///< Connection establisher
+
+ int socket; ///< MPD socket
+ struct str read_buffer; ///< Input yet to be processed
+ struct str write_buffer; ///< Outut yet to be be sent out
+ struct poller_fd socket_event; ///< We can read from the socket
+
+ struct poller_timer timeout_timer; ///< Connection seems to be dead
+
+ // Protocol:
+
+ bool got_hello; ///< Got the OK MPD hello message
+
+ bool idling; ///< Sent idle as the last command
+ unsigned idling_subsystems; ///< Subsystems we're idling for
+ bool in_list; ///< We're inside a command list
+
+ struct mpd_client_task *tasks; ///< Task queue
+ struct mpd_client_task *tasks_tail; ///< Tail of task queue
+ struct str_vector data; ///< Data from last command
+
+ // User configuration:
+
+ void *user_data; ///< User data for callbacks
+
+ /// Callback after connection has been successfully established
+ void (*on_connected) (void *user_data);
+
+ /// Callback for general failures or even normal disconnection;
+ /// the interface is reinitialized
+ void (*on_failure) (void *user_data);
+
+ /// Callback to receive "idle" updates.
+ /// Remember to restart the idle if needed.
+ void (*on_event) (unsigned subsystems, void *user_data);
+};
+
+static void mpd_client_reset (struct mpd_client *self);
+static void mpd_client_destroy_connector (struct mpd_client *self);
+
+static void
+mpd_client_init (struct mpd_client *self, struct poller *poller)
+{
+ memset (self, 0, sizeof *self);
+
+ self->poller = poller;
+ self->socket = -1;
+
+ str_init (&self->read_buffer);
+ str_init (&self->write_buffer);
+
+ str_vector_init (&self->data);
+
+ poller_fd_init (&self->socket_event, poller, -1);
+ poller_timer_init (&self->timeout_timer, poller);
+}
+
+static void
+mpd_client_free (struct mpd_client *self)
+{
+ // So that we don't have to repeat most of the stuff
+ mpd_client_reset (self);
+
+ str_free (&self->read_buffer);
+ str_free (&self->write_buffer);
+
+ str_vector_free (&self->data);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Reinitialize the interface so that you can reconnect anew
+static void
+mpd_client_reset (struct mpd_client *self)
+{
+ if (self->state == MPD_CONNECTING)
+ mpd_client_destroy_connector (self);
+
+ if (self->socket != -1)
+ xclose (self->socket);
+ self->socket = -1;
+
+ self->socket_event.closed = true;
+ poller_fd_reset (&self->socket_event);
+ poller_timer_reset (&self->timeout_timer);
+
+ str_reset (&self->read_buffer);
+ str_reset (&self->write_buffer);
+
+ str_vector_reset (&self->data);
+
+ self->got_hello = false;
+ self->idling = false;
+ self->idling_subsystems = 0;
+ self->in_list = false;
+
+ LIST_FOR_EACH (struct mpd_client_task, iter, self->tasks)
+ free (iter);
+ self->tasks = self->tasks_tail = NULL;
+
+ self->state = MPD_DISCONNECTED;
+}
+
+static void
+mpd_client_fail (struct mpd_client *self)
+{
+ mpd_client_reset (self);
+ if (self->on_failure)
+ self->on_failure (self->user_data);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+mpd_client_parse_response (const char *p, struct mpd_response *response)
+{
+ if (!strcmp (p, "OK"))
+ return response->success = true;
+ if (!strcmp (p, "list_OK"))
+ // TODO: either implement this or fail the connection properly
+ hard_assert (!"command_list_ok_begin not implemented");
+
+ char *end = NULL;
+ if (*p++ != 'A' || *p++ != 'C' || *p++ != 'K' || *p++ != ' ' || *p++ != '[')
+ return false;
+
+ errno = 0;
+ response->error = strtoul (p, &end, 10);
+ if (errno != 0 || end == p)
+ return false;
+ p = end;
+ if (*p++ != '@')
+ return false;
+
+ errno = 0;
+ response->list_offset = strtoul (p, &end, 10);
+ if (errno != 0 || end == p)
+ return false;
+ p = end;
+ if (*p++ != ']' || *p++ != ' ' || *p++ != '{' || !(end = strchr (p, '}')))
+ return false;
+
+ response->current_command = xstrndup (p, end - p);
+ p = end + 1;
+
+ if (*p++ != ' ')
+ return false;
+
+ response->message_text = xstrdup (p);
+ response->success = false;
+ return true;
+}
+
+static void
+mpd_client_dispatch (struct mpd_client *self, struct mpd_response *response)
+{
+ struct mpd_client_task *task;
+ if (!(task = self->tasks))
+ return;
+
+ if (task->callback)
+ task->callback (response, &self->data, task->user_data);
+ str_vector_reset (&self->data);
+
+ LIST_UNLINK_WITH_TAIL (self->tasks, self->tasks_tail, task);
+ free (task);
+}
+
+static bool
+mpd_client_parse_hello (struct mpd_client *self, const char *line)
+{
+ const char hello[] = "OK MPD ";
+ if (strncmp (line, hello, sizeof hello - 1))
+ {
+ print_debug ("invalid MPD hello message");
+ return false;
+ }
+
+ // TODO: call "on_connected" now. We should however also set up a timer
+ // so that we don't wait on this message forever.
+ return self->got_hello = true;
+}
+
+static bool
+mpd_client_parse_line (struct mpd_client *self, const char *line)
+{
+ print_debug ("MPD >> %s", line);
+
+ if (!self->got_hello)
+ return mpd_client_parse_hello (self, line);
+
+ struct mpd_response response;
+ memset (&response, 0, sizeof response);
+ if (mpd_client_parse_response (line, &response))
+ {
+ mpd_client_dispatch (self, &response);
+ free (response.current_command);
+ free (response.message_text);
+ }
+ else
+ str_vector_add (&self->data, line);
+ return true;
+}
+
+/// All output from MPD commands seems to be in a trivial "key: value" format
+static char *
+mpd_client_parse_kv (char *line, char **value)
+{
+ char *sep;
+ if (!(sep = strstr (line, ": ")))
+ return NULL;
+
+ *sep = 0;
+ *value = sep + 2;
+ return line;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+mpd_client_update_poller (struct mpd_client *self)
+{
+ poller_fd_set (&self->socket_event,
+ self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN);
+}
+
+static bool
+mpd_client_process_input (struct mpd_client *self)
+{
+ // Split socket input at newlines and process them separately
+ struct str *rb = &self->read_buffer;
+ char *start = rb->str, *end = start + rb->len;
+ for (char *p = start; p < end; p++)
+ {
+ if (*p != '\n')
+ continue;
+
+ *p = 0;
+ if (!mpd_client_parse_line (self, start))
+ return false;
+ start = p + 1;
+ }
+
+ str_remove_slice (rb, 0, start - rb->str);
+ return true;
+}
+
+static void
+mpd_client_on_ready (const struct pollfd *pfd, void *user_data)
+{
+ (void) pfd;
+
+ struct mpd_client *self = user_data;
+ if (socket_io_try_read (self->socket, &self->read_buffer) != SOCKET_IO_OK
+ || !mpd_client_process_input (self)
+ || socket_io_try_write (self->socket, &self->write_buffer) != SOCKET_IO_OK)
+ mpd_client_fail (self);
+ else
+ mpd_client_update_poller (self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+mpd_client_must_quote_char (char c)
+{
+ return (unsigned char) c <= ' ' || c == '"' || c == '\'';
+}
+
+static bool
+mpd_client_must_quote (const char *s)
+{
+ if (!*s)
+ return true;
+ for (; *s; s++)
+ if (mpd_client_must_quote_char (*s))
+ return true;
+ return false;
+}
+
+static void
+mpd_client_quote (const char *s, struct str *output)
+{
+ str_append_c (output, '"');
+ for (; *s; s++)
+ {
+ if (mpd_client_must_quote_char (*s))
+ str_append_c (output, '\\');
+ str_append_c (output, *s);
+ }
+ str_append_c (output, '"');
+}
+
+/// Beware that delivery of the event isn't deferred and you musn't make
+/// changes to the interface while processing the event!
+static void
+mpd_client_add_task
+ (struct mpd_client *self, mpd_client_task_cb cb, void *user_data)
+{
+ // This only has meaning with command_list_ok_begin, and then it requires
+ // special handling (all in-list tasks need to be specially marked and
+ // later flushed if an early ACK or OK arrives).
+ hard_assert (!self->in_list);
+
+ struct mpd_client_task *task = xcalloc (1, sizeof *self);
+ task->callback = cb;
+ task->user_data = user_data;
+ LIST_APPEND_WITH_TAIL (self->tasks, self->tasks_tail, task);
+}
+
+/// Send a command. Remember to call mpd_client_add_task() to handle responses,
+/// unless the command is being sent in a list.
+static void mpd_client_send_command
+ (struct mpd_client *self, const char *command, ...) ATTRIBUTE_SENTINEL;
+
+static void
+mpd_client_send_commandv (struct mpd_client *self, char **commands)
+{
+ // Automatically interrupt idle mode
+ if (self->idling)
+ {
+ poller_timer_reset (&self->timeout_timer);
+
+ self->idling = false;
+ self->idling_subsystems = 0;
+ mpd_client_send_command (self, "noidle", NULL);
+ }
+
+ struct str line;
+ str_init (&line);
+
+ for (; *commands; commands++)
+ {
+ if (line.len)
+ str_append_c (&line, ' ');
+
+ if (mpd_client_must_quote (*commands))
+ mpd_client_quote (*commands, &line);
+ else
+ str_append (&line, *commands);
+ }
+
+ print_debug ("MPD << %s", line.str);
+ str_append_c (&line, '\n');
+ str_append_str (&self->write_buffer, &line);
+ str_free (&line);
+
+ mpd_client_update_poller (self);
+}
+
+static void
+mpd_client_send_command (struct mpd_client *self, const char *command, ...)
+{
+ struct str_vector v;
+ str_vector_init (&v);
+
+ va_list ap;
+ va_start (ap, command);
+ for (; command; command = va_arg (ap, const char *))
+ str_vector_add (&v, command);
+ va_end (ap);
+
+ mpd_client_send_commandv (self, v.vector);
+ str_vector_free (&v);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+mpd_client_list_begin (struct mpd_client *self)
+{
+ hard_assert (!self->in_list);
+ mpd_client_send_command (self, "command_list_begin", NULL);
+ self->in_list = true;
+}
+
+/// End a list of commands. Remember to call mpd_client_add_task()
+/// to handle the summary response.
+static void
+mpd_client_list_end (struct mpd_client *self)
+{
+ hard_assert (self->in_list);
+ mpd_client_send_command (self, "command_list_end", NULL);
+ self->in_list = false;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+mpd_resolve_subsystem (const char *name, unsigned *output)
+{
+ for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++)
+ if (!strcasecmp_ascii (name, mpd_subsystem_names[i]))
+ {
+ *output |= 1 << i;
+ return true;
+ }
+ return false;
+}
+
+static void
+mpd_client_on_idle_return (const struct mpd_response *response,
+ const struct str_vector *data, void *user_data)
+{
+ (void) response;
+
+ struct mpd_client *self = user_data;
+ unsigned subsystems = 0;
+ for (size_t i = 0; i < data->len; i++)
+ {
+ char *value, *key;
+ if (!(key = mpd_client_parse_kv (data->vector[i], &value)))
+ print_debug ("%s: %s", "erroneous MPD output", data->vector[i]);
+ else if (strcasecmp_ascii (key, "changed"))
+ print_debug ("%s: %s", "unexpected idle key", key);
+ else if (!mpd_resolve_subsystem (value, &subsystems))
+ print_debug ("%s: %s", "unknown subsystem", value);
+ }
+
+ // Not resetting "idling" here, we may send an extra "noidle" no problem
+ if (self->on_event && subsystems)
+ self->on_event (subsystems, self->user_data);
+}
+
+static void mpd_client_idle (struct mpd_client *self, unsigned subsystems);
+
+static void
+mpd_client_on_timeout (void *user_data)
+{
+ struct mpd_client *self = user_data;
+ unsigned subsystems = self->idling_subsystems;
+
+ // Just sending this out should bring a dead connection down over TCP
+ // TODO: set another timer to make sure the ping reply arrives
+ mpd_client_send_command (self, "ping", NULL);
+ mpd_client_add_task (self, NULL, NULL);
+
+ // Restore the incriminating idle immediately
+ mpd_client_idle (self, subsystems);
+}
+
+/// When not expecting to send any further commands, you should call this
+/// in order to keep the connection alive. Or to receive updates.
+static void
+mpd_client_idle (struct mpd_client *self, unsigned subsystems)
+{
+ hard_assert (!self->in_list);
+
+ struct str_vector v;
+ str_vector_init (&v);
+
+ str_vector_add (&v, "idle");
+ for (size_t i = 0; i < N_ELEMENTS (mpd_subsystem_names); i++)
+ if (subsystems & (1 << i))
+ str_vector_add (&v, mpd_subsystem_names[i]);
+
+ mpd_client_send_commandv (self, v.vector);
+ str_vector_free (&v);
+
+ self->timeout_timer.dispatcher = mpd_client_on_timeout;
+ self->timeout_timer.user_data = self;
+ poller_timer_set (&self->timeout_timer, 5 * 60 * 1000);
+
+ mpd_client_add_task (self, mpd_client_on_idle_return, self);
+ self->idling = true;
+ self->idling_subsystems = subsystems;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static void
+mpd_client_finish_connection (struct mpd_client *self, int socket)
+{
+ set_blocking (socket, false);
+ self->socket = socket;
+ self->state = MPD_CONNECTED;
+
+ poller_fd_init (&self->socket_event, self->poller, self->socket);
+ self->socket_event.dispatcher = mpd_client_on_ready;
+ self->socket_event.user_data = self;
+
+ mpd_client_update_poller (self);
+
+ if (self->on_connected)
+ self->on_connected (self->user_data);
+}
+
+static void
+mpd_client_destroy_connector (struct mpd_client *self)
+{
+ if (self->connector)
+ connector_free (self->connector);
+ free (self->connector);
+ self->connector = NULL;
+
+ // Not connecting anymore
+ self->state = MPD_DISCONNECTED;
+}
+
+static void
+mpd_client_on_connector_failure (void *user_data)
+{
+ struct mpd_client *self = user_data;
+ mpd_client_destroy_connector (self);
+ mpd_client_fail (self);
+}
+
+static void
+mpd_client_on_connector_connected
+ (void *user_data, int socket, const char *host)
+{
+ (void) host;
+
+ struct mpd_client *self = user_data;
+ mpd_client_destroy_connector (self);
+ mpd_client_finish_connection (self, socket);
+}
+
+static bool
+mpd_client_connect_unix (struct mpd_client *self, const char *address,
+ struct error **e)
+{
+ int fd = socket (AF_UNIX, SOCK_STREAM, 0);
+ if (fd == -1)
+ {
+ error_set (e, "%s: %s", "socket", strerror (errno));
+ return false;
+ }
+
+ // Expand tilde if needed
+ char *expanded = resolve_filename (address, xstrdup);
+
+ struct sockaddr_un sun;
+ sun.sun_family = AF_UNIX;
+ strncpy (sun.sun_path, expanded, sizeof sun.sun_path);
+ sun.sun_path[sizeof sun.sun_path - 1] = 0;
+
+ free (expanded);
+
+ if (connect (fd, (struct sockaddr *) &sun, sizeof sun))
+ {
+ error_set (e, "%s: %s", "connect", strerror (errno));
+ return false;
+ }
+
+ mpd_client_finish_connection (self, fd);
+ return true;
+}
+
+static bool
+mpd_client_connect (struct mpd_client *self, const char *address,
+ const char *service, struct error **e)
+{
+ hard_assert (self->state == MPD_DISCONNECTED);
+
+ // If it looks like a path, assume it's a UNIX socket
+ if (strchr (address, '/'))
+ return mpd_client_connect_unix (self, address, e);
+
+ struct connector *connector = xmalloc (sizeof *connector);
+ connector_init (connector, self->poller);
+ self->connector = connector;
+
+ connector->user_data = self;
+ connector->on_connected = mpd_client_on_connector_connected;
+ connector->on_failure = mpd_client_on_connector_failure;
+
+ connector_add_target (connector, address, service);
+ self->state = MPD_CONNECTING;
+ return true;
+}
diff --git a/nncmpp.c b/nncmpp.c
new file mode 100644
index 0000000..27c175b
--- /dev/null
+++ b/nncmpp.c
@@ -0,0 +1,1506 @@
+/*
+ * nncmpp -- the MPD client you never knew you needed
+ *
+ * Copyright (c) 2016, Přemysl Janouch <p.janouch@gmail.com>
+ * 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 <sys/un.h>
+#include "mpd.c"
+
+#include <locale.h>
+#include <termios.h>
+#ifndef TIOCGWINSZ
+#include <sys/ioctl.h>
+#endif // ! TIOCGWINSZ
+#include <ncurses.h>
+
+// 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 <unistr.h>
+#include <uniwidth.h>
+#include <uniconv.h>
+
+#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;
+}
+
diff --git a/termo b/termo
new file mode 160000
+Subproject 4282f3715c7d4307f57c27edf66874762bdee85