diff options
author | Přemysl Janouch <p.janouch@gmail.com> | 2016-01-10 02:22:43 +0100 |
---|---|---|
committer | Přemysl Janouch <p.janouch@gmail.com> | 2016-01-10 03:12:47 +0100 |
commit | a91e24930cec67804db411b31a0a6eee411077eb (patch) | |
tree | 73081bc68721c5a2f9c32ef1e8489d5998110e90 | |
download | desktop-tools-a91e24930cec67804db411b31a0a6eee411077eb.tar.gz desktop-tools-a91e24930cec67804db411b31a0a6eee411077eb.tar.xz desktop-tools-a91e24930cec67804db411b31a0a6eee411077eb.zip |
Initial commit
These used to be part of my dwm fork repository.
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | .gitmodules | 3 | ||||
-rw-r--r-- | CMakeLists.txt | 67 | ||||
-rw-r--r-- | LICENSE | 14 | ||||
-rw-r--r-- | README.adoc | 49 | ||||
-rw-r--r-- | brightness.c | 445 | ||||
-rw-r--r-- | config.h.in | 8 | ||||
-rw-r--r-- | dwmstatus.c | 2742 | ||||
m--------- | liberty | 0 |
9 files changed, 3337 insertions, 0 deletions
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 <p.janouch@gmail.com>") +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) @@ -0,0 +1,14 @@ + Copyright (c) 2015 - 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..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 <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/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 <p.janouch@gmail.com> + * + * 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 <sys/ioctl.h> +#include <dirent.h> + +#include <linux/i2c.h> +#include <linux/i2c-dev.h> + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +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 <percentage diff>\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 <p.janouch@gmail.com> + * + * 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 <dirent.h> +#include <sys/un.h> + +#include <X11/Xlib.h> +#include <X11/keysym.h> + +#include <pulse/mainloop.h> +#include <pulse/context.h> +#include <pulse/error.h> +#include <pulse/introspect.h> +#include <pulse/subscribe.h> + +// --- 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 +Subproject 9e3cb2b6aa2db3ca0ea6a854fb3f89a163c8423 |