summaryrefslogtreecommitdiff
path: root/src/fruits/cmsrv
diff options
context:
space:
mode:
Diffstat (limited to 'src/fruits/cmsrv')
-rw-r--r--src/fruits/cmsrv/cmsrv.c196
-rw-r--r--src/fruits/cmsrv/meson.build19
-rw-r--r--src/fruits/cmsrv/ui.c210
-rw-r--r--src/fruits/cmsrv/ui.h44
4 files changed, 469 insertions, 0 deletions
diff --git a/src/fruits/cmsrv/cmsrv.c b/src/fruits/cmsrv/cmsrv.c
new file mode 100644
index 0000000..08f67dd
--- /dev/null
+++ b/src/fruits/cmsrv/cmsrv.c
@@ -0,0 +1,196 @@
+#define CAMU_LOCAL_SOCKET
+
+#include <al/log.h>
+#include <aki/common.h>
+#ifdef CAMU_LOCAL_SOCKET
+#include <aki/line_processor.h>
+#endif
+#include <aki/timer.h>
+
+#include "../../server/server.h"
+#include "../../server/list.h"
+#include "../../server/common.h"
+#include "../../cache/handlers/cdio.h"
+#include "../../codec/ffmpeg/common.h"
+#ifdef CAMU_LOCAL_SOCKET
+#include "../../server/local_compat.h"
+#endif
+
+#include "ui.h"
+
+struct cmsrv {
+ struct aki_event_loop loop;
+ struct aki_signal quit_signal;
+ struct camu_server server;
+#ifdef CAMU_LOCAL_SOCKET
+ struct {
+ struct aki_socket sock;
+ struct aki_line_processor cli;
+ struct camu_local_compat compat;
+ } local;
+#endif
+ struct cmsrv_ui ui;
+ struct aki_poll input_poll;
+ struct aki_timer render_timer;
+};
+
+#ifdef CAMU_LOCAL_SOCKET
+static u8 server_line_callback(void *userdata, str *line)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ struct camu_list *list = al_array_at(s->server.lists, 0);
+ if (al_str_eq(line, al_str_c(";PAUSE"))) {
+ lia_list_toggle_pause(&list->impl, LIANA_SEQUENCE_ANY, -1.0);
+ } else if (al_str_eq(line, al_str_c(";NEXT"))) {
+ lia_list_skip(&list->impl, LIANA_SEQUENCE_ANY, 1);
+ } else if (al_str_eq(line, al_str_c(";PREV"))) {
+ lia_list_skip(&list->impl, LIANA_SEQUENCE_ANY, -1);
+ } else if (al_str_eq(line, al_str_c(";SHUFFLE"))) {
+ lia_list_shuffle(&list->impl);
+ } else if (al_str_eq(line, al_str_c(";SORT"))) {
+ lia_list_sort(&list->impl);
+ } else if (al_str_eq(line, al_str_c(";REVERSE"))) {
+ lia_list_reverse(&list->impl);
+ } else if (al_str_eq(line, al_str_c(";CLEAR"))) {
+ lia_list_clear(&list->impl);
+ } else {
+ struct aki_packet *packet = aki_packet_create();
+ aki_packet_write_str(packet, line);
+ camu_server_local_add(&s->server, packet);
+ }
+ return AKI_LINE_PROCESSOR_CONTINUE;
+}
+#endif
+
+static void meta_callback(void *userdata, u8 op, struct lia_list_entry *entry)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ (void)s;
+ switch (op) {
+ case LIANA_META_PLAYING: {
+ struct camu_server_resource *resource = (struct camu_server_resource *)entry->opaque;
+#ifdef CAMU_HAVE_PORTAL
+ if (resource->post) {
+ al_log_info("server", "now playing: %.*ls.", AL_WSTR_PRINTF(&resource->post->title));
+ } else {
+#endif
+ al_log_info("server", "now playing: %.*s.", AL_STR_PRINTF(&resource->unique_id));
+#ifdef CAMU_HAVE_PORTAL
+ }
+#endif
+ break;
+ }
+ }
+}
+
+static void render_timer_callback(void *userdata, struct aki_timer *timer)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ (void)timer;
+ cmsrv_ui_render(&s->ui);
+ aki_timer_again(&s->render_timer);
+}
+
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ (void)revents;
+ struct ncinput input;
+ do {
+ if (!cmsrv_ui_read_input(&s->ui, &input)) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ switch (input.id) {
+ case 'q':
+ aki_event_loop_break_one(&s->loop);
+ break;
+ case '1':
+ cmsrv_ui_set_pane(&s->ui, CMSRV_UI_LISTS);
+ break;
+ case '2':
+ cmsrv_ui_set_pane(&s->ui, CMSRV_UI_RESOURCES);
+ break;
+ case '3':
+ cmsrv_ui_set_pane(&s->ui, CMSRV_UI_NODES);
+ break;
+ }
+ }
+ } while (1);
+}
+
+static void quit_signal_callback(void *userdata)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ aki_event_loop_break_one(&s->loop);
+}
+
+static s32 log_callback(void *userdata, char *message)
+{
+ struct cmsrv *s = (struct cmsrv *)userdata;
+ cmsrv_ui_push_message(&s->ui, message);
+ return al_strlen(message);
+}
+
+static struct cmsrv s = { 0 };
+
+static void sigint_handler(s32 signum)
+{
+ (void)signum;
+ aki_signal_send(&s.quit_signal);
+}
+
+#ifndef _WIN32
+s32 main(s32 argc, char *argv[])
+#else
+s32 wmain(s32 argc, wchar_t **argv)
+#endif
+{
+ (void)argc;
+ (void)argv;
+ if (!aki_common_init() || !cmsrv_ui_init(&s.ui, &s.server)) return EXIT_FAILURE;
+
+ signal(SIGINT, sigint_handler);
+
+ al_set_print(log_callback, &s);
+
+ camu_ff_set_default_log_callback();
+
+ aki_event_loop_init(&s.loop);
+
+ aki_signal_init(&s.quit_signal, quit_signal_callback, &s);
+ aki_signal_start(&s.quit_signal, &s.loop);
+
+ if (!camu_server_init(&s.server, CAMU_LOCAL_TYPE, &s.loop)) return EXIT_FAILURE;
+ camu_server_listen(&s.server, CAMU_LOCAL_ADDR, CAMU_PORT);
+ struct camu_list *list = al_array_at(s.server.lists, 0);
+ list->impl.callback = meta_callback;
+ list->impl.userdata = &s;
+
+#ifdef CAMU_LOCAL_SOCKET
+ s.local.sock.type = AKI_SOCKET_UNIX;
+ aki_socket_init(&s.local.sock);
+ aki_socket_set_blocking(&s.local.sock, false);
+ s.local.cli.callback = server_line_callback;
+ s.local.cli.userdata = &s;
+ aki_line_processor_init(&s.local.cli, al_str_c("\n"));
+ aki_line_processor_open_socket(&s.local.cli, &s.local.sock);
+ if (aki_socket_bind(&s.local.sock, CAMU_UNIX_LOCAL, 0) && aki_socket_listen(&s.local.sock)) {
+ aki_line_processor_run(&s.local.cli, &s.loop);
+ }
+#endif
+
+ aki_poll_init(&s.input_poll, input_poll_callback, &s);
+ aki_poll_set(&s.input_poll, cmsrv_ui_get_input_fd(&s.ui), AKI_POLL_READ);
+ aki_poll_start(&s.input_poll, &s.loop);
+
+ aki_timer_init(&s.render_timer, &s.loop, render_timer_callback, &s);
+ aki_timer_set_repeat(&s.render_timer, AKI_TS_FROM_USEC(100000));
+ aki_timer_again(&s.render_timer);
+
+ aki_event_loop_run(&s.loop);
+
+ cmsrv_ui_close(&s.ui);
+
+ aki_common_close();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/fruits/cmsrv/meson.build b/src/fruits/cmsrv/meson.build
new file mode 100644
index 0000000..ee8eb91
--- /dev/null
+++ b/src/fruits/cmsrv/meson.build
@@ -0,0 +1,19 @@
+cmsrv_src = ['cmsrv.c']
+cmsrv_deps = [server]
+cmsrv_args = []
+
+if get_option('portal').enabled()
+ cmsrv_deps += [portal]
+endif
+
+use_tui = true
+if use_tui
+ cmsrv_src += ['ui.c']
+ cmsrv_deps += [dependency('notcurses-core')]
+endif
+
+if is_windows and meson.is_cross_build()
+ cmsrv_args += ['-static', '-static-libgcc', '-static-libstdc++', '-municode', '-mwindows']
+endif
+
+executable('cmsrv', cmsrv_src, dependencies: cmsrv_deps, link_args: cmsrv_args)
diff --git a/src/fruits/cmsrv/ui.c b/src/fruits/cmsrv/ui.c
new file mode 100644
index 0000000..48f50ca
--- /dev/null
+++ b/src/fruits/cmsrv/ui.c
@@ -0,0 +1,210 @@
+#include <al/lib.h>
+
+#include "../../server/list.h"
+
+#include "ui.h"
+
+#define LOG_RATIO 1.3
+
+static s32 resize_cb(struct ncplane *p)
+{
+ struct cmsrv_ui *ui = (struct cmsrv_ui *)ncplane_userptr(p);
+ ui->pending_layout = true;
+ return 0;
+}
+
+bool cmsrv_ui_init(struct cmsrv_ui *ui, struct camu_server *server)
+{
+ if (!(ui->nc = notcurses_core_init(NULL, stdin))) {
+ return false;
+ }
+ ui->server = server;
+ notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols);
+ struct ncplane *stdplane = notcurses_stdplane(ui->nc);
+ ncplane_set_resizecb(stdplane, resize_cb);
+ ncplane_set_userptr(stdplane, ui);
+ ui->pending_layout = true;
+ al_array_init(ui->log.messages);
+ return true;
+}
+
+s32 cmsrv_ui_get_input_fd(struct cmsrv_ui *ui)
+{
+ return notcurses_inputready_fd(ui->nc);
+}
+
+bool cmsrv_ui_read_input(struct cmsrv_ui *ui, struct ncinput *input)
+{
+ al_memset(input, 0, sizeof(struct ncinput));
+ u32 ret = notcurses_get_nblock(ui->nc, input);
+ return !(ret == (u32)-1 || ret == 0);
+}
+
+void cmsrv_ui_set_pane(struct cmsrv_ui *ui, u8 pane)
+{
+ ui->pane = pane;
+}
+
+void cmsrv_ui_push_message(struct cmsrv_ui *ui, char *message)
+{
+ al_array_push(ui->log.messages, message);
+}
+
+static void layout_log(struct cmsrv_ui *ui, struct ncplane *parent)
+{
+ struct ncplane_options nopts = {
+ .rows = ui->term_rows / LOG_RATIO,
+ .cols = ui->term_cols,
+ .x = 0
+ };
+ nopts.y = ui->term_rows - nopts.rows;
+ ui->log.p = ncplane_create(parent, &nopts);
+}
+
+static void erase_log(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->log.p;
+ ncplane_erase(p);
+}
+
+static void render_log(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->log.p;
+ ncplane_erase(p);
+
+ u32 max_width = ncplane_dim_x(p) - 2;
+ u32 max_height = ncplane_dim_y(p) - 2;
+
+ u64 c = 0;
+ ncchannels_set_fg_default(&c);
+ ncplane_rounded_box(p, NCSTYLE_NONE, c, max_height + 1, max_width + 1, 0);
+
+ u32 size = ui->log.messages.size;
+ u32 index = size > max_height ? size - max_height : 0;
+ for (u32 i = index; i < size; i++) {
+ ncplane_putnstr_yx(p, (i - index) + 1, 1, max_width, al_array_at(ui->log.messages, i));
+ }
+}
+
+static void layout_lists(struct cmsrv_ui *ui, struct ncplane *parent)
+{
+ struct ncplane_options nopts = {
+ .rows = ui->term_rows - (ui->term_rows / LOG_RATIO),
+ .cols = ui->term_cols,
+ .x = 0,
+ .y = 0
+ };
+ ui->lists.p = ncplane_create(parent, &nopts);
+}
+
+static void erase_lists(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->lists.p;
+ ncplane_erase(p);
+}
+
+static void render_lists(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->lists.p;
+
+ u32 max_width = ncplane_dim_x(p);
+ u32 max_height = ncplane_dim_y(p);
+ u32 current_line = 0;
+ s32 entries_per_list = 10;
+
+ struct camu_list *list;
+ al_array_foreach(ui->server->lists, i, list) {
+ if (current_line++ >= max_height) break;
+ char *c_str = al_str_to_c_str(&list->name);
+ ncplane_putnstr_yx(p, i, 0, max_width, c_str);
+ al_free(c_str);
+ struct lia_list *impl = &list->impl;
+ s32 current = (s32)impl->current;
+ s32 index = AL_MAX(current - (entries_per_list / 2), 0);
+ s32 size = (s32)impl->entries.size;
+ s32 end = AL_MIN(index + entries_per_list, size);
+ for (s32 j = index; j < end; j++) {
+ struct lia_list_entry *entry = al_array_at(impl->entries, j);
+ c_str = al_str_to_c_str(&entry->name);
+ u32 y = i + (j - index) + 1;
+ if (j == current) {
+ ncplane_putchar_yx(p, y, 1, '>');
+ ncplane_putnstr_yx(p, y, 3, max_width - 3, c_str);
+ } else {
+ ncplane_putnstr_yx(p, y, 1, max_width - 1, c_str);
+ }
+ al_free(c_str);
+ if (++current_line >= max_height) break;
+ }
+ }
+}
+
+static void layout_nodes(struct cmsrv_ui *ui, struct ncplane *parent)
+{
+ struct ncplane_options nopts = {
+ .rows = ui->term_rows - (ui->term_rows / LOG_RATIO),
+ .cols = ui->term_cols,
+ .x = 0,
+ .y = 0
+ };
+ ui->nodes.p = ncplane_create(parent, &nopts);
+}
+
+static void erase_nodes(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->nodes.p;
+ ncplane_erase(p);
+}
+
+static void render_nodes(struct cmsrv_ui *ui)
+{
+ struct ncplane *p = ui->lists.p;
+
+ u32 max_width = ncplane_dim_x(p);
+ u32 max_height = ncplane_dim_y(p);
+ (void)max_height;
+
+ char strbuf[16];
+
+ struct lia_node *node;
+ al_array_foreach(ui->server->data.server.nodes, i, node) {
+ al_snprintf(strbuf, sizeof(strbuf), "%hu", node->id);
+ ncplane_putnstr_yx(p, i, 0, max_width, strbuf);
+ struct lia_node_connection *conn;
+ al_array_foreach(node->connections, j, conn) {
+ al_snprintf(strbuf, sizeof(strbuf), "%u", conn->pool.ready.size);
+ ncplane_putnstr_yx(p, i + j + 1, 2, max_width, strbuf);
+ }
+ }
+}
+
+void cmsrv_ui_render(struct cmsrv_ui *ui)
+{
+ if (ui->pending_layout) {
+ ui->pending_layout = false;
+ struct ncplane *stdplane = notcurses_stdplane(ui->nc);
+ layout_log(ui, stdplane);
+ layout_lists(ui, stdplane);
+ layout_nodes(ui, stdplane);
+ }
+ erase_log(ui);
+ erase_lists(ui);
+ erase_nodes(ui);
+ render_log(ui);
+ switch (ui->pane) {
+ case CMSRV_UI_LISTS:
+ render_lists(ui);
+ break;
+ case CMSRV_UI_RESOURCES:
+ break;
+ case CMSRV_UI_NODES:
+ render_nodes(ui);
+ break;
+ }
+ notcurses_render(ui->nc);
+}
+
+void cmsrv_ui_close(struct cmsrv_ui *ui)
+{
+ notcurses_stop(ui->nc);
+}
diff --git a/src/fruits/cmsrv/ui.h b/src/fruits/cmsrv/ui.h
new file mode 100644
index 0000000..08ad956
--- /dev/null
+++ b/src/fruits/cmsrv/ui.h
@@ -0,0 +1,44 @@
+#include <notcurses/notcurses.h>
+#include <al/types.h>
+#include <al/array.h>
+
+#include "../../server/server.h"
+
+enum {
+ CMSRV_UI_LISTS = 0,
+ CMSRV_UI_RESOURCES,
+ CMSRV_UI_NODES
+};
+
+struct cmsrv_ui {
+ struct notcurses *nc;
+ u32 term_cols;
+ u32 term_rows;
+ bool pending_layout;
+ u8 pane;
+ struct {
+ struct ncplane *p;
+ array(char *) messages;
+ } log;
+ struct {
+ struct ncplane *p;
+ } lists;
+ struct {
+ } resources;
+ struct {
+ struct ncplane *p;
+ } nodes;
+ struct camu_server *server;
+};
+
+bool cmsrv_ui_init(struct cmsrv_ui *ui, struct camu_server *server);
+
+s32 cmsrv_ui_get_input_fd(struct cmsrv_ui *ui);
+bool cmsrv_ui_read_input(struct cmsrv_ui *ui, struct ncinput *input);
+
+void cmsrv_ui_set_pane(struct cmsrv_ui *ui, u8 pane);
+
+void cmsrv_ui_push_message(struct cmsrv_ui *ui, char *message);
+void cmsrv_ui_render(struct cmsrv_ui *ui);
+
+void cmsrv_ui_close(struct cmsrv_ui *ui);