/*
* 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;
}