diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-06-22 14:43:39 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-06-22 14:43:39 -0400 |
| commit | c7e71230f74415c3f56220d29701780e6f1a9fdd (patch) | |
| tree | 7f8869fe64d3fd63c06df8ffc76b9ab1b0a58035 | |
| parent | cbc81ba76807acefecaa7cc60853fd7b35853266 (diff) | |
Extends moc_library with playlist tracking and thread-safe playback
commands (play/pause/stop/next/prev/playlist add/remove/clear), and
adds moc_xaw.c, an Athena widget UI with a directory browser and live
playlist view. Also taller default window and playlist rendering in
moc_x11.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | moc_library.c | 741 | ||||
| -rw-r--r-- | moc_library.h | 39 | ||||
| -rw-r--r-- | moc_x11.c | 14 | ||||
| -rw-r--r-- | moc_xaw.c | 596 |
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); + @@ -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; +} |
