From a91e24930cec67804db411b31a0a6eee411077eb Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sun, 10 Jan 2016 02:22:43 +0100 Subject: Initial commit These used to be part of my dwm fork repository. --- .gitignore | 9 + .gitmodules | 3 + CMakeLists.txt | 67 ++ LICENSE | 14 + README.adoc | 49 + brightness.c | 445 +++++++++ config.h.in | 8 + dwmstatus.c | 2742 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ liberty | 1 + 9 files changed, 3338 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.adoc create mode 100644 brightness.c create mode 100644 config.h.in create mode 100644 dwmstatus.c create mode 160000 liberty diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90b12a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Build files +/build + +# Qt Creator files +/CMakeLists.txt.user* +/desktop-tools.config +/desktop-tools.files +/desktop-tools.creator* +/desktop-tools.includes diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5f55ff8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "liberty"] + path = liberty + url = https://github.com/pjanouch/liberty.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7f6614b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,67 @@ +project (desktop-tools C) +cmake_minimum_required (VERSION 2.8.11) + +# Moar warnings +if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + # -Wunused-function is pretty annoying here, as everything is static + set (CMAKE_C_FLAGS "-std=c99 -Wall -Wextra -Wno-unused-function") +endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUC) + +# Version +set (project_VERSION_MAJOR "0") +set (project_VERSION_MINOR "1") +set (project_VERSION_PATCH "0") + +set (project_VERSION "${project_VERSION_MAJOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_MINOR}") +set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}") + +# For custom modules +set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) + +# Dependencies +set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/liberty/cmake) +include (AddThreads) + +find_package (PkgConfig REQUIRED) +pkg_check_modules (dependencies REQUIRED libpulse x11) + +set (project_libraries ${dependencies_LIBRARIES}) +include_directories (${dependencies_INCLUDE_DIRS}) + +# Generate a configuration file +configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h) +include_directories (${PROJECT_BINARY_DIR}) + +# Build +add_executable (dwmstatus dwmstatus.c) +target_link_libraries (dwmstatus ${project_libraries}) +add_threads (dwmstatus) + +add_executable (brightness brightness.c) +target_link_libraries (brightness ${project_libraries}) +add_threads (brightness) + +# The files to be installed +include (GNUInstallDirs) +install (TARGETS dwmstatus brightness DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR}) + +# CPack +set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Desktop tools") +set (CPACK_PACKAGE_VENDOR "Premysl Janouch") +set (CPACK_PACKAGE_CONTACT "Přemysl Janouch ") +set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") +set (CPACK_PACKAGE_VERSION_MAJOR ${project_VERSION_MAJOR}) +set (CPACK_PACKAGE_VERSION_MINOR ${project_VERSION_MINOR}) +set (CPACK_PACKAGE_VERSION_PATCH ${project_VERSION_PATCH}) +set (CPACK_GENERATOR "TGZ;ZIP") +set (CPACK_PACKAGE_FILE_NAME + "${PROJECT_NAME}-${project_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") +set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${project_VERSION}") +set (CPACK_SOURCE_GENERATOR "TGZ;ZIP") +set (CPACK_SOURCE_IGNORE_FILES "/\\\\.git;/build;/CMakeLists.txt.user") +set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${project_VERSION}") + +set (CPACK_SET_DESTDIR TRUE) +include (CPack) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc6385c --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ + Copyright (c) 2015 - 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. diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..b0f884b --- /dev/null +++ b/README.adoc @@ -0,0 +1,49 @@ +desktop-tools +============= +:compact-option: + +'desktop-tools' is a collection of tools to run my desktop that might be useful +to other people as well. + + - 'dwmstatus' does literally everything my dwm doesn't but I'd like it to. It + includes PulseAudio volume management and hand-written NUT and MPD clients, + all in the name of liberation from GPL-licensed software of course + - 'brightness' allows me to change the brightness of w/e display device I have. + +Don't expect them to work under any OS that isn't Linux. + +Building and Running +-------------------- +Build dependencies: CMake, pkg-config, liberty (included) + +Runtime dependencies: libpulse, libx11 + + $ git clone --recursive https://github.com/pjanouch/desktop-tools.git + $ mkdir desktop-tools/build + $ cd desktop-tools/build + $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug + $ make + +To install the applications, 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 desktop-tools-*.deb + +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://anathema.irc.so, channel #anathema. + +License +------- +'desktop-tools' is written by Přemysl Janouch . + +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/brightness.c b/brightness.c new file mode 100644 index 0000000..f30a0c9 --- /dev/null +++ b/brightness.c @@ -0,0 +1,445 @@ +/* + * brightness.c: set display brightness via DDC/CI - Linux only + * + * Copyright (c) 2015, Přemysl Janouch + * + * 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. + * + */ + +// Sources: ddcciv1r1.pdf, i2c-dev.c in Linux, ddccontrol source code, +// http://www.boichat.ch/nicolas/ddcci/specs.html was also helpful + +// This makes openat() available even though I set _POSIX_C_SOURCE and +// _XOPEN_SOURCE to a version of POSIX older than 2008 +#define _GNU_SOURCE + +// Undo some dwm Makefile damage and import my everything-library +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "brightness" +#include "liberty/liberty.c" + +#include +#include + +#include +#include + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +log_message_custom (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + (void) user_data; + FILE *stream = stdout; + + fprintf (stream, PROGRAM_NAME ": "); + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +#define FAIL(...) \ + BLOCK_START \ + error_set (e, __VA_ARGS__); \ + return false; \ + BLOCK_END + +static void +wait_ms (long ms) +{ + struct timespec ts = { 0, ms * 1000 * 1000 }; + nanosleep (&ts, NULL); +} + +static bool +xstrtol (const char *s, long *out) +{ + char *end; + errno = 0; + *out = strtol (s, &end, 10); + return errno == 0 && !*end && end != s; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define DDC_LENGTH_XOR 0x80 + +enum +{ + DDC_ADDRESS_HOST = 0x50, ///< Bus master's base address + DDC_ADDRESS_DISPLAY = 0x6E ///< The display's base address +}; + +enum { I2C_WRITE, I2C_READ }; + +enum +{ + DDC_GET_VCP_FEATURE = 0x01, ///< Request info about a feature + DDC_GET_VCP_FEATURE_REPLY = 0x02, ///< Feature info response + DDC_SET_VCP_FEATURE = 0x03 ///< Set or activate a feature +}; + +enum +{ + VCP_BRIGHTNESS = 0x10, ///< Standard VCP opcode for brightness + VCP_CONTRAST = 0x12 ///< Standard VCP opcode for contrast +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +check_edid (int fd, struct error **e) +{ + uint8_t edid_req[] = { 0x00 }; + uint8_t buf[8] = ""; + struct i2c_msg bufs[] = + { + { .addr = 0x50, .flags = 0, + .len = 1, .buf = edid_req }, + { .addr = 0x50, .flags = I2C_M_RD, + .len = sizeof buf, .buf = buf }, + }; + + struct i2c_rdwr_ioctl_data data; + data.msgs = bufs; + data.nmsgs = 2; + + if (ioctl (fd, I2C_RDWR, &data) < 0) + FAIL ("%s: %s", "ioctl", strerror (errno)); + if (memcmp ("\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", buf, sizeof buf)) + FAIL ("invalid EDID"); + return true; +} + +static bool +is_a_display (int fd, struct error **e) +{ + struct stat st; + if (fstat (fd, &st) < 0) + FAIL ("%s: %s", "fstat", strerror (errno)); + + unsigned long funcs; + if (!(st.st_mode & S_IFCHR) + || ioctl (fd, I2C_FUNCS, &funcs) < 0 + || !(funcs & I2C_FUNC_I2C)) + FAIL ("not an I2C device"); + + return check_edid (fd, e); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +ddc_send (int fd, unsigned command, void *args, size_t args_len, + struct error **e) +{ + struct str buf; + str_init (&buf); + str_pack_u8 (&buf, DDC_ADDRESS_HOST | I2C_READ); + str_pack_u8 (&buf, DDC_LENGTH_XOR | (args_len + 1)); + str_pack_u8 (&buf, command); + str_append_data (&buf, args, args_len); + + unsigned xor = DDC_ADDRESS_DISPLAY; + for (size_t i = 0; i < buf.len; i++) + xor ^= buf.str[i]; + str_pack_u8 (&buf, xor); + + struct i2c_msg msg = + { + // The driver unshifts it back + .addr = DDC_ADDRESS_DISPLAY >> 1, .flags = 0, + .len = buf.len, .buf = (uint8_t *) buf.str, + }; + + struct i2c_rdwr_ioctl_data data; + data.msgs = &msg; + data.nmsgs = 1; + + bool failed = ioctl (fd, I2C_RDWR, &data) < 0; + str_free (&buf); + if (failed) + FAIL ("%s: %s", "ioctl", strerror (errno)); + return true; +} + +static bool +ddc_read (int fd, unsigned *command, void *out_buf, size_t *n_read, + struct error **e) +{ + uint8_t buf[128] = ""; + struct i2c_msg msg = + { + // The driver unshifts it back + .addr = DDC_ADDRESS_DISPLAY >> 1, .flags = I2C_M_RD, + .len = sizeof buf, .buf = buf, + }; + + struct i2c_rdwr_ioctl_data data; + data.msgs = &msg; + data.nmsgs = 1; + + if (ioctl (fd, I2C_RDWR, &data) < 0) + FAIL ("%s: %s", "ioctl", strerror (errno)); + + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, buf, sizeof buf); + + uint8_t sender, length, cmd; + (void) msg_unpacker_u8 (&unpacker, &sender); + (void) msg_unpacker_u8 (&unpacker, &length); + (void) msg_unpacker_u8 (&unpacker, &cmd); + + if (sender != (DDC_ADDRESS_DISPLAY | I2C_WRITE) || !(length & 0x80)) + FAIL ("invalid response"); + if (!(length ^= 0x80)) + FAIL ("NULL response"); + + // TODO: also check the checksum + + *command = cmd; + memcpy (out_buf, unpacker.data + unpacker.offset, (*n_read = length - 1)); + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +set_brightness (int fd, long diff, struct error **e) +{ + uint8_t get_req[] = { VCP_BRIGHTNESS }; + if (!ddc_send (fd, DDC_GET_VCP_FEATURE, get_req, sizeof get_req, e)) + return false; + + wait_ms (40); + + unsigned command = 0; + uint8_t buf[128] = ""; + size_t len = 0; + if (!ddc_read (fd, &command, buf, &len, e)) + return false; + + if (command != DDC_GET_VCP_FEATURE_REPLY || len != 7) + FAIL ("invalid response"); + + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, buf, len); + + uint8_t result; msg_unpacker_u8 (&unpacker, &result); + uint8_t vcp_opcode; msg_unpacker_u8 (&unpacker, &vcp_opcode); + uint8_t type; msg_unpacker_u8 (&unpacker, &type); + int16_t max; msg_unpacker_i16 (&unpacker, &max); + int16_t cur; msg_unpacker_i16 (&unpacker, &cur); + + if (result == 0x01) + FAIL ("error reported by monitor"); + + if (result != 0x00 + || vcp_opcode != VCP_BRIGHTNESS) + FAIL ("invalid response"); + + // These are unsigned but usually just one byte long + if (max < 0 || cur < 0) + FAIL ("capability range overflow"); + + int16_t req = (cur * 100 + diff * max + 50) / 100; + if (req > max) req = max; + if (req < 0) req = 0; + + uint8_t set_req[] = { VCP_BRIGHTNESS, req >> 8, req }; + if (!ddc_send (fd, DDC_SET_VCP_FEATURE, set_req, sizeof set_req, e)) + return false; + + wait_ms (50); + + printf ("brightness set to %.2f%%\n", 100. * req / max); + return true; +} + +static void +i2c (long diff) +{ + DIR *dev = opendir ("/dev"); + if (!dev) + { + print_error ("cannot access %s: %s: %s", + "/dev", "opendir", strerror (errno)); + return; + } + + struct dirent *entry; + while ((entry = readdir (dev))) + { + if (strncmp (entry->d_name, "i2c-", 4)) + continue; + + printf ("Trying %s... ", entry->d_name); + int fd = openat (dirfd (dev), entry->d_name, O_RDONLY); + if (fd < 0) + { + print_error ("%s: %s", "openat", strerror (errno)); + continue; + } + + struct error *e = NULL; + if (!is_a_display (fd, &e) + || !set_brightness (fd, diff, &e)) + { + printf ("%s\n", e->message); + error_free (e); + } + + close (fd); + } + closedir (dev); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static long +read_value (int dir, const char *filename, struct error **e) +{ + int fd = openat (dir, filename, O_RDONLY); + if (fd < 0) + { + error_set (e, "%s: %s: %s", filename, "openat", strerror (errno)); + return -1; + } + + FILE *fp = fdopen (fd, "r"); + if (!fp) + { + error_set (e, "%s: %s: %s", filename, "fdopen", strerror (errno)); + close (fd); + return -1; + } + + struct str s; + str_init (&s); + + long value; + if (!read_line (fp, &s) + || !xstrtol (s.str, &value)) + { + value = -1; + error_set (e, "%s: %s", filename, "failed reading an integer value"); + } + + str_free (&s); + fclose (fp); + return value; +} + +static bool +set_backlight (int dir, long diff, struct error **e) +{ + long cur, max; + struct error *error = NULL; + if ((cur = read_value (dir, "brightness", &error), error) + || (max = read_value (dir, "max_brightness", &error), error)) + { + error_propagate (e, error); + return false; + } + + if (cur < 0 || max < 0) + { + error_set (e, "invalid range or current value"); + return false; + } + + long req = (cur * 100 + diff * max + 50) / 100; + if (req > max) req = max; + if (req < 0) req = 0; + + int fd = openat (dir, "brightness", O_WRONLY); + if (fd < 0) + { + error_set (e, "%s: %s: %s", "brightness", "openat", strerror (errno)); + return false; + } + + struct str s; + str_init (&s); + str_append_printf (&s, "%ld", req); + bool result = write (fd, s.str, s.len) == s.len; + str_free (&s); + + if (!result) + error_set (e, "%s: %s: %s", "brightness", "write", strerror (errno)); + + close (fd); + printf ("brightness set to %.2f%%\n", 100. * req / max); + return result; +} + +static void +backlight (long diff) +{ + DIR *backlight = opendir ("/sys/class/backlight"); + if (!backlight) + { + print_error ("cannot access %s: %s: %s", + "/sys/class/backlight", "opendir", strerror (errno)); + return; + } + + struct dirent *entry; + while ((entry = readdir (backlight))) + { + const char *device_name = entry->d_name; + if (device_name[0] == '.') + continue; + + printf ("Trying %s... ", entry->d_name); + int dir = openat (dirfd (backlight), entry->d_name, O_RDONLY); + if (dir < 0) + { + print_error ("%s: %s", "openat", strerror (errno)); + continue; + } + + struct error *e = NULL; + if (!set_backlight (dir, diff, &e)) + { + printf ("%s\n", e->message); + error_free (e); + } + + close (dir); + } + closedir (backlight); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +int +main (int argc, char *argv[]) +{ + g_log_message_real = log_message_custom; + + long diff = 0; + if (argc > 1 && !xstrtol (argv[1], &diff)) + { + printf ("Usage: %s \n", argv[0]); + exit (EXIT_FAILURE); + } + + i2c (diff); + backlight (diff); + return 0; +} + diff --git a/config.h.in b/config.h.in new file mode 100644 index 0000000..89cd306 --- /dev/null +++ b/config.h.in @@ -0,0 +1,8 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#define PROGRAM_NAME "${PROJECT_NAME}" +#define PROGRAM_VERSION "${project_VERSION}" + +#endif // ! CONFIG_H + diff --git a/dwmstatus.c b/dwmstatus.c new file mode 100644 index 0000000..e3e3a29 --- /dev/null +++ b/dwmstatus.c @@ -0,0 +1,2742 @@ +/* + * dwmstatus.c: simple PulseAudio-enabled dwmstatus + * + * Copyright (c) 2015, Přemysl Janouch + * + * 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. + * + */ + +#define LIBERTY_WANT_POLLER +#define LIBERTY_WANT_ASYNC + +#define _GNU_SOURCE // openat + +#include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "dwmstatus" +#include "liberty/liberty.c" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +// --- Utilities --------------------------------------------------------------- + +static void +log_message_custom (void *user_data, const char *quote, const char *fmt, + va_list ap) +{ + (void) user_data; + FILE *stream = stdout; + + fprintf (stream, PROGRAM_NAME ": "); + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +static void +set_dwm_status (Display *dpy, const char *str) +{ + print_debug ("setting status to: %s", str); + XStoreName (dpy, DefaultRootWindow (dpy), str); + XSync (dpy, False); +} + +// --- Simple network I/O ------------------------------------------------------ + +enum socket_io_result +{ + SOCKET_IO_OK = 0, ///< Completed successfully + SOCKET_IO_EOF, ///< Connection shut down by peer + SOCKET_IO_ERROR ///< Connection error +}; + +static enum socket_io_result +socket_io_try_read (int socket_fd, struct str *rb) +{ + ssize_t n_read; + while (true) + { + str_ensure_space (rb, 512); + n_read = recv (socket_fd, rb->str + rb->len, + rb->alloc - rb->len - 1 /* null byte */, 0); + + if (n_read > 0) + { + rb->str[rb->len += n_read] = '\0'; + continue; + } + if (n_read == 0) + return SOCKET_IO_EOF; + + if (errno == EAGAIN) + return SOCKET_IO_OK; + if (errno == EINTR) + continue; + + LOG_LIBC_FAILURE ("recv"); + return SOCKET_IO_ERROR; + } +} + +static enum socket_io_result +socket_io_try_write (int socket_fd, struct str *wb) +{ + ssize_t n_written; + while (wb->len) + { + n_written = send (socket_fd, wb->str, wb->len, 0); + if (n_written >= 0) + { + str_remove_slice (wb, 0, n_written); + continue; + } + + if (errno == EAGAIN) + return SOCKET_IO_OK; + if (errno == EINTR) + continue; + + LOG_LIBC_FAILURE ("send"); + return SOCKET_IO_ERROR; + } + return SOCKET_IO_OK; +} + +// --- PulseAudio mainloop abstraction ----------------------------------------- + +struct pa_io_event +{ + LIST_HEADER (pa_io_event) + + pa_mainloop_api *api; ///< Parent structure + struct poller_fd fd; ///< Underlying FD event + + pa_io_event_cb_t dispatch; ///< Dispatcher + pa_io_event_destroy_cb_t free; ///< Destroyer + void *user_data; ///< User data +}; + +struct pa_time_event +{ + LIST_HEADER (pa_time_event) + + pa_mainloop_api *api; ///< Parent structure + struct poller_timer timer; ///< Underlying timer event + + pa_time_event_cb_t dispatch; ///< Dispatcher + pa_time_event_destroy_cb_t free; ///< Destroyer + void *user_data; ///< User data +}; + +struct pa_defer_event +{ + LIST_HEADER (pa_defer_event) + + pa_mainloop_api *api; ///< Parent structure + struct poller_idle idle; ///< Underlying idle event + + pa_defer_event_cb_t dispatch; ///< Dispatcher + pa_defer_event_destroy_cb_t free; ///< Destroyer + void *user_data; ///< User data +}; + +struct poller_pa +{ + struct poller *poller; ///< The underlying event loop + int result; ///< Result on quit + bool running; ///< Not quitting + + pa_io_event *io_list; ///< I/O events + pa_time_event *time_list; ///< Timer events + pa_defer_event *defer_list; ///< Deferred events +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static short +poller_pa_flags_to_events (pa_io_event_flags_t flags) +{ + short result = 0; + if (flags & PA_IO_EVENT_ERROR) result |= POLLERR; + if (flags & PA_IO_EVENT_HANGUP) result |= POLLHUP; + if (flags & PA_IO_EVENT_INPUT) result |= POLLIN; + if (flags & PA_IO_EVENT_OUTPUT) result |= POLLOUT; + return result; +} + +static pa_io_event_flags_t +poller_pa_events_to_flags (short events) +{ + pa_io_event_flags_t result = 0; + if (events & POLLERR) result |= PA_IO_EVENT_ERROR; + if (events & POLLHUP) result |= PA_IO_EVENT_HANGUP; + if (events & POLLIN) result |= PA_IO_EVENT_INPUT; + if (events & POLLOUT) result |= PA_IO_EVENT_OUTPUT; + return result; +} + +static struct timeval +poller_pa_get_current_time (void) +{ + struct timeval tv; +#ifdef _POSIX_TIMERS + struct timespec tp; + hard_assert (clock_gettime (CLOCK_REALTIME, &tp) != -1); + tv.tv_sec = tp.tv_sec; + tv.tv_usec = tp.tv_nsec / 1000; +#else + gettimeofday (&tv, NULL); +#endif + return tv; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +poller_pa_io_dispatcher (const struct pollfd *pfd, void *user_data) +{ + pa_io_event *self = user_data; + self->dispatch (self->api, self, + pfd->fd, poller_pa_events_to_flags (pfd->revents), self->user_data); +} + +static void +poller_pa_io_enable (pa_io_event *self, pa_io_event_flags_t events) +{ + struct poller_fd *fd = &self->fd; + if (events) + poller_fd_set (fd, poller_pa_flags_to_events (events)); + else + poller_fd_reset (fd); +} + +static pa_io_event * +poller_pa_io_new (pa_mainloop_api *api, int fd_, pa_io_event_flags_t events, + pa_io_event_cb_t cb, void *userdata) +{ + pa_io_event *self = xcalloc (1, sizeof *self); + self->api = api; + self->dispatch = cb; + self->user_data = userdata; + + struct poller_pa *data = api->userdata; + struct poller_fd *fd = &self->fd; + poller_fd_init (fd, data->poller, fd_); + fd->user_data = self; + fd->dispatcher = poller_pa_io_dispatcher; + + poller_pa_io_enable (self, events); + LIST_PREPEND (data->io_list, self); + return self; +} + +static void +poller_pa_io_free (pa_io_event *self) +{ + if (self->free) + self->free (self->api, self, self->user_data); + + struct poller_pa *data = self->api->userdata; + poller_fd_reset (&self->fd); + LIST_UNLINK (data->io_list, self); + free (self); +} + +static void +poller_pa_io_set_destroy (pa_io_event *self, pa_io_event_destroy_cb_t cb) +{ + self->free = cb; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +poller_pa_time_dispatcher (void *user_data) +{ + pa_time_event *self = user_data; + // XXX: the meaning of the time argument is undocumented, + // so let's just put current Unix time in there + struct timeval now = poller_pa_get_current_time (); + self->dispatch (self->api, self, &now, self->user_data); +} + +static void +poller_pa_time_restart (pa_time_event *self, const struct timeval *tv) +{ + struct poller_timer *timer = &self->timer; + if (tv) + { + struct timeval now = poller_pa_get_current_time (); + poller_timer_set (timer, + (tv->tv_sec - now.tv_sec) * 1000 + + (tv->tv_usec - now.tv_usec) / 1000); + } + else + poller_timer_reset (timer); +} + +static pa_time_event * +poller_pa_time_new (pa_mainloop_api *api, const struct timeval *tv, + pa_time_event_cb_t cb, void *userdata) +{ + pa_time_event *self = xcalloc (1, sizeof *self); + self->api = api; + self->dispatch = cb; + self->user_data = userdata; + + struct poller_pa *data = api->userdata; + struct poller_timer *timer = &self->timer; + poller_timer_init (timer, data->poller); + timer->user_data = self; + timer->dispatcher = poller_pa_time_dispatcher; + + poller_pa_time_restart (self, tv); + LIST_PREPEND (data->time_list, self); + return self; +} + +static void +poller_pa_time_free (pa_time_event *self) +{ + if (self->free) + self->free (self->api, self, self->user_data); + + struct poller_pa *data = self->api->userdata; + poller_timer_reset (&self->timer); + LIST_UNLINK (data->time_list, self); + free (self); +} + +static void +poller_pa_time_set_destroy (pa_time_event *self, pa_time_event_destroy_cb_t cb) +{ + self->free = cb; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +poller_pa_defer_dispatcher (void *user_data) +{ + pa_defer_event *self = user_data; + self->dispatch (self->api, self, self->user_data); +} + +static pa_defer_event * +poller_pa_defer_new (pa_mainloop_api *api, + pa_defer_event_cb_t cb, void *userdata) +{ + pa_defer_event *self = xcalloc (1, sizeof *self); + self->api = api; + self->dispatch = cb; + self->user_data = userdata; + + struct poller_pa *data = api->userdata; + struct poller_idle *idle = &self->idle; + poller_idle_init (idle, data->poller); + idle->user_data = self; + idle->dispatcher = poller_pa_defer_dispatcher; + + poller_idle_set (idle); + LIST_PREPEND (data->defer_list, self); + return self; +} + +static void +poller_pa_defer_enable (pa_defer_event *self, int enable) +{ + struct poller_idle *idle = &self->idle; + if (enable) + poller_idle_set (idle); + else + poller_idle_reset (idle); +} + +static void +poller_pa_defer_free (pa_defer_event *self) +{ + if (self->free) + self->free (self->api, self, self->user_data); + + struct poller_pa *data = self->api->userdata; + poller_idle_reset (&self->idle); + LIST_UNLINK (data->defer_list, self); + free (self); +} + +static void +poller_pa_defer_set_destroy (pa_defer_event *self, + pa_defer_event_destroy_cb_t cb) +{ + self->free = cb; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +poller_pa_quit (pa_mainloop_api *api, int retval) +{ + struct poller_pa *data = api->userdata; + data->result = retval; + data->running = false; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct pa_mainloop_api g_poller_pa_template = +{ + .io_new = poller_pa_io_new, + .io_enable = poller_pa_io_enable, + .io_free = poller_pa_io_free, + .io_set_destroy = poller_pa_io_set_destroy, + + .time_new = poller_pa_time_new, + .time_restart = poller_pa_time_restart, + .time_free = poller_pa_time_free, + .time_set_destroy = poller_pa_time_set_destroy, + + .defer_new = poller_pa_defer_new, + .defer_enable = poller_pa_defer_enable, + .defer_free = poller_pa_defer_free, + .defer_set_destroy = poller_pa_defer_set_destroy, + + .quit = poller_pa_quit, +}; + +static struct pa_mainloop_api * +poller_pa_new (struct poller *self) +{ + struct poller_pa *data = xcalloc (1, sizeof *data); + data->poller = self; + + struct pa_mainloop_api *api = xmalloc (sizeof *api); + *api = g_poller_pa_template; + api->userdata = data; + return api; +} + +static void +poller_pa_destroy (struct pa_mainloop_api *api) +{ + struct poller_pa *data = api->userdata; + + LIST_FOR_EACH (pa_io_event, iter, data->io_list) + poller_pa_io_free (iter); + LIST_FOR_EACH (pa_time_event, iter, data->time_list) + poller_pa_time_free (iter); + LIST_FOR_EACH (pa_defer_event, iter, data->defer_list) + poller_pa_defer_free (iter); + + free (data); + free (api); +} + +/// Since our poller API doesn't care much about continuous operation, +/// we need to provide that in the PulseAudio abstraction itself +static int +poller_pa_run (struct pa_mainloop_api *api) +{ + struct poller_pa *data = api->userdata; + data->running = true; + while (data->running) + poller_run (data->poller); + return data->result; +} + +// --- 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; +} + +// --- NUT --------------------------------------------------------------------- + +// More or less copied and pasted from the MPD client. This code doesn't even +// deserve much love, the protocol is somehow even worse than MPD's. +// +// http://www.networkupstools.org/docs/developer-guide.chunked/ar01s09.html + +// This was written by loosely following the top comment in NUT's parseconf.c. + +enum nut_parser_state +{ + NUT_STATE_START_LINE, ///< Start of a line + NUT_STATE_BETWEEN, ///< Between words, expecting non-WS + NUT_STATE_UNQUOTED, ///< Within unquoted word + NUT_STATE_UNQUOTED_ESCAPE, ///< Dtto after a backslash + NUT_STATE_QUOTED, ///< Within a quoted word + NUT_STATE_QUOTED_ESCAPE, ///< Dtto after a backslash + NUT_STATE_QUOTED_END ///< End of word, expecting WS +}; + +struct nut_parser +{ + enum nut_parser_state state; ///< Parser state + struct str current_field; ///< Current field + + // Public: + + struct str_vector fields; ///< Line fields +}; + +static void +nut_parser_init (struct nut_parser *self) +{ + self->state = NUT_STATE_START_LINE; + str_init (&self->current_field); + str_vector_init (&self->fields); +} + +static void +nut_parser_free (struct nut_parser *self) +{ + str_free (&self->current_field); + str_vector_free (&self->fields); +} + +static int +nut_parser_end_field (struct nut_parser *self, char c) +{ + str_vector_add (&self->fields, self->current_field.str); + str_reset (&self->current_field); + + if (c == '\n') + { + self->state = NUT_STATE_START_LINE; + return 1; + } + + self->state = NUT_STATE_BETWEEN; + return 0; +} + +/// Returns 1 if a complete line has been read, -1 on error, 0 otherwise +static int +nut_parser_push (struct nut_parser *self, char c) +{ + switch (self->state) + { + case NUT_STATE_START_LINE: + str_vector_reset (&self->fields); + str_reset (&self->current_field); + self->state = NUT_STATE_BETWEEN; + // Fall-through + + case NUT_STATE_BETWEEN: + if (c == '\\') + self->state = NUT_STATE_UNQUOTED_ESCAPE; + else if (c == '"') + self->state = NUT_STATE_QUOTED; + else if (c == '\n' && self->fields.len) + { + self->state = NUT_STATE_START_LINE; + return 1; + } + else if (!isspace_ascii (c)) + { + str_append_c (&self->current_field, c); + self->state = NUT_STATE_UNQUOTED; + } + return 0; + + case NUT_STATE_UNQUOTED: + if (c == '\\') + self->state = NUT_STATE_UNQUOTED_ESCAPE; + else if (c == '"') + return -1; + else if (!isspace_ascii (c)) + str_append_c (&self->current_field, c); + else + return nut_parser_end_field (self, c); + return 0; + + case NUT_STATE_UNQUOTED_ESCAPE: + str_append_c (&self->current_field, c); + self->state = NUT_STATE_UNQUOTED; + return 0; + + case NUT_STATE_QUOTED: + if (c == '\\') + self->state = NUT_STATE_QUOTED_ESCAPE; + else if (c == '"') + self->state = NUT_STATE_QUOTED_END; + else + str_append_c (&self->current_field, c); + return 0; + + case NUT_STATE_QUOTED_ESCAPE: + str_append_c (&self->current_field, c); + self->state = NUT_STATE_QUOTED; + return 0; + + case NUT_STATE_QUOTED_END: + if (!isspace_ascii (c)) + return -1; + return nut_parser_end_field (self, c); + } + + // Silence the compiler + hard_assert (!"unhandled NUT parser state"); + return -1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct nut_line +{ + LIST_HEADER (struct nut_line) + + struct str_vector fields; ///< Parsed fields from the line +}; + +struct nut_response +{ + struct nut_line *data; ///< Raw result data + + bool success; ///< Whether a general failure occured + char *message; ///< Eventually an error ID string +}; + +/// Task completion callback +typedef void (*nut_client_task_cb) + (const struct nut_response *response, void *user_data); + +struct nut_client_task +{ + LIST_HEADER (struct nut_client_task) + + nut_client_task_cb callback; ///< Callback on completion + void *user_data; ///< User data +}; + +enum nut_client_state +{ + NUT_DISCONNECTED, ///< Not connected + NUT_CONNECTING, ///< Currently connecting + NUT_CONNECTED ///< Connected +}; + +struct nut_client +{ + struct poller *poller; ///< Poller + + // Connection: + + enum nut_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 + + // Protocol: + + struct nut_parser parser; ///< Protocol parser + struct nut_line *data; ///< Data from last command + struct nut_line *data_tail; ///< Tail of data list + bool in_list; ///< Currently within a list + + struct nut_client_task *tasks; ///< Task queue + struct nut_client_task *tasks_tail; ///< Tail of task queue + + // 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); +}; + +static void nut_client_reset (struct nut_client *self); +static void nut_client_destroy_connector (struct nut_client *self); + +static void +nut_client_init (struct nut_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); + + nut_parser_init (&self->parser); + + poller_fd_init (&self->socket_event, poller, -1); +} + +static void +nut_client_free (struct nut_client *self) +{ + // So that we don't have to repeat most of the stuff + nut_client_reset (self); + + str_free (&self->read_buffer); + str_free (&self->write_buffer); + + nut_parser_free (&self->parser); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_client_flush_data (struct nut_client *self) +{ + LIST_FOR_EACH (struct nut_line, iter, self->data) + { + str_vector_free (&iter->fields); + free (iter); + } + self->data = self->data_tail = NULL; +} + +/// Reinitialize the interface so that you can reconnect anew +static void +nut_client_reset (struct nut_client *self) +{ + if (self->state == NUT_CONNECTING) + nut_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); + + str_reset (&self->read_buffer); + str_reset (&self->write_buffer); + + self->parser.state = NUT_STATE_START_LINE; + nut_client_flush_data (self); + self->in_list = false; + + LIST_FOR_EACH (struct nut_client_task, iter, self->tasks) + free (iter); + self->tasks = self->tasks_tail = NULL; + + self->state = NUT_DISCONNECTED; +} + +static void +nut_client_fail (struct nut_client *self) +{ + nut_client_reset (self); + if (self->on_failure) + self->on_failure (self->user_data); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_client_quote (const char *s, struct str *output) +{ + str_append_c (output, '"'); + for (; *s; s++) + { + if (*s == '"' || *s == '\\') + str_append_c (output, '\\'); + str_append_c (output, *s); + } + str_append_c (output, '"'); +} + +static bool +nut_client_must_quote (const char *s) +{ + if (!*s) + return true; + for (; *s; s++) + if ((unsigned char) *s <= ' ' || *s == '"' || *s == '\\') + return true; + return false; +} + +static void +nut_client_serialize (char **commands, struct str *line) +{ + for (; *commands; commands++) + { + if (line->len) + str_append_c (line, ' '); + + if (nut_client_must_quote (*commands)) + nut_client_quote (*commands, line); + else + str_append (line, *commands); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_client_dispatch (struct nut_client *self, struct nut_response *response) +{ + struct nut_client_task *task; + if (!(task = self->tasks)) + return; + + if (task->callback) + task->callback (response, task->user_data); + nut_client_flush_data (self); + + LIST_UNLINK_WITH_TAIL (self->tasks, self->tasks_tail, task); + free (task); +} + +static bool +nut_client_parse_line (struct nut_client *self) +{ + struct str reconstructed; + str_init (&reconstructed); + nut_client_serialize (self->parser.fields.vector, &reconstructed); + print_debug ("NUT >> %s", reconstructed.str); + str_free (&reconstructed); + + struct str_vector *fields = &self->parser.fields; + hard_assert (fields->len != 0); + + // Lists are always dispatched as their innards (and they can be empty) + if (fields->len >= 2 + && !strcmp (fields->vector[0], "BEGIN") + && !strcmp (fields->vector[1], "LIST")) + self->in_list = true; + else if (fields->len >= 2 + && !strcmp (fields->vector[0], "END") + && !strcmp (fields->vector[1], "LIST")) + self->in_list = false; + else + { + struct nut_line *line = xcalloc (1, sizeof *line); + str_vector_init (&line->fields); + str_vector_add_vector (&line->fields, fields->vector); + LIST_APPEND_WITH_TAIL (self->data, self->data_tail, line); + } + + if (!self->in_list) + { + struct nut_response response; + memset (&response, 0, sizeof response); + response.success = true; + response.data = self->data; + + if (!strcmp (fields->vector[0], "ERR")) + { + response.success = false; + if (fields->len < 2) + return false; + response.message = xstrdup (fields->vector[1]); + } + + nut_client_dispatch (self, &response); + free (response.message); + } + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_client_update_poller (struct nut_client *self) +{ + poller_fd_set (&self->socket_event, + self->write_buffer.len ? (POLLIN | POLLOUT) : POLLIN); +} + +static bool +nut_client_process_input (struct nut_client *self) +{ + struct str *rb = &self->read_buffer; + for (size_t i = 0; i < rb->len; i++) + { + int res = nut_parser_push (&self->parser, rb->str[i]); + if (res == -1 || (res == 1 && !nut_client_parse_line (self))) + return false; + } + + str_reset (rb); + return true; +} + +static void +nut_client_on_ready (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + + struct nut_client *self = user_data; + bool read_succeeded = socket_io_try_read + (self->socket, &self->read_buffer) == SOCKET_IO_OK; + + // Whether or not the read was successful, we need to process all data + if (!nut_client_process_input (self) || !read_succeeded + || socket_io_try_write (self->socket, &self->write_buffer) != SOCKET_IO_OK) + nut_client_fail (self); + else + nut_client_update_poller (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// Beware that delivery of the event isn't deferred and you musn't make +/// changes to the interface while processing the event! +static void +nut_client_add_task + (struct nut_client *self, nut_client_task_cb cb, void *user_data) +{ + struct nut_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 nut_client_add_task() to handle responses, +/// unless the command generates none. +static void nut_client_send_command + (struct nut_client *self, const char *command, ...) ATTRIBUTE_SENTINEL; + +static void +nut_client_send_commandv (struct nut_client *self, char **commands) +{ + struct str line; + str_init (&line); + nut_client_serialize (commands, &line); + + print_debug ("NUT << %s", line.str); + str_append_c (&line, '\n'); + str_append_str (&self->write_buffer, &line); + str_free (&line); + + nut_client_update_poller (self); +} + +static void +nut_client_send_command (struct nut_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); + + nut_client_send_commandv (self, v.vector); + str_vector_free (&v); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_client_finish_connection (struct nut_client *self, int socket) +{ + set_blocking (socket, false); + self->socket = socket; + self->state = NUT_CONNECTED; + + poller_fd_init (&self->socket_event, self->poller, self->socket); + self->socket_event.dispatcher = nut_client_on_ready; + self->socket_event.user_data = self; + + nut_client_update_poller (self); + + if (self->on_connected) + self->on_connected (self->user_data); +} + +static void +nut_client_destroy_connector (struct nut_client *self) +{ + if (self->connector) + connector_free (self->connector); + free (self->connector); + self->connector = NULL; + + // Not connecting anymore + self->state = NUT_DISCONNECTED; +} + +static void +nut_client_on_connector_failure (void *user_data) +{ + struct nut_client *self = user_data; + nut_client_destroy_connector (self); + nut_client_fail (self); +} + +static void +nut_client_on_connector_connected + (void *user_data, int socket, const char *host) +{ + (void) host; + + struct nut_client *self = user_data; + nut_client_destroy_connector (self); + nut_client_finish_connection (self, socket); +} + +static void +nut_client_connect + (struct nut_client *self, const char *address, const char *service) +{ + hard_assert (self->state == NUT_DISCONNECTED); + + struct connector *connector = xmalloc (sizeof *connector); + connector_init (connector, self->poller); + self->connector = connector; + + connector->user_data = self; + connector->on_connected = nut_client_on_connector_connected; + connector->on_failure = nut_client_on_connector_failure; + + connector_add_target (connector, address, service); + self->state = NUT_CONNECTING; +} + +// --- Configuration ----------------------------------------------------------- + +static struct simple_config_item g_config_table[] = +{ + { "mpd_address", "localhost", "MPD host or socket" }, + { "mpd_service", "6600", "MPD service name or port" }, + { "mpd_password", NULL, "MPD password" }, + + { "nut_enabled", "off", "NUT UPS status reading enabled" }, + + { NULL, NULL, NULL } +}; + +// --- Application ------------------------------------------------------------- + +struct app_context +{ + struct str_map config; ///< Program configuration + + Display *dpy; ///< X display handle + const char *prefix; ///< User-defined prefix + + struct poller poller; ///< Poller + struct poller_timer time_changed; ///< Time change timer + struct poller_timer make_context; ///< Start PulseAudio communication + struct poller_timer refresh_rest; ///< Refresh unpollable information + + // Hotkeys: + + struct poller_fd x_event; ///< X11 event + + // MPD: + + struct poller_timer mpd_reconnect; ///< Start MPD communication + struct mpd_client mpd_client; ///< MPD client + + char *mpd_song; ///< MPD current song + char *mpd_status; ///< MPD status (overrides song) + + // NUT: + + struct poller_timer nut_reconnect; ///< Start NUT communication + struct nut_client nut_client; ///< NUT client + struct str_map nut_ups_info; ///< Per-UPS information + bool nut_success; ///< Information retrieved successfully + + char *nut_status; ///< NUT status + + // PulseAudio: + + pa_mainloop_api *api; ///< PulseAudio event loop proxy + pa_context *context; ///< PulseAudio connection context + + bool failed; ///< General PulseAudio failure + + char *sink_name; ///< The sink to watch volume of + pa_cvolume volume; ///< Current volume + bool muted; ///< Currently muted? +}; + +static void +str_map_destroy (void *self) +{ + str_map_free (self); + free (self); +} + +static void +app_context_init (struct app_context *self) +{ + memset (self, 0, sizeof *self); + + str_map_init (&self->config); + self->config.free = free; + simple_config_load_defaults (&self->config, g_config_table); + + if (!(self->dpy = XOpenDisplay (NULL))) + exit_fatal ("cannot open display"); + + poller_init (&self->poller); + self->api = poller_pa_new (&self->poller); + + poller_fd_init (&self->x_event, &self->poller, + ConnectionNumber (self->dpy)); + + mpd_client_init (&self->mpd_client, &self->poller); + + nut_client_init (&self->nut_client, &self->poller); + str_map_init (&self->nut_ups_info); + self->nut_ups_info.free = str_map_destroy; +} + +static void +app_context_free (struct app_context *self) +{ + str_map_free (&self->config); + + poller_fd_reset (&self->x_event); + + if (self->context) pa_context_unref (self->context); + if (self->dpy) XCloseDisplay (self->dpy); + + poller_pa_destroy (self->api); + poller_free (&self->poller); + + mpd_client_free (&self->mpd_client); + free (self->mpd_song); + free (self->mpd_status); + + nut_client_free (&self->nut_client); + str_map_free (&self->nut_ups_info); + + free (self->sink_name); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +read_value (int dir, const char *filename, struct error **e) +{ + int fd = openat (dir, filename, O_RDONLY); + if (fd < 0) + { + error_set (e, "%s: %s: %s", filename, "openat", strerror (errno)); + return NULL; + } + + FILE *fp = fdopen (fd, "r"); + if (!fp) + { + error_set (e, "%s: %s: %s", filename, "fdopen", strerror (errno)); + close (fd); + return NULL; + } + + struct str s; + str_init (&s); + bool success = read_line (fp, &s); + fclose (fp); + + if (!success) + { + error_set (e, "%s: %s", filename, "read failed"); + return NULL; + } + return str_steal (&s); +} + +static unsigned long +read_number (int dir, const char *filename, struct error **e) +{ + char *value; + if (!(value = read_value (dir, filename, e))) + return false; + + unsigned long number = 0; + if (!xstrtoul (&number, value, 10)) + error_set (e, "%s: %s", filename, "doesn't contain an unsigned number"); + free (value); + return number; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +read_battery_status (int dir, struct error **e) +{ + char *result = NULL; + struct error *error = NULL; + + char *status; + double charge_now; + double charge_full; + + if ((status = read_value (dir, "status", &error), error) + || (charge_now = read_number (dir, "charge_now", &error), error) + || (charge_full = read_number (dir, "charge_full", &error), error)) + error_propagate (e, error); + else + result = xstrdup_printf ("%s (%u%%)", + status, (unsigned) (charge_now / charge_full * 100 + 0.5)); + + free (status); + return result; +} + +static char * +try_power_supply (int dir, struct error **e) +{ + char *type; + struct error *error = NULL; + if (!(type = read_value (dir, "type", &error))) + { + error_propagate (e, error); + return NULL; + } + + bool is_relevant = + !strcmp (type, "Battery") || + !strcmp (type, "UPS"); + + char *result = NULL; + if (is_relevant) + { + char *status = read_battery_status (dir, &error); + if (error) + error_propagate (e, error); + if (status) + result = xstrdup_printf ("%s %s", type, status); + free (status); + } + free (type); + return result; +} + +static char * +make_battery_status (void) +{ + DIR *power_supply = opendir ("/sys/class/power_supply"); + if (!power_supply) + { + print_debug ("cannot access %s: %s: %s", + "/sys/class/power_supply", "opendir", strerror (errno)); + return NULL; + } + + struct dirent *entry; + char *status = NULL; + while (!status && (entry = readdir (power_supply))) + { + const char *device_name = entry->d_name; + if (device_name[0] == '.') + continue; + + int dir = openat (dirfd (power_supply), device_name, O_RDONLY); + if (dir < 0) + { + print_error ("%s: %s: %s", device_name, "openat", strerror (errno)); + continue; + } + + struct error *error = NULL; + status = try_power_supply (dir, &error); + close (dir); + if (error) + { + print_error ("%s: %s", device_name, error->message); + error_free (error); + } + } + closedir (power_supply); + return status; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +make_time_status (char *fmt) +{ + char buf[129] = ""; + time_t now = time (NULL); + struct tm *local = localtime (&now); + + if (local == NULL) + exit_fatal ("%s: %s", "localtime", strerror (errno)); + if (!strftime (buf, sizeof buf, fmt, local)) + exit_fatal ("strftime == 0"); + + return xstrdup (buf); +} + +#define VOLUME_PERCENT(x) (((x) * 100 + PA_VOLUME_NORM / 2) / PA_VOLUME_NORM) + +static char * +make_volume_status (struct app_context *ctx) +{ + if (!ctx->volume.channels) + return xstrdup (""); + + struct str s; + str_init (&s); + str_append_printf (&s, "%u%%", VOLUME_PERCENT (ctx->volume.values[0])); + if (!pa_cvolume_channels_equal_to (&ctx->volume, ctx->volume.values[0])) + for (size_t i = 1; i < ctx->volume.channels; i++) + str_append_printf (&s, " / %u%%", + VOLUME_PERCENT (ctx->volume.values[i])); + return str_steal (&s); +} + +static void +refresh_status (struct app_context *ctx) +{ + struct str status; + str_init (&status); + + if (ctx->prefix) + str_append_printf (&status, "%s ", ctx->prefix); + + if (ctx->mpd_status) + str_append_printf (&status, "%s ", ctx->mpd_status); + else if (ctx->mpd_song) + str_append_printf (&status, "%s ", ctx->mpd_song); + + if (ctx->failed) + str_append_printf (&status, "%s ", "PA failure"); + else + { + char *volumes = make_volume_status (ctx); + str_append_printf (&status, "%s %s ", + ctx->muted ? "Muted" : "Volume", volumes); + free (volumes); + } + + char *battery = make_battery_status (); + if (battery) + str_append_printf (&status, "%s ", battery); + free (battery); + + if (ctx->nut_status) + str_append_printf (&status, "%s ", ctx->nut_status); + + char *times = make_time_status ("Week %V, %a %d %b %Y %H:%M %Z"); + str_append (&status, times); + free (times); + + set_dwm_status (ctx->dpy, status.str); + str_free (&status); +} + +static void +on_time_changed (void *user_data) +{ + struct app_context *ctx = user_data; + refresh_status (ctx); + + const time_t now = time (NULL); + const time_t next = (now / 60 + 1) * 60; + poller_timer_set (&ctx->time_changed, (next - now) * 1000); +} + +static void +on_refresh_rest (void *user_data) +{ + struct app_context *ctx = user_data; + + // We cannot use poll() on most sysfs entries, including battery charge + + refresh_status (ctx); + poller_timer_set (&ctx->refresh_rest, 5000); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// 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) +{ + if (!response->success) + { + print_debug ("%s: %s", + "retrieving MPD info failed", response->message_text); + return; + } + + struct app_context *ctx = user_data; + struct str_map map; + mpd_vector_to_map (data, &map); + + free (ctx->mpd_status); + ctx->mpd_status = NULL; + + const char *value; + if ((value = str_map_find (&map, "state"))) + { + if (!strcmp (value, "stop")) + ctx->mpd_status = xstrdup ("MPD stopped"); + if (!strcmp (value, "pause")) + ctx->mpd_status = xstrdup ("MPD paused"); + } + + struct str s; + str_init (&s); + + str_append (&s, "Playing: "); + 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); + + free (ctx->mpd_song); + ctx->mpd_song = str_steal (&s); + + refresh_status (ctx); + str_map_free (&map); +} + +static void +mpd_request_info (struct app_context *ctx) +{ + struct mpd_client *c = &ctx->mpd_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, ctx); + + mpd_client_idle (c, 0); +} + +static void +mpd_on_events (unsigned subsystems, void *user_data) +{ + struct app_context *ctx = user_data; + struct mpd_client *c = &ctx->mpd_client; + + if (subsystems & (MPD_SUBSYSTEM_PLAYER | MPD_SUBSYSTEM_PLAYLIST)) + mpd_request_info (ctx); + else + mpd_client_idle (c, 0); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +mpd_queue_reconnect (struct app_context *ctx) +{ + poller_timer_set (&ctx->mpd_reconnect, 30 * 1000); +} + +static void +mpd_on_password_response (const struct mpd_response *response, + const struct str_vector *data, void *user_data) +{ + (void) data; + struct app_context *ctx = user_data; + struct mpd_client *c = &ctx->mpd_client; + + if (response->success) + mpd_request_info (ctx); + 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) +{ + struct app_context *ctx = user_data; + struct mpd_client *c = &ctx->mpd_client; + + const char *password = str_map_find (&ctx->config, "mpd_password"); + if (password) + { + mpd_client_send_command (c, "password", password, NULL); + mpd_client_add_task (c, mpd_on_password_response, ctx); + } + else + mpd_request_info (ctx); +} + +static void +mpd_on_failure (void *user_data) +{ + // This is also triggered both by a failed connect and a clean disconnect + struct app_context *ctx = user_data; + print_error ("connection to MPD failed"); + mpd_queue_reconnect (ctx); +} + +static void +on_mpd_reconnect (void *user_data) +{ + struct app_context *ctx = user_data; + + struct mpd_client *c = &ctx->mpd_client; + c->user_data = ctx; + c->on_failure = mpd_on_failure; + c->on_connected = mpd_on_connected; + c->on_event = mpd_on_events; + + struct error *e = NULL; + if (!mpd_client_connect (&ctx->mpd_client, + str_map_find (&ctx->config, "mpd_address"), + str_map_find (&ctx->config, "mpd_service"), &e)) + { + print_error ("%s: %s", "cannot connect to MPD", e->message); + error_free (e); + mpd_queue_reconnect (ctx); + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static bool +nut_common_handler (const struct nut_response *response) +{ + if (response->success) + return true; + + print_error ("%s: %s", "retrieving NUT info failed", response->message); + return false; +} + +static void +nut_translate_status (const char *status, struct str_vector *out) +{ + // https://github.com/networkupstools/nut/blob/master/clients/status.h + if (!strcmp (status, "OL")) str_vector_add (out, "on-line"); + if (!strcmp (status, "OB")) str_vector_add (out, "on battery"); + if (!strcmp (status, "LB")) str_vector_add (out, "low battery"); + if (!strcmp (status, "RB")) str_vector_add (out, "replace battery"); + if (!strcmp (status, "CHRG")) str_vector_add (out, "charging"); + if (!strcmp (status, "DISCHRG")) str_vector_add (out, "discharging"); + if (!strcmp (status, "OVER")) str_vector_add (out, "overload"); + if (!strcmp (status, "OFF")) str_vector_add (out, "off"); + if (!strcmp (status, "TRIM")) str_vector_add (out, "voltage trim"); + if (!strcmp (status, "BOOST")) str_vector_add (out, "voltage boost"); + if (!strcmp (status, "BYPASS")) str_vector_add (out, "bypass"); +} + +static char * +interval_string (unsigned long seconds) +{ + unsigned long hours = seconds / 3600; seconds %= 3600; + unsigned long mins = seconds / 60; seconds %= 60; + return xstrdup_printf ("%lu:%02lu:%02lu", hours, mins, seconds); +} + +static void +nut_process_ups (struct str_vector *ups_list, + const char *ups_name, struct str_map *dict) +{ + // Not currently interested in this kind of information; + // maybe if someone had more than one UPS installed + (void) ups_name; + + // http://www.networkupstools.org/docs/developer-guide.chunked/apas01.html + const char *status = str_map_find (dict, "ups.status"); + const char *charge = str_map_find (dict, "battery.charge"); + const char *runtime = str_map_find (dict, "battery.runtime"); + const char *load = str_map_find (dict, "ups.load"); + + if (!soft_assert (status && charge && runtime)) + return; + + unsigned long runtime_sec; + if (!soft_assert (xstrtoul (&runtime_sec, runtime, 10))) + return; + + struct str_vector items; + str_vector_init (&items); + + bool running_on_batteries = false; + + struct str_vector v; + str_vector_init (&v); + cstr_split_ignore_empty (status, ' ', &v); + for (size_t i = 0; i < v.len; i++) + { + const char *status = v.vector[i]; + nut_translate_status (status, &items); + + if (!strcmp (status, "OB")) + running_on_batteries = true; + } + str_vector_free (&v); + + if (running_on_batteries || strcmp (charge, "100")) + str_vector_add_owned (&items, xstrdup_printf ("%s%%", charge)); + if (running_on_batteries) + str_vector_add_owned (&items, interval_string (runtime_sec)); + if (load) + str_vector_add_owned (&items, xstrdup_printf ("load %s%%", load)); + + struct str result; + str_init (&result); + str_append (&result, "UPS: "); + + for (size_t i = 0; i < items.len; i++) + { + if (i) str_append (&result, "; "); + str_append (&result, items.vector[i]); + } + str_vector_free (&items); + str_vector_add_owned (ups_list, str_steal (&result)); +} + +static void +nut_on_logout_response (const struct nut_response *response, void *user_data) +{ + if (!nut_common_handler (response)) + return; + + struct app_context *ctx = user_data; + struct str_vector ups_list; + str_vector_init (&ups_list); + + struct str_map_iter iter; + str_map_iter_init (&iter, &ctx->nut_ups_info); + struct str_map *dict; + while ((dict = str_map_iter_next (&iter))) + nut_process_ups (&ups_list, iter.link->key, dict); + + free (ctx->nut_status); + ctx->nut_status = NULL; + + if (ups_list.len) + { + struct str status; + str_init (&status); + str_append (&status, ups_list.vector[0]); + for (size_t i = 1; i < ups_list.len; i++) + str_append_printf (&status, " %s", ups_list.vector[0]); + ctx->nut_status = str_steal (&status); + } + + ctx->nut_success = true; + str_vector_free (&ups_list); + refresh_status (ctx); +} + +static void +nut_store_var (struct app_context *ctx, + const char *ups_name, const char *key, const char *value) +{ + struct str_map *map; + if (!(map = str_map_find (&ctx->nut_ups_info, ups_name))) + { + str_map_init ((map = xcalloc (1, sizeof *map))); + map->free = free; + str_map_set (&ctx->nut_ups_info, ups_name, map); + } + + str_map_set (map, key, xstrdup (value)); +} + +static void +nut_on_var_response (const struct nut_response *response, void *user_data) +{ + if (!nut_common_handler (response)) + return; + + struct app_context *ctx = user_data; + LIST_FOR_EACH (struct nut_line, iter, response->data) + { + const struct str_vector *fields = &iter->fields; + if (!soft_assert (fields->len >= 4 + && !strcmp (fields->vector[0], "VAR"))) + continue; + + nut_store_var (ctx, fields->vector[1], + fields->vector[2], fields->vector[3]); + } +} + +static void +nut_on_list_ups_response (const struct nut_response *response, void *user_data) +{ + if (!nut_common_handler (response)) + return; + + struct app_context *ctx = user_data; + struct nut_client *c = &ctx->nut_client; + + // Then we list all their properties and terminate the connection + LIST_FOR_EACH (struct nut_line, iter, response->data) + { + const struct str_vector *fields = &iter->fields; + if (!soft_assert (fields->len >= 2 + && !strcmp (fields->vector[0], "UPS"))) + continue; + + nut_client_send_command (c, "LIST", "VAR", fields->vector[1], NULL); + nut_client_add_task (c, nut_on_var_response, ctx); + } + + nut_client_send_command (c, "LOGOUT", NULL); + nut_client_add_task (c, nut_on_logout_response, ctx); +} + +static void +nut_on_connected (void *user_data) +{ + struct app_context *ctx = user_data; + struct nut_client *c = &ctx->nut_client; + + // First we list all available UPS devices + nut_client_send_command (c, "LIST", "UPS", NULL); + nut_client_add_task (c, nut_on_list_ups_response, ctx); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +nut_indicate_failure (struct app_context *ctx) +{ + free (ctx->nut_status); + ctx->nut_status = xstrdup ("NUT failure"); + + refresh_status (ctx); +} + +static void +nut_on_failure (void *user_data) +{ + struct app_context *ctx = user_data; + + // This is also triggered both by a failed connect and a clean disconnect + if (!ctx->nut_success) + { + print_error ("connection to NUT failed"); + nut_indicate_failure (ctx); + } +} + +static void +on_nut_reconnect (void *user_data) +{ + struct app_context *ctx = user_data; + + bool want_nut = false; + if (!set_boolean_if_valid (&want_nut, + str_map_find (&ctx->config, "nut_enabled"))) + print_error ("invalid configuration value for `%s'", "nut_enabled"); + if (!want_nut) + return; + + struct nut_client *c = &ctx->nut_client; + c->user_data = ctx; + c->on_failure = nut_on_failure; + c->on_connected = nut_on_connected; + + // So that we don't have to maintain a separate timeout timer, + // we keep a simple periodic reconnect timer + if (c->state != NUT_DISCONNECTED) + { + print_error ("failed to retrieve NUT status within the interval"); + nut_indicate_failure (ctx); + nut_client_reset (c); + } + + str_map_clear (&ctx->nut_ups_info); + + nut_client_connect (&ctx->nut_client, "localhost", "3493"); + ctx->nut_success = false; + poller_timer_set (&ctx->nut_reconnect, 10 * 1000); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +on_sink_info + (pa_context *context, const pa_sink_info *info, int eol, void *userdata) +{ + (void) context; + + if (!info && eol) + return; + + struct app_context *ctx = userdata; + ctx->volume = info->volume; + ctx->muted = !!info->mute; + refresh_status (ctx); +} + +static void +update_volume (struct app_context *ctx) +{ + pa_operation_unref (pa_context_get_sink_info_by_name + (ctx->context, ctx->sink_name, on_sink_info, ctx)); +} + +static void +on_server_info (pa_context *context, const pa_server_info *info, void *userdata) +{ + (void) context; + + struct app_context *ctx = userdata; + free (ctx->sink_name); + ctx->sink_name = strdup (info->default_sink_name); + update_volume (ctx); +} + +static void +on_event (pa_context *context, pa_subscription_event_type_t event, + uint32_t index, void *userdata) +{ + (void) context; + (void) index; + + struct app_context *ctx = userdata; + pa_subscription_event_type_t facility = + event & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + pa_subscription_event_type_t type = + event & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + + // XXX: can the default sink be removed before being changed? + if (facility == PA_SUBSCRIPTION_EVENT_SINK + && type == PA_SUBSCRIPTION_EVENT_CHANGE) + update_volume (ctx); + + // Default sink could change + if (facility == PA_SUBSCRIPTION_EVENT_SERVER) + pa_operation_unref (pa_context_get_server_info (context, + on_server_info, userdata)); +} + +static void +on_subscribe_finish (pa_context *context, int success, void *userdata) +{ + (void) context; + + struct app_context *ctx = userdata; + if (!success) + { + ctx->failed = true; + refresh_status (ctx); + } +} + +static void +on_context_state_change (pa_context *context, void *userdata) +{ + struct app_context *ctx = userdata; + switch (pa_context_get_state (context)) + { + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + ctx->failed = true; + refresh_status (ctx); + + pa_context_unref (context); + ctx->context = NULL; + + // Retry after an arbitrary delay of 5 seconds + poller_timer_set (&ctx->make_context, 5000); + return; + case PA_CONTEXT_READY: + ctx->failed = false; + refresh_status (ctx); + + pa_context_get_server_info (context, on_server_info, userdata); + pa_context_set_subscribe_callback (context, on_event, userdata); + pa_operation_unref (pa_context_subscribe (context, + PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER, + on_subscribe_finish, userdata)); + default: + return; + } +} + +static void +on_make_context (void *user_data) +{ + struct app_context *ctx = user_data; + ctx->context = pa_context_new (ctx->api, PROGRAM_NAME); + pa_context_set_state_callback (ctx->context, on_context_state_change, ctx); + pa_context_connect (ctx->context, NULL, PA_CONTEXT_NOFLAGS, NULL); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define MPD_SIMPLE(name, ...) \ + static void \ + on_mpd_ ## name (struct app_context *ctx) \ + { \ + struct mpd_client *c = &ctx->mpd_client; \ + if (c->state != MPD_CONNECTED) \ + return; \ + mpd_client_send_command (c, __VA_ARGS__); \ + mpd_client_add_task (c, NULL, NULL); \ + mpd_client_idle (c, 0); \ + } + +// XXX: pause without argument is deprecated, we can watch play state +// if we want to have the toggle pause/play functionality +MPD_SIMPLE (play, "pause", NULL) +MPD_SIMPLE (stop, "stop", NULL) +MPD_SIMPLE (prev, "previous", NULL) +MPD_SIMPLE (next, "next", NULL) +MPD_SIMPLE (forward, "seekcur", "+10", NULL) +MPD_SIMPLE (backward, "seekcur", "-10", NULL) + +struct +{ + unsigned mod; + KeySym keysym; + void (*handler) (struct app_context *ctx); +} +g_keys[] = +{ + { Mod4Mask, XK_Up, on_mpd_play }, + { Mod4Mask, XK_Down, on_mpd_stop }, + { Mod4Mask, XK_Left, on_mpd_prev }, + { Mod4Mask, XK_Right, on_mpd_next }, + /* xmodmap | grep -e Alt_R -e Meta_R -e ISO_Level3_Shift -e Mode_switch */ + { Mod4Mask | Mod5Mask, XK_Left, on_mpd_backward }, + { Mod4Mask | Mod5Mask, XK_Right, on_mpd_forward }, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static unsigned +get_numlock_mask (struct app_context *ctx) +{ + unsigned result = 0; + XModifierKeymap *modmap = XGetModifierMapping (ctx->dpy); + for (unsigned i = 0; i < 8; i++) + for (int j = 0; j < modmap->max_keypermod; j++) + if (modmap->modifiermap[i * modmap->max_keypermod + j] == + XKeysymToKeycode (ctx->dpy, XK_Num_Lock)) + result = (1 << i); + XFreeModifiermap(modmap); + return result; +} + +#define CLEANMASK(mask) (mask & ~(numlock_mask | LockMask)) + +static void +on_x_keypress (struct app_context *ctx, XEvent *e) +{ + unsigned numlock_mask = get_numlock_mask (ctx); + + XKeyEvent *ev = &e->xkey; + KeySym keysym = XKeycodeToKeysym (ctx->dpy, (KeyCode) ev->keycode, 0); + for (size_t i = 0; i < N_ELEMENTS (g_keys); i++) + if (keysym == g_keys[i].keysym + && CLEANMASK (g_keys[i].mod) == CLEANMASK (ev->state) + && g_keys[i].handler) + g_keys[i].handler (ctx); +} + +static void +on_x_ready (const struct pollfd *pfd, void *user_data) +{ + (void) pfd; + struct app_context *ctx = user_data; + + XEvent ev; + while (XPending (ctx->dpy)) + { + if (XNextEvent (ctx->dpy, &ev)) + exit_fatal ("XNextEvent returned non-zero"); + if (ev.type == KeyPress) + on_x_keypress (ctx, &ev); + } +} + +static void +grab_keys (struct app_context *ctx) +{ + unsigned numlock_mask = get_numlock_mask (ctx); + unsigned modifiers[] = + { 0, LockMask, numlock_mask, numlock_mask | LockMask }; + + KeyCode code; + Window root = DefaultRootWindow (ctx->dpy); + for (size_t i = 0; i < N_ELEMENTS (g_keys); i++) + if ((code = XKeysymToKeycode (ctx->dpy, g_keys[i].keysym))) + for (size_t j = 0; j < N_ELEMENTS (modifiers); j++) + XGrabKey (ctx->dpy, code, g_keys[i].mod | modifiers[j], root, + False /* ? */, GrabModeAsync, GrabModeAsync); + + XSelectInput (ctx->dpy, root, KeyPressMask); + XSync (ctx->dpy, False); + + ctx->x_event.dispatcher = on_x_ready; + ctx->x_event.user_data = ctx; + poller_fd_set (&ctx->x_event, POLLIN); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +poller_timer_init_and_set (struct poller_timer *self, struct poller *poller, + poller_timer_fn cb, void *user_data) +{ + poller_timer_init (self, poller); + self->dispatcher = cb; + self->user_data = user_data; + + poller_timer_set (self, 0); +} + +int +main (int argc, char *argv[]) +{ + g_log_message_real = log_message_custom; + + 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" }, + { 'w', "write-default-cfg", "FILENAME", + OPT_OPTIONAL_ARG | OPT_LONG_ONLY, + "write a default configuration file and exit" }, + { 0, NULL, NULL, 0, NULL } + }; + + struct opt_handler oh; + opt_handler_init (&oh, argc, argv, opts, NULL, "Set root window name."); + + 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); + case 'w': + call_simple_config_write_default (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + opt_handler_usage (&oh, stderr); + exit (EXIT_FAILURE); + } + + argc -= optind; + argv += optind; + + opt_handler_free (&oh); + + struct app_context ctx; + app_context_init (&ctx); + ctx.prefix = argc > 1 ? argv[1] : NULL; + + struct error *e = NULL; + if (!simple_config_update_from_file (&ctx.config, &e)) + exit_fatal ("%s", e->message); + + poller_timer_init_and_set (&ctx.time_changed, &ctx.poller, + on_time_changed, &ctx); + poller_timer_init_and_set (&ctx.make_context, &ctx.poller, + on_make_context, &ctx); + poller_timer_init_and_set (&ctx.refresh_rest, &ctx.poller, + on_refresh_rest, &ctx); + poller_timer_init_and_set (&ctx.mpd_reconnect, &ctx.poller, + on_mpd_reconnect, &ctx); + poller_timer_init_and_set (&ctx.nut_reconnect, &ctx.poller, + on_nut_reconnect, &ctx); + + grab_keys (&ctx); + + poller_pa_run (ctx.api); + app_context_free (&ctx); + + return 0; +} diff --git a/liberty b/liberty new file mode 160000 index 0000000..9e3cb2b --- /dev/null +++ b/liberty @@ -0,0 +1 @@ +Subproject commit 9e3cb2b6aa2db3ca0ea6a854fb3f89a163c84235 -- cgit v1.2.3-70-g09d2