aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--moc_library.c741
-rw-r--r--moc_library.h39
-rw-r--r--moc_x11.c14
-rw-r--r--moc_xaw.c596
4 files changed, 1270 insertions, 120 deletions
diff --git a/moc_library.c b/moc_library.c
index 6b2441c..dd265b5 100644
--- a/moc_library.c
+++ b/moc_library.c
@@ -6,6 +6,10 @@
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
+#include <string.h>
+#include <time.h>
+
+#include "moc_library.h"
/* State of the server. */
#define STATE_PLAY 0x01
@@ -16,6 +20,23 @@
#define CMD_GET_TAGS 0x2c /* get tags for the currently played file */
#define CMD_GET_FILE_TAGS 0x2f /* get tags for the specified file */
#define CMD_GET_SNAME 0x0f /* get the stream file name */
+#define CMD_PLAY 0x00 /* play the first element on the list, or a
+ specific file if a non-empty name is given */
+#define CMD_LIST_ADD 0x02 /* add an item to the server's playback list */
+#define CMD_PAUSE 0x05
+#define CMD_UNPAUSE 0x06
+#define CMD_STOP 0x04
+#define CMD_NEXT 0x10
+#define CMD_PREV 0x20
+/* Add an item to the synced playlist view (broadcast as EV_PLIST_ADD to
+ * every client, including ourselves). Doesn't touch the server's actual
+ * playback list - that's CMD_LIST_ADD, above. */
+#define CMD_CLI_PLIST_ADD 0x24
+/* Remove/clear an item from the synced playlist view (broadcast as
+ * EV_PLIST_DEL/EV_PLIST_CLEAR). Like CMD_CLI_PLIST_ADD, these never touch
+ * the server's actual playback list. */
+#define CMD_CLI_PLIST_DEL 0x25
+#define CMD_CLI_PLIST_CLEAR 0x26
#define MAX_SEND_STRING 4096
#define RANGE(min, val, max) ((val) >= (min) && (val) <= (max))
@@ -41,6 +62,10 @@
#define EV_AUDIO_START 0x13 /* playing of audio has started */
#define EV_AUDIO_STOP 0x14 /* playing of audio has stopped */
+/* Not a real protocol value - an internal marker pushed onto our pending
+ * request queue while we're waiting for the reply to CMD_GET_SNAME. */
+#define PENDING_SNAME -100
+
/* Events caused by a client that wants to modify the playlist (see
* CMD_CLI_PLIST* commands). */
#define EV_PLIST_ADD 0x50 /* add an item, followed by the file name */
@@ -54,42 +79,15 @@
#define EV_QUEUE_DEL 0x55
#define EV_QUEUE_MOVE 0x56
#define EV_QUEUE_CLEAR 0x57
-/* Flags for the info decoder function. */
-enum tags_select
-{
- TAGS_COMMENTS = 0x01, /* artist, title, etc. */
- TAGS_TIME = 0x02 /* time of the file. */
-};
-enum moc_state {
- STOPPED,
- PLAYING,
- PAUSED
-};
-
-enum moc_status {
- INITIALIZING,
- CONNECTED,
- FAILED_TO_CONNECT,
- ERROR,
- RECONNECTING,
-
-};
-
-struct moc {
- pthread_mutex_t lock;
- enum moc_status status;
- enum moc_state state;
-
- char *filename;
- char *title;
- char *album;
- char *artist;
- int track;
- int time;
- int filled;
- int length;
-};
+/* Ask another connected client (if any) to send us its playlist. */
+#define CMD_GET_PLIST 0x22 /* get the playlist from one of the clients */
+/* Unlike the playlist, the queue is tracked by the server itself, so this
+ * always succeeds (possibly with an empty queue). */
+#define CMD_GET_QUEUE 0x3f /* request the queue from the server */
+/* Subscribe to playlist/queue broadcast events; the server won't send them
+ * to us otherwise. */
+#define CMD_SEND_PLIST_EVENTS 0x1d /* request for playlist events */
extern const char * moc_str_status(enum moc_status s) {
static const char *strings[] = { "INITIALIZING", "CONNECTED", "FAILED_TO_CONNECT", "ERROR", "RECONNECTING" };
@@ -102,7 +100,6 @@ extern const char * moc_str_state(enum moc_state s) {
}
-static char *get_curr_file(int sock);
int send_str (int sock, const char *str);
char *get_str (int sock);
int send_int (int sock, int i);
@@ -119,13 +116,29 @@ void reset_data(struct moc *data) {
data->state = STOPPED;
}
-static char *get_curr_file (const int sock) {
- send_int (sock, CMD_GET_SNAME);
- int ev;
- while(ev != EV_DATA) {
- get_int(sock, &ev);
- }
- return get_str(sock);
+#define CMD_QUEUE_SIZE 16
+
+struct cmd_queue {
+ int items[CMD_QUEUE_SIZE];
+ int head;
+ int tail;
+};
+
+static void cmd_queue_push (struct cmd_queue *q, int cmd) {
+ q->items[q->tail] = cmd;
+ q->tail = (q->tail + 1) % CMD_QUEUE_SIZE;
+}
+
+/* Returns -1 if nothing is pending. */
+static int cmd_queue_pop (struct cmd_queue *q) {
+ int cmd;
+
+ if (q->head == q->tail)
+ return -1;
+
+ cmd = q->items[q->head];
+ q->head = (q->head + 1) % CMD_QUEUE_SIZE;
+ return cmd;
}
@@ -189,12 +202,184 @@ int send_int (int sock, int i)
int get_int (int sock, int *i)
{
int res;
+ size_t nread = 0;
+
+ while (nread < sizeof(int)) {
+ res = recv (sock, (char *)i + nread, sizeof(int) - nread, 0);
+ if (res == -1) {
+ printf("recv() failed when getting int: %s", strerror(errno));
+ return 0;
+ }
+ if (res == 0)
+ return 0;
+ nread += res;
+ }
+
+ return 1;
+}
+
+static int get_time (int sock, time_t *t)
+{
+ int res;
+ size_t nread = 0;
- res = recv (sock, i, sizeof(int), 0);
+ while (nread < sizeof(time_t)) {
+ res = recv (sock, (char *)t + nread, sizeof(time_t) - nread, 0);
+ if (res == -1) {
+ printf("recv() failed when getting time_t: %s", strerror(errno));
+ return 0;
+ }
+ if (res == 0)
+ return 0;
+ nread += res;
+ }
+
+ return 1;
+}
+
+static int send_time (int sock, time_t t)
+{
+ int res = send (sock, &t, sizeof(t), 0);
if (res == -1)
- printf("recv() failed when getting int: %s", strerror(errno));
+ printf("send() failed: %s", strerror(errno));
+ return res == sizeof(t) ? 1 : 0;
+}
- return res == sizeof(int) ? 1 : 0;
+/* Send a minimal/untagged item in the wire format recv_item() expects -
+ * used when adding a file we haven't read tags for ourselves. Our own
+ * EV_PLIST_ADD/EV_QUEUE_ADD handler notices the missing title and asks the
+ * server for real tags afterward. */
+static void send_bare_item (int sock, const char *file)
+{
+ send_str(sock, file);
+ send_str(sock, ""); /* title_tags */
+ send_str(sock, ""); /* title */
+ send_str(sock, ""); /* artist */
+ send_str(sock, ""); /* album */
+ send_int(sock, -1); /* track */
+ send_int(sock, -1); /* time */
+ send_int(sock, 0); /* filled */
+ send_time(sock, time(NULL)); /* mtime */
+}
+
+static void plist_item_free (struct moc_plist_item *item)
+{
+ if (!item)
+ return;
+ free(item->file);
+ free(item->title);
+ free(item->artist);
+ free(item->album);
+ free(item);
+}
+
+/* Receive one playlist/queue item from the socket: file name, and (if the
+ * file name is non-empty) a filename-derived title, full tags, and an mtime.
+ * An empty file name is the "end of playlist" marker used by CMD_GET_PLIST's
+ * bulk transfer; the caller checks item->file[0] for that. Returns NULL on
+ * a read error. */
+static struct moc_plist_item *recv_item (int sock)
+{
+ struct moc_plist_item *item = (struct moc_plist_item *)calloc(1, sizeof(*item));
+ char *title_tags;
+ int filled;
+ time_t mtime;
+
+ if (!(item->file = get_str(sock))) {
+ free(item);
+ return NULL;
+ }
+
+ if (item->file[0]) {
+ if (!(title_tags = get_str(sock))) {
+ plist_item_free(item);
+ return NULL;
+ }
+ free(title_tags); /* filename-derived fallback title; we keep tags->title instead */
+
+ if (!(item->title = get_str(sock)) ||
+ !(item->artist = get_str(sock)) ||
+ !(item->album = get_str(sock)) ||
+ !get_int(sock, &item->track) ||
+ !get_int(sock, &item->time) ||
+ !get_int(sock, &filled) ||
+ !get_time(sock, &mtime)) {
+ plist_item_free(item);
+ return NULL;
+ }
+ }
+
+ return item;
+}
+
+static void playlist_append (struct moc_plist_item **list, struct moc_plist_item *item)
+{
+ struct moc_plist_item **tail = list;
+
+ while (*tail)
+ tail = &(*tail)->next;
+ *tail = item;
+}
+
+static void playlist_remove (struct moc_plist_item **list, const char *file)
+{
+ struct moc_plist_item *item, *prev = NULL;
+
+ for (item = *list; item; prev = item, item = item->next) {
+ if (!strcmp(item->file, file)) {
+ if (prev)
+ prev->next = item->next;
+ else
+ *list = item->next;
+ plist_item_free(item);
+ return;
+ }
+ }
+}
+
+/* The server's EV_PLIST_MOVE/EV_QUEUE_MOVE just identifies two files that
+ * exchange position, so swap their contents in place. */
+static void playlist_swap (struct moc_plist_item *list, const char *from, const char *to)
+{
+ struct moc_plist_item *a = NULL, *b = NULL, *item;
+
+ for (item = list; item; item = item->next) {
+ if (!strcmp(item->file, from))
+ a = item;
+ else if (!strcmp(item->file, to))
+ b = item;
+ }
+
+ if (a && b) {
+ char *file = a->file, *title = a->title, *artist = a->artist, *album = a->album;
+ int track = a->track, time = a->time;
+
+ a->file = b->file; a->title = b->title; a->artist = b->artist; a->album = b->album;
+ a->track = b->track; a->time = b->time;
+
+ b->file = file; b->title = title; b->artist = artist; b->album = album;
+ b->track = track; b->time = time;
+ }
+}
+
+static void playlist_clear (struct moc_plist_item **list)
+{
+ struct moc_plist_item *item = *list, *next;
+
+ while (item) {
+ next = item->next;
+ plist_item_free(item);
+ item = next;
+ }
+ *list = NULL;
+}
+
+static struct moc_plist_item *playlist_find (struct moc_plist_item *list, const char *file)
+{
+ for (; list; list = list->next)
+ if (!strcmp(list->file, file))
+ return list;
+ return NULL;
}
static int moc_server_connect ()
@@ -243,10 +428,107 @@ static int moc_reconnect(struct moc *data) {
pthread_mutex_lock(&data->lock);
data->status = CONNECTED;
+ data->sock = srv_sock;
pthread_mutex_unlock(&data->lock);
return srv_sock;
}
+/* Block until an EV_DATA marker arrives, discarding the payload of any
+ * other event that shows up first. Only safe to use before we've
+ * subscribed to playlist/queue events and outside the main request queue -
+ * i.e. during the one-time startup sync below, where nothing else we care
+ * about can be in flight yet. */
+static void wait_for_ev_data (int sock) {
+ int ev = -1;
+
+ while (ev != EV_DATA) {
+ get_int(sock, &ev);
+ if (ev == EV_SRV_ERROR || ev == EV_STATUS_MSG)
+ free(get_str(sock));
+ }
+}
+
+/* Ask another connected client (usually the real mocp ncurses UI, if one is
+ * running) for its current playlist. There may be none, in which case we
+ * just end up with an empty list. */
+static void fetch_playlist_snapshot (int sock, struct moc *data) {
+ int exists, serial;
+ struct moc_plist_item *item;
+
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(sock, CMD_GET_PLIST);
+ pthread_mutex_unlock(&data->sock_lock);
+ wait_for_ev_data(sock);
+ if (!get_int(sock, &exists) || !exists)
+ return;
+
+ wait_for_ev_data(sock);
+ get_int(sock, &serial);
+
+ for (;;) {
+ item = recv_item(sock);
+ if (!item || !item->file[0]) {
+ plist_item_free(item);
+ break;
+ }
+ pthread_mutex_lock(&data->lock);
+ playlist_append(&data->playlist, item);
+ pthread_mutex_unlock(&data->lock);
+ }
+}
+
+/* Unlike the playlist, the queue lives on the server, so this always
+ * succeeds (possibly with an empty queue). */
+static void fetch_queue_snapshot (int sock, struct moc *data) {
+ struct moc_plist_item *item;
+
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(sock, CMD_GET_QUEUE);
+ pthread_mutex_unlock(&data->sock_lock);
+ wait_for_ev_data(sock);
+
+ for (;;) {
+ item = recv_item(sock);
+ if (!item || !item->file[0]) {
+ plist_item_free(item);
+ break;
+ }
+ pthread_mutex_lock(&data->lock);
+ playlist_append(&data->queue, item);
+ pthread_mutex_unlock(&data->lock);
+ }
+}
+
+/* Pull a one-time snapshot of the playlist and queue, then subscribe to
+ * live updates for both going forward. Must run before anything else talks
+ * to the socket, since it does its own raw synchronous reads outside the
+ * normal pending-request queue. */
+static void sync_playlists (int sock, struct moc *data) {
+ pthread_mutex_lock(&data->lock);
+ playlist_clear(&data->playlist);
+ playlist_clear(&data->queue);
+ pthread_mutex_unlock(&data->lock);
+
+ fetch_playlist_snapshot(sock, data);
+ fetch_queue_snapshot(sock, data);
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(sock, CMD_SEND_PLIST_EVENTS);
+ pthread_mutex_unlock(&data->sock_lock);
+
+ {
+ int plist_n = 0, queue_n = 0;
+ struct moc_plist_item *item;
+
+ pthread_mutex_lock(&data->lock);
+ for (item = data->playlist; item; item = item->next) plist_n++;
+ for (item = data->queue; item; item = item->next) queue_n++;
+ pthread_mutex_unlock(&data->lock);
+
+ fprintf(stderr, "moc: playlist sync: %d playlist item(s), %d queued\n",
+ plist_n, queue_n);
+ }
+}
+
void *moc_loop(void *input) {
@@ -257,27 +539,38 @@ void *moc_loop(void *input) {
fprintf(stderr, "moc: connecting\n");
srv_sock = moc_reconnect(data);
fprintf(stderr, "moc: connected: %d\n", srv_sock);
-
-
- int last_cmd = -1;
+ sync_playlists(srv_sock, data);
+
+ /* Server replies to our requests in the order we sent them, but it can
+ * also send unsolicited notifications (EV_CTIME, EV_STATE, ...) that each
+ * trigger a follow-up request of their own. A single "last_cmd" variable
+ * can't track more than one outstanding request, so if two notifications
+ * arrive back-to-back the second overwrites the first and the next
+ * EV_DATA reply gets matched against the wrong request, desyncing the
+ * whole stream. Use a small FIFO queue of pending request types instead. */
+ struct cmd_queue pending = { .head = 0, .tail = 0 };
int event = -1;
+ pthread_mutex_lock(&data->sock_lock);
send_int(srv_sock, CMD_GET_STATE);
- last_cmd = EV_STATE;
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, EV_STATE);
while(1) {
- get_int(srv_sock, &event);
+ get_int(srv_sock, &event);
switch (event) {
// handle our data. we MUST know what the last request was otherwise we
// don't know how to parse it.
- case EV_DATA:
- fprintf(stderr, "moc: EV_DATA\n");
+ case EV_DATA: {
+ int last_cmd = cmd_queue_pop(&pending);
if(last_cmd == EV_CTIME) {
get_int(srv_sock, &data->time);
fprintf(stderr, "moc: update ctime\n");
if(data->state == 0) {
+ pthread_mutex_lock(&data->sock_lock);
send_int(srv_sock, CMD_GET_STATE);
- last_cmd = EV_STATE;
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, EV_STATE);
}
}
// on state change, we need to figure out what file we have and what
@@ -289,122 +582,210 @@ void *moc_loop(void *input) {
update_state(data, state);
if(state != STATE_STOP) {
- char *file = get_curr_file(srv_sock);
- fprintf(stderr, "moc: current file %s\n", file);
- // if it's the same file as we had, don't update.
- if(data->filename == NULL || strcmp(file, data->filename) != 0) {
- fprintf(stderr, "moc: new file, asking for tags\n");
- data->filename = file;
- // get tags.
- send_int(srv_sock, CMD_GET_FILE_TAGS);
- send_str(srv_sock, file);
- send_int(srv_sock,TAGS_COMMENTS | TAGS_TIME);
- last_cmd = EV_FILE_TAGS;
- }
+ pthread_mutex_unlock(&data->lock);
+ // ask for the filename; the reply is handled below under
+ // PENDING_SNAME once it's actually our turn in the queue.
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(srv_sock, CMD_GET_SNAME);
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, PENDING_SNAME);
} else {
reset_data(data);
+ pthread_mutex_unlock(&data->lock);
+ }
+ }
+ // reply to our CMD_GET_SNAME request, queued above.
+ else if(last_cmd == PENDING_SNAME) {
+ char *file = get_str(srv_sock);
+ fprintf(stderr, "moc: current file %s\n", file);
+ pthread_mutex_lock(&data->lock);
+ // if it's the same file as we had, don't update.
+ if(data->filename == NULL || strcmp(file, data->filename) != 0) {
+ fprintf(stderr, "moc: new file, asking for tags\n");
+ data->filename = file;
+ // get tags.
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(srv_sock, CMD_GET_FILE_TAGS);
+ send_str(srv_sock, file);
+ send_int(srv_sock,TAGS_COMMENTS | TAGS_TIME);
+ pthread_mutex_unlock(&data->sock_lock);
+ } else {
+ free(file);
}
pthread_mutex_unlock(&data->lock);
}
else {
+ int unused;
+ get_int(srv_sock, &unused);
fprintf(stderr, "moc: UNKNOWN STATE\n");
}
- last_cmd = -1;
break;
+ }
case EV_BUSY:
- fprintf(stderr, "moc: EV_BUSY\n");
break;
case EV_CTIME:
- fprintf(stderr, "moc: EV_CTIME\n");
- last_cmd = EV_CTIME;
+ pthread_mutex_lock(&data->sock_lock);
send_int(srv_sock, CMD_GET_CTIME);
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, EV_CTIME);
break;
case EV_STATE:
- fprintf(stderr, "moc: EV_STATE\n");
- last_cmd = EV_STATE;
+ pthread_mutex_lock(&data->sock_lock);
send_int(srv_sock, CMD_GET_STATE);
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, EV_STATE);
break;
case EV_EXIT:
- fprintf(stderr,"moc: EV_EXIT\n");
+ pthread_mutex_lock(&data->lock);
data->status = RECONNECTING;
+ data->sock = -1;
reset_data(data);
+ pthread_mutex_unlock(&data->lock);
srv_sock = moc_reconnect(data);
+ pending.head = pending.tail = 0;
+ sync_playlists(srv_sock, data);
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(srv_sock, CMD_GET_STATE);
+ pthread_mutex_unlock(&data->sock_lock);
+ cmd_queue_push(&pending, EV_STATE);
break;
case EV_BITRATE:
- fprintf(stderr, "moc: EV_BITRATE\n");
break;
case EV_RATE:
- fprintf(stderr, "moc: EV_RATE\n");
break;
case EV_CHANNELS:
- printf("moc: EV_CHANNELS\n");
break;
case EV_SRV_ERROR:
get_str(srv_sock);
break;
case EV_OPTIONS:
- printf("moc: EV_OPTIONS\n");
break;
case EV_SEND_PLIST:
+ /* We never register via CMD_CAN_SEND_PLIST, so the server should
+ * never ask us to act as the playlist source. Nothing to consume. */
+ break;
+ case EV_TAGS:
+ break;
case EV_PLIST_ADD:
- case EV_PLIST_CLEAR:
+ case EV_QUEUE_ADD: {
+ struct moc_plist_item *item = recv_item(srv_sock);
+ if (item) {
+ fprintf(stderr, "moc: %s added: %s\n",
+ event == EV_PLIST_ADD ? "playlist item" : "queue item", item->file);
+ pthread_mutex_lock(&data->lock);
+ playlist_append(event == EV_PLIST_ADD ? &data->playlist : &data->queue, item);
+ pthread_mutex_unlock(&data->lock);
+
+ // freshly added items often arrive untagged (e.g. a directory add
+ // via mocp's SyncPlaylist option sends bare items before reading
+ // tags); ask for them ourselves. The reply comes back as its own
+ // EV_FILE_TAGS event, matched by filename below.
+ if (!item->title || !item->title[0]) {
+ pthread_mutex_lock(&data->sock_lock);
+ send_int(srv_sock, CMD_GET_FILE_TAGS);
+ send_str(srv_sock, item->file);
+ send_int(srv_sock, TAGS_COMMENTS | TAGS_TIME);
+ pthread_mutex_unlock(&data->sock_lock);
+ }
+ }
+ break;
+ }
case EV_PLIST_DEL:
+ case EV_QUEUE_DEL: {
+ char *file = get_str(srv_sock);
+ if (file) {
+ pthread_mutex_lock(&data->lock);
+ playlist_remove(event == EV_PLIST_DEL ? &data->playlist : &data->queue, file);
+ pthread_mutex_unlock(&data->lock);
+ free(file);
+ }
+ break;
+ }
case EV_PLIST_MOVE:
- printf("moc: EV_PLAYLIST\n");
- case EV_TAGS:
- last_cmd = EV_TAGS;
- printf("moc: EV_TAGS\n");
+ case EV_QUEUE_MOVE: {
+ char *from = get_str(srv_sock);
+ char *to = get_str(srv_sock);
+ if (from && to) {
+ pthread_mutex_lock(&data->lock);
+ playlist_swap(event == EV_PLIST_MOVE ? data->playlist : data->queue, from, to);
+ pthread_mutex_unlock(&data->lock);
+ }
+ free(from);
+ free(to);
+ break;
+ }
+ case EV_PLIST_CLEAR:
+ case EV_QUEUE_CLEAR:
+ pthread_mutex_lock(&data->lock);
+ playlist_clear(event == EV_PLIST_CLEAR ? &data->playlist : &data->queue);
+ pthread_mutex_unlock(&data->lock);
break;
case EV_STATUS_MSG:
- printf("moc: EV_STATUS_MSG\n");
- printf("moc: status update: %s\n", get_str(srv_sock));
+ free(get_str(srv_sock));
break;
case EV_MIXER_CHANGE:
- printf("moc: EV_MIXER_CHANGE\n");
break;
- case EV_FILE_TAGS:
- printf("moc: EV_FILE_TAGS\n");
+ case EV_FILE_TAGS: {
+ // EV_FILE_TAGS answers whichever CMD_GET_FILE_TAGS request matches
+ // this filename - that could be the "now playing" request from the
+ // EV_STATE/PENDING_SNAME flow above, or one of the lazy per-item
+ // requests we fire off in EV_PLIST_ADD/EV_QUEUE_ADD. There's no
+ // correlation id in the protocol, so we match by filename content.
+ char *filename = get_str(srv_sock);
+ char *title = get_str(srv_sock);
+ char *artist = get_str(srv_sock);
+ char *album = get_str(srv_sock);
+ int track = -1, length = -1, filled = 0;
+ get_int(srv_sock, &track);
+ get_int(srv_sock, &length);
+ get_int(srv_sock, &filled);
pthread_mutex_lock(&data->lock);
- if (!(data->filename = get_str(srv_sock))) {
- fprintf(stderr, "Error while receiving filename\n");
- }
-
- if (!(data->title = get_str(srv_sock))) {
- fprintf(stderr, "Error while receiving title\n");
- }
-
- if (!(data->artist = get_str(srv_sock))) {
- fprintf(stderr, "Error while receiving artist\n");
- }
-
- if (!(data->album = get_str(srv_sock))) {
- fprintf(stderr, "Error while receiving album\n");
- }
-
- if (!get_int(srv_sock, &data->track)) {
- fprintf(stderr, "Error while receiving track\n");
- }
-
- if (!get_int(srv_sock, &data->length)) {
- fprintf(stderr, "Error while receiving length\n");
- }
-
- if (!get_int(srv_sock, &data->filled)) {
- fprintf(stderr, "Error while receiving filled\n");
+ if (filename && data->filename && !strcmp(filename, data->filename)) {
+ free(data->title);
+ free(data->artist);
+ free(data->album);
+ data->title = title;
+ data->artist = artist;
+ data->album = album;
+ data->track = track;
+ data->length = length;
+ data->filled = filled;
+ free(filename);
+ } else if (filename) {
+ struct moc_plist_item *item = playlist_find(data->playlist, filename);
+ if (!item)
+ item = playlist_find(data->queue, filename);
+ if (item) {
+ free(item->title);
+ free(item->artist);
+ free(item->album);
+ item->title = title;
+ item->artist = artist;
+ item->album = album;
+ item->track = track;
+ item->time = length;
+ } else {
+ free(title);
+ free(artist);
+ free(album);
+ }
+ free(filename);
+ } else {
+ free(title);
+ free(artist);
+ free(album);
}
pthread_mutex_unlock(&data->lock);
break;
+ }
case EV_AVG_BITRATE:
- printf("moc: EV_AVG_BITRATE\n");
break;
case EV_AUDIO_START:
- printf("moc: AUDIO START\n");
break;
case EV_AUDIO_STOP:
- printf("moc: AUDIO STOP\n");
break;
default:
fprintf(stderr, "Unknown event: 0x%02x!\n", event);
@@ -416,7 +797,7 @@ void *moc_loop(void *input) {
extern struct moc *moc_init() {
- struct moc *moc = (struct moc*)malloc(sizeof(struct moc));
+ struct moc *moc = (struct moc*)calloc(1, sizeof(struct moc));
reset_data(moc);
pthread_t thread_id;
@@ -424,8 +805,134 @@ extern struct moc *moc_init() {
printf("\n mutex init has failed\n");
return NULL;
}
+ if (pthread_mutex_init(&moc->sock_lock, NULL) != 0) {
+ printf("\n mutex init has failed\n");
+ return NULL;
+ }
moc->status = INITIALIZING;
+ moc->sock = -1;
pthread_create(&thread_id, NULL, moc_loop, (void *)moc);
return moc;
}
+
+/* Grab the current socket fd, or -1 if we're disconnected/reconnecting. */
+static int moc_sock (struct moc *mh)
+{
+ int sock;
+
+ pthread_mutex_lock(&mh->lock);
+ sock = mh->sock;
+ pthread_mutex_unlock(&mh->lock);
+
+ return sock;
+}
+
+void moc_play (struct moc *mh, const char *file)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_PLAY);
+ send_str(sock, file ? file : "");
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_pause (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_PAUSE);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_unpause (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_UNPAUSE);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_stop (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_STOP);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_next (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_NEXT);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_prev (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_PREV);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+/* Register file with the server's actual playback list (so CMD_PLAY can
+ * find it later) and broadcast it to every client's synced playlist view,
+ * including our own - we'll pick it up the normal way via EV_PLIST_ADD. */
+void moc_playlist_add (struct moc *mh, const char *file)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_LIST_ADD);
+ send_str(sock, file);
+ send_int(sock, CMD_CLI_PLIST_ADD);
+ send_bare_item(sock, file);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+/* Broadcast removal of a single playlist entry; we'll pick up the change
+ * ourselves the normal way via EV_PLIST_DEL. */
+void moc_playlist_remove (struct moc *mh, const char *file)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_CLI_PLIST_DEL);
+ send_str(sock, file);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
+
+void moc_playlist_clear (struct moc *mh)
+{
+ int sock = moc_sock(mh);
+ if (sock < 0)
+ return;
+
+ pthread_mutex_lock(&mh->sock_lock);
+ send_int(sock, CMD_CLI_PLIST_CLEAR);
+ pthread_mutex_unlock(&mh->sock_lock);
+}
diff --git a/moc_library.h b/moc_library.h
index c545927..d97d8be 100644
--- a/moc_library.h
+++ b/moc_library.h
@@ -25,10 +25,23 @@ enum moc_status {
INITIALIZING,
CONNECTED,
FAILED_TO_CONNECT,
- ERROR
+ ERROR,
+ RECONNECTING
};
-struct moc {
+/* One entry in the server's playlist or queue. A singly-linked list hangs
+ * off struct moc's playlist/queue fields. */
+struct moc_plist_item {
+ char *file;
+ char *title;
+ char *artist;
+ char *album;
+ int track;
+ int time;
+ struct moc_plist_item *next;
+};
+
+struct moc {
pthread_mutex_t lock;
enum moc_status status;
enum moc_state state;
@@ -41,9 +54,31 @@ struct moc {
int time;
int filled;
int length;
+
+ struct moc_plist_item *playlist;
+ struct moc_plist_item *queue;
+
+ /* The background thread owns the actual reads; sock/sock_lock let any
+ * other thread send commands without racing/interleaving with it or with
+ * the background thread's own internal requests. */
+ int sock;
+ pthread_mutex_t sock_lock;
};
extern const char * moc_str_status(enum moc_status s);
extern const char * moc_str_state(enum moc_state s);
extern struct moc * moc_init();
+/* Commands a UI can call from any thread to control playback or modify the
+ * playlist. Safe to call concurrently with each other and with the
+ * background thread; each is a single atomic request to the server. */
+extern void moc_play(struct moc *mh, const char *file);
+extern void moc_pause(struct moc *mh);
+extern void moc_unpause(struct moc *mh);
+extern void moc_stop(struct moc *mh);
+extern void moc_next(struct moc *mh);
+extern void moc_prev(struct moc *mh);
+extern void moc_playlist_add(struct moc *mh, const char *file);
+extern void moc_playlist_remove(struct moc *mh, const char *file);
+extern void moc_playlist_clear(struct moc *mh);
+
diff --git a/moc_x11.c b/moc_x11.c
index efff073..3ec7c50 100644
--- a/moc_x11.c
+++ b/moc_x11.c
@@ -93,7 +93,7 @@ int main() {
}
int s = DefaultScreen(dpy);
- Window win = XCreateSimpleWindow(dpy, RootWindow(dpy, s), 10, 10, 660, 200, 1,
+ Window win = XCreateSimpleWindow(dpy, RootWindow(dpy, s), 10, 10, 660, 600, 1,
BlackPixel(dpy, s), WhitePixel(dpy, s));
XSelectInput(dpy, win, ExposureMask | ButtonPress | KeyPressMask);
XMapWindow(dpy, win);
@@ -152,6 +152,18 @@ int main() {
}
+
+ y_offset += 10;
+ XDrawString(dpy, win, DefaultGC(dpy, s), x, y_offset, "Playlist:", 9);
+ y_offset += 15;
+ {
+ struct moc_plist_item *item;
+ for (item = mh->playlist; item; item = item->next) {
+ const char *label = (item->title && item->title[0]) ? item->title : item->file;
+ XDrawString(dpy, win, DefaultGC(dpy, s), x, y_offset, label, strlen(label));
+ y_offset += 15;
+ }
+ }
pthread_mutex_unlock(&mh->lock);
}
diff --git a/moc_xaw.c b/moc_xaw.c
new file mode 100644
index 0000000..25a6587
--- /dev/null
+++ b/moc_xaw.c
@@ -0,0 +1,596 @@
+/* Old-school Athena widget UI for libmoc: now-playing info, transport
+ * controls, a local directory browser, and the live-synced playlist. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <dirent.h>
+#include <limits.h>
+#include <pthread.h>
+#include <sys/stat.h>
+
+#include <X11/Intrinsic.h>
+#include <X11/StringDefs.h>
+#include <X11/Shell.h>
+#include <X11/Xaw/Form.h>
+#include <X11/Xaw/Label.h>
+#include <X11/Xaw/Command.h>
+#include <X11/Xaw/List.h>
+#include <X11/Xaw/Viewport.h>
+
+#include "moc_library.h"
+
+#define REFRESH_MS 250
+
+struct app {
+ struct moc *mh;
+
+ Widget now_label;
+ Widget dir_list;
+ Widget plist_list;
+
+ char cwd[PATH_MAX];
+ char **dir_strs;
+ int dir_count;
+ char *selected_path; /* full path of the last-clicked dir-browser entry */
+ int selected_is_dir;
+ int last_click_index; /* for double-click detection */
+ Time last_click_time;
+
+ char **plist_strs;
+ char **plist_files; /* parallel array: plist_strs[i] is a label (possibly
+ just the tag title), plist_files[i] is always the
+ real path, needed for play/remove */
+ int plist_count;
+ char *last_playlist_filename; /* mh->filename as of the last refresh, to
+ detect when the highlighted now-playing
+ entry needs to move */
+ char *plist_selected_file;
+ int plist_last_click_index;
+ Time plist_last_click_time;
+
+ Widget toplevel;
+};
+
+/* mocp's decoder plugins cover roughly this set; anything else found while
+ * recursively adding a directory is almost certainly not a track. */
+static const char *sound_extensions[] = {
+ ".mp3", ".ogg", ".oga", ".flac", ".wav", ".m4a", ".aac",
+ ".wma", ".opus", ".ape", ".mpc", ".wv", ".mid", ".midi", NULL
+};
+
+static int is_sound_file (const char *path)
+{
+ const char *dot = strrchr(path, '.');
+ int i;
+
+ if (!dot)
+ return 0;
+ for (i = 0; sound_extensions[i]; i++)
+ if (!strcasecmp(dot, sound_extensions[i]))
+ return 1;
+ return 0;
+}
+
+static char *time_format (int sec)
+{
+ char *str = malloc(32);
+ if (sec < 0) sec = 0;
+ snprintf(str, 32, "%d:%02d", sec / 60, sec % 60);
+ return str;
+}
+
+static void free_str_array (char **arr, int count)
+{
+ int i;
+ if (!arr) return;
+ for (i = 0; i < count; i++) free(arr[i]);
+ free(arr);
+}
+
+/* Pick a starting directory: an explicit argv[1], failing that wherever
+ * mocp itself last left off (same ~/.moc/last_directory file it reads via
+ * read_last_dir() in interface.c), failing that $HOME. */
+static void pick_start_dir (struct app *app, int argc, char **argv)
+{
+ struct stat st;
+ char *home = getenv("HOME");
+
+ if (argc > 1 && stat(argv[1], &st) == 0 && S_ISDIR(st.st_mode)) {
+ strncpy(app->cwd, argv[1], sizeof(app->cwd) - 1);
+ return;
+ }
+
+ if (home) {
+ char last_dir_path[PATH_MAX];
+ FILE *f;
+
+ snprintf(last_dir_path, sizeof(last_dir_path), "%s/.moc/last_directory", home);
+ if ((f = fopen(last_dir_path, "r"))) {
+ size_t n = fread(app->cwd, 1, sizeof(app->cwd) - 1, f);
+ fclose(f);
+ if (n > 0) {
+ app->cwd[n] = '\0';
+ if (stat(app->cwd, &st) == 0 && S_ISDIR(st.st_mode))
+ return;
+ }
+ }
+ }
+
+ strncpy(app->cwd, home ? home : "/", sizeof(app->cwd) - 1);
+}
+
+static int dirent_filter (const struct dirent *d)
+{
+ /* Skip dotfiles, but keep "." (so the current directory itself is
+ * selectable for Add) and ".." (so we can navigate up). */
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ return 1;
+ if (d->d_name[0] == '.')
+ return 0;
+ return 1;
+}
+
+/* Re-read app->cwd and rebuild the left-pane list. */
+static void reload_dir (struct app *app)
+{
+ struct dirent **entries;
+ int n, i;
+ char path[PATH_MAX];
+
+ n = scandir(app->cwd, &entries, dirent_filter, alphasort);
+ if (n < 0) {
+ fprintf(stderr, "moc_xaw: can't read directory %s\n", app->cwd);
+ return;
+ }
+
+ free_str_array(app->dir_strs, app->dir_count);
+ free(app->selected_path);
+ app->selected_path = NULL;
+ app->selected_is_dir = 0;
+
+ /* XawListChange requires a NULL-terminated array when nitems <= 0, so
+ * always leave room for (and set) a trailing NULL even when n > 0. */
+ app->dir_strs = malloc(sizeof(char*) * (n + 1));
+ app->dir_strs[n] = NULL;
+ app->dir_count = n;
+
+ for (i = 0; i < n; i++) {
+ int is_dir;
+ struct stat st;
+
+ snprintf(path, sizeof(path), "%s/%s",
+ strcmp(app->cwd, "/") ? app->cwd : "", entries[i]->d_name);
+ is_dir = (stat(path, &st) == 0 && S_ISDIR(st.st_mode));
+
+ app->dir_strs[i] = malloc(strlen(entries[i]->d_name) + 2);
+ sprintf(app->dir_strs[i], "%s%s", entries[i]->d_name, is_dir ? "/" : "");
+ free(entries[i]);
+ }
+ free(entries);
+
+ XawListChange(app->dir_list, app->dir_strs, app->dir_count, 0, True);
+ XawListUnhighlight(app->dir_list);
+}
+
+/* Recursively walk path, adding every file that looks like a track to the
+ * playlist - the directory-browser equivalent of mocp's shift-A. */
+static void add_dir_recursive (struct app *app, const char *path)
+{
+ struct dirent **entries;
+ int n, i;
+
+ n = scandir(path, &entries, dirent_filter, alphasort);
+ if (n < 0)
+ return;
+
+ for (i = 0; i < n; i++) {
+ char child[PATH_MAX];
+ struct stat st;
+
+ /* dirent_filter lets "." and ".." through for the browser's benefit
+ * (so "." is selectable for Add), but recursing into either here would
+ * just loop forever on the same directory. */
+ if (!strcmp(entries[i]->d_name, ".") || !strcmp(entries[i]->d_name, ".."))
+ continue;
+
+ snprintf(child, sizeof(child), "%s/%s", path, entries[i]->d_name);
+ if (stat(child, &st) != 0)
+ continue;
+
+ if (S_ISDIR(st.st_mode))
+ add_dir_recursive(app, child);
+ else if (S_ISREG(st.st_mode) && is_sound_file(child))
+ moc_playlist_add(app->mh, child);
+ }
+
+ for (i = 0; i < n; i++)
+ free(entries[i]);
+ free(entries);
+}
+
+static void navigate_to (struct app *app, const char *name)
+{
+ char path[PATH_MAX];
+
+ if (!strcmp(name, "..")) {
+ char *slash = strrchr(app->cwd, '/');
+ if (slash && slash != app->cwd)
+ *slash = '\0';
+ else
+ strcpy(app->cwd, "/");
+ } else {
+ size_t len = strlen(name);
+ char trimmed[PATH_MAX];
+ strncpy(trimmed, name, sizeof(trimmed) - 1);
+ trimmed[sizeof(trimmed) - 1] = '\0';
+ if (len > 0 && trimmed[len - 1] == '/')
+ trimmed[len - 1] = '\0';
+ snprintf(path, sizeof(path), "%s/%s",
+ strcmp(app->cwd, "/") ? app->cwd : "", trimmed);
+ strncpy(app->cwd, path, sizeof(app->cwd) - 1);
+ }
+ reload_dir(app);
+}
+
+/* Single click selects an entry (so the Add button knows what to act on)
+ * without leaving the current directory. Clicking the same entry again
+ * within DOUBLE_CLICK_MS is treated as a double-click, which descends into
+ * a directory (".." always navigates immediately - there's nothing else a
+ * single click on it could mean). */
+#define DOUBLE_CLICK_MS 500
+
+static void dir_list_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ XawListReturnStruct *ret = (XawListReturnStruct *)call_data;
+ char *name = ret->string;
+ size_t len;
+ char path[PATH_MAX];
+ Time now;
+ int is_double;
+
+ if (ret->list_index == XAW_LIST_NONE || !name)
+ return;
+
+ if (!strcmp(name, "..")) {
+ navigate_to(app, name);
+ return;
+ }
+
+ now = XtLastTimestampProcessed(XtDisplay(w));
+ is_double = (ret->list_index == app->last_click_index &&
+ (now - app->last_click_time) < DOUBLE_CLICK_MS);
+ app->last_click_index = ret->list_index;
+ app->last_click_time = now;
+
+ len = strlen(name);
+ if (is_double && len > 0 && name[len - 1] == '/') {
+ navigate_to(app, name);
+ return;
+ }
+
+ free(app->selected_path);
+ snprintf(path, sizeof(path), "%s/%s",
+ strcmp(app->cwd, "/") ? app->cwd : "", name);
+ if (len > 0 && name[len - 1] == '/')
+ path[strlen(path) - 1] = '\0'; /* drop the trailing marker slash */
+ app->selected_path = strdup(path);
+ app->selected_is_dir = (len > 0 && name[len - 1] == '/');
+}
+
+static void add_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+
+ if (!app->selected_path)
+ return;
+
+ if (app->selected_is_dir)
+ add_dir_recursive(app, app->selected_path);
+ else
+ moc_playlist_add(app->mh, app->selected_path);
+}
+
+static void play_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ moc_play(app->mh, "");
+}
+
+static void pause_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ if (app->mh->state == PAUSED)
+ moc_unpause(app->mh);
+ else
+ moc_pause(app->mh);
+}
+
+static void stop_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ moc_stop(app->mh);
+}
+
+static void next_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ moc_next(app->mh);
+}
+
+static void prev_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ moc_prev(app->mh);
+}
+
+/* Single click selects (so Remove knows what to act on); double click
+ * (same row, within DOUBLE_CLICK_MS) plays that track immediately. */
+static void plist_list_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+ XawListReturnStruct *ret = (XawListReturnStruct *)call_data;
+ Time now;
+ int is_double;
+ const char *file;
+
+ if (ret->list_index == XAW_LIST_NONE || ret->list_index >= app->plist_count)
+ return;
+ file = app->plist_files[ret->list_index];
+
+ now = XtLastTimestampProcessed(XtDisplay(w));
+ is_double = (ret->list_index == app->plist_last_click_index &&
+ (now - app->plist_last_click_time) < DOUBLE_CLICK_MS);
+ app->plist_last_click_index = ret->list_index;
+ app->plist_last_click_time = now;
+
+ free(app->plist_selected_file);
+ app->plist_selected_file = strdup(file);
+
+ if (is_double)
+ moc_play(app->mh, file);
+}
+
+static void remove_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+
+ if (!app->plist_selected_file)
+ return;
+ moc_playlist_remove(app->mh, app->plist_selected_file);
+ free(app->plist_selected_file);
+ app->plist_selected_file = NULL;
+}
+
+static void clear_callback (Widget w, XtPointer client_data, XtPointer call_data)
+{
+ struct app *app = (struct app *)client_data;
+
+ moc_playlist_clear(app->mh);
+ free(app->plist_selected_file);
+ app->plist_selected_file = NULL;
+}
+
+static void refresh_now_playing (struct app *app)
+{
+ char buf[1024];
+ char title[1024];
+ char *t, *l;
+
+ if (app->mh->status != CONNECTED) {
+ snprintf(buf, sizeof(buf), "%s", moc_str_status(app->mh->status));
+ XtVaSetValues(app->now_label, XtNlabel, buf, NULL);
+ XtVaSetValues(app->toplevel, XtNtitle, "moc_xaw", NULL);
+ return;
+ }
+
+ if (app->mh->state != PLAYING && app->mh->state != PAUSED) {
+ snprintf(buf, sizeof(buf), "%s", moc_str_state(app->mh->state));
+ XtVaSetValues(app->now_label, XtNlabel, buf, NULL);
+ XtVaSetValues(app->toplevel, XtNtitle, "moc_xaw", NULL);
+ return;
+ }
+
+ t = time_format(app->mh->time);
+ l = time_format(app->mh->length);
+ snprintf(buf, sizeof(buf), "%s\n%s\n%s\n%s\n%s / %s",
+ moc_str_state(app->mh->state),
+ app->mh->title ? app->mh->title : "",
+ app->mh->artist ? app->mh->artist : "",
+ app->mh->album ? app->mh->album : "",
+ t, l);
+ free(t);
+ free(l);
+
+ XtVaSetValues(app->now_label, XtNlabel, buf, NULL);
+
+ /* mirrors mocp's own ncurses title bar, e.g. "MOC [play] - Artist - Title" */
+ snprintf(title, sizeof(title), "moc_xaw [%s] - %s%s%s",
+ app->mh->state == PAUSED ? "paused" : "playing",
+ app->mh->artist ? app->mh->artist : "",
+ (app->mh->artist && app->mh->title) ? " - " : "",
+ app->mh->title ? app->mh->title : "");
+ XtVaSetValues(app->toplevel, XtNtitle, title, NULL);
+}
+
+/* XawListChange unconditionally rebuilds the widget's internal state and
+ * resets scroll position, and re-highlighting every tick would undo
+ * whatever row the user just clicked on. So: only touch the list when its
+ * contents actually changed, and only move the highlight when the
+ * now-playing file itself changes - not on every 250ms refresh. */
+static void refresh_playlist (struct app *app)
+{
+ struct moc_plist_item *item;
+ int n = 0, i;
+ char **new_strs, **new_files;
+ int changed;
+
+ for (item = app->mh->playlist; item; item = item->next) n++;
+
+ new_strs = malloc(sizeof(char*) * (n + 1));
+ new_strs[n] = NULL;
+ new_files = malloc(sizeof(char*) * (n + 1));
+ new_files[n] = NULL;
+ for (item = app->mh->playlist, i = 0; item; item = item->next, i++) {
+ const char *label = (item->title && item->title[0]) ? item->title : item->file;
+ int is_playing = app->mh->filename && !strcmp(item->file, app->mh->filename);
+ char buf[1024];
+
+ snprintf(buf, sizeof(buf), "%s%s", is_playing ? "> " : "", label);
+ new_strs[i] = strdup(buf);
+ new_files[i] = strdup(item->file);
+ }
+
+ changed = (n != app->plist_count);
+ if (!changed)
+ for (i = 0; i < n; i++)
+ if (strcmp(new_strs[i], app->plist_strs[i]) != 0) { changed = 1; break; }
+
+ if (changed) {
+ free_str_array(app->plist_strs, app->plist_count);
+ free_str_array(app->plist_files, app->plist_count);
+ app->plist_strs = new_strs;
+ app->plist_files = new_files;
+ app->plist_count = n;
+ XawListChange(app->plist_list, app->plist_strs, app->plist_count, 0, True);
+ } else {
+ free_str_array(new_strs, n);
+ free_str_array(new_files, n);
+ }
+
+ if (!app->mh->filename) {
+ free(app->last_playlist_filename);
+ app->last_playlist_filename = NULL;
+ } else if (!app->last_playlist_filename ||
+ strcmp(app->mh->filename, app->last_playlist_filename) != 0) {
+ int playing_index = -1;
+
+ for (item = app->mh->playlist, i = 0; item; item = item->next, i++)
+ if (!strcmp(item->file, app->mh->filename)) { playing_index = i; break; }
+ if (playing_index >= 0)
+ XawListHighlight(app->plist_list, playing_index);
+
+ free(app->last_playlist_filename);
+ app->last_playlist_filename = strdup(app->mh->filename);
+ }
+}
+
+static void do_refresh (struct app *app)
+{
+ pthread_mutex_lock(&app->mh->lock);
+ refresh_now_playing(app);
+ refresh_playlist(app);
+ pthread_mutex_unlock(&app->mh->lock);
+}
+
+static void timer_callback (XtPointer client_data, XtIntervalId *id)
+{
+ struct app *app = (struct app *)client_data;
+
+ do_refresh(app);
+
+ XtAppAddTimeOut(XtWidgetToApplicationContext(app->now_label),
+ REFRESH_MS, timer_callback, client_data);
+}
+
+int main (int argc, char **argv)
+{
+ XtAppContext ctx;
+ Widget toplevel, form, prev_b, play_b, pause_b, stop_b, next_b, add_b,
+ remove_b, clear_b,
+ now_label, dir_view, dir_list, plist_view, plist_list;
+ struct app app;
+
+ memset(&app, 0, sizeof(app));
+ app.mh = moc_init();
+ if (!app.mh) {
+ fprintf(stderr, "moc_xaw: failed to initialize library\n");
+ return 1;
+ }
+
+ pick_start_dir(&app, argc, argv);
+ app.plist_count = -1; /* sentinel: forces the first refresh_playlist()
+ call to actually sync the (possibly empty) list
+ widget instead of leaving its uninitialized
+ placeholder content on screen */
+
+ toplevel = XtAppInitialize(&ctx, "MocXaw", NULL, 0, &argc, argv, NULL, NULL, 0);
+ XtVaSetValues(toplevel, XtNtitle, "moc_xaw", XtNiconName, "moc_xaw", NULL);
+ app.toplevel = toplevel;
+
+ form = XtVaCreateManagedWidget("form", formWidgetClass, toplevel, NULL);
+
+ now_label = XtVaCreateManagedWidget("nowPlaying", labelWidgetClass, form,
+ XtNlabel, "connecting...",
+ XtNwidth, 660,
+ XtNheight, 120,
+ XtNjustify, XtJustifyLeft,
+ XtNresize, False,
+ NULL);
+ app.now_label = now_label;
+
+ prev_b = XtVaCreateManagedWidget("prev", commandWidgetClass, form,
+ XtNlabel, "<<", XtNfromVert, now_label, NULL);
+ play_b = XtVaCreateManagedWidget("play", commandWidgetClass, form,
+ XtNlabel, ">", XtNfromVert, now_label, XtNfromHoriz, prev_b, NULL);
+ pause_b = XtVaCreateManagedWidget("pause", commandWidgetClass, form,
+ XtNlabel, "||", XtNfromVert, now_label, XtNfromHoriz, play_b, NULL);
+ stop_b = XtVaCreateManagedWidget("stop", commandWidgetClass, form,
+ XtNlabel, "[]", XtNfromVert, now_label, XtNfromHoriz, pause_b, NULL);
+ next_b = XtVaCreateManagedWidget("next", commandWidgetClass, form,
+ XtNlabel, ">>", XtNfromVert, now_label, XtNfromHoriz, stop_b, NULL);
+
+ dir_view = XtVaCreateManagedWidget("dirView", viewportWidgetClass, form,
+ XtNfromVert, prev_b,
+ XtNwidth, 300, XtNheight, 300,
+ XtNallowVert, True,
+ NULL);
+ dir_list = XtVaCreateManagedWidget("dirList", listWidgetClass, dir_view,
+ XtNdefaultColumns, 1, XtNforceColumns, True,
+ NULL);
+ app.dir_list = dir_list;
+
+ add_b = XtVaCreateManagedWidget("add", commandWidgetClass, form,
+ XtNlabel, "Add =>",
+ XtNfromVert, prev_b, XtNfromHoriz, dir_view,
+ NULL);
+ remove_b = XtVaCreateManagedWidget("remove", commandWidgetClass, form,
+ XtNlabel, "Remove",
+ XtNfromVert, add_b, XtNfromHoriz, dir_view,
+ NULL);
+ clear_b = XtVaCreateManagedWidget("clear", commandWidgetClass, form,
+ XtNlabel, "Clear",
+ XtNfromVert, remove_b, XtNfromHoriz, dir_view,
+ NULL);
+
+ plist_view = XtVaCreateManagedWidget("plistView", viewportWidgetClass, form,
+ XtNfromVert, prev_b, XtNfromHoriz, add_b,
+ XtNwidth, 300, XtNheight, 300,
+ XtNallowVert, True,
+ NULL);
+ plist_list = XtVaCreateManagedWidget("plistList", listWidgetClass, plist_view,
+ XtNdefaultColumns, 1, XtNforceColumns, True,
+ NULL);
+ app.plist_list = plist_list;
+
+ XtAddCallback(dir_list, XtNcallback, dir_list_callback, &app);
+ XtAddCallback(add_b, XtNcallback, add_callback, &app);
+ XtAddCallback(plist_list, XtNcallback, plist_list_callback, &app);
+ XtAddCallback(remove_b, XtNcallback, remove_callback, &app);
+ XtAddCallback(clear_b, XtNcallback, clear_callback, &app);
+ XtAddCallback(play_b, XtNcallback, play_callback, &app);
+ XtAddCallback(pause_b, XtNcallback, pause_callback, &app);
+ XtAddCallback(stop_b, XtNcallback, stop_callback, &app);
+ XtAddCallback(next_b, XtNcallback, next_callback, &app);
+ XtAddCallback(prev_b, XtNcallback, prev_callback, &app);
+
+ XtRealizeWidget(toplevel);
+
+ reload_dir(&app);
+ do_refresh(&app);
+ XtAppAddTimeOut(ctx, REFRESH_MS, timer_callback, &app);
+
+ XtAppMainLoop(ctx);
+
+ return 0;
+}