summaryrefslogtreecommitdiff
path: root/src/fruits
diff options
context:
space:
mode:
Diffstat (limited to 'src/fruits')
-rw-r--r--src/fruits/cmc/cmc.c129
-rw-r--r--src/fruits/cmc/cmc.h16
-rw-r--r--src/fruits/cmc/meson.build3
-rw-r--r--src/fruits/cmc/ui/pane_list.c157
-rw-r--r--src/fruits/cmc/ui/pane_search.c177
-rw-r--r--src/fruits/cmc/ui/ui.c204
-rw-r--r--src/fruits/cmc/ui/ui.h52
-rw-r--r--src/fruits/cmsrv/cmsrv.c12
-rw-r--r--src/fruits/cmsrv/meson.build2
-rw-r--r--src/fruits/cmv/cmv.c15
-rw-r--r--src/fruits/meson.build35
11 files changed, 711 insertions, 91 deletions
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
index 7691f99..58bb3b1 100644
--- a/src/fruits/cmc/cmc.c
+++ b/src/fruits/cmc/cmc.c
@@ -4,6 +4,7 @@
#include "../../portal/src/packet_ext.h"
#include "../../server/common.h"
+#include "../../liana/list.h"
#include "../common.h"
@@ -26,10 +27,22 @@ static struct cmc_search *get_search_by_id(struct cmc *c, s32 id)
return NULL;
}
-static void parse_user_state(struct cmc *c, struct nn_packet *packet)
+static void parse_list_entry(struct nn_packet *packet, struct cmc_list_entry *entry)
{
- u32 size = nn_packet_read_u32(packet);
- for (u32 i = 0; i < size; i++) {
+ entry->id = nn_packet_read_u32(packet);
+ entry->duration = nn_packet_read_u64(packet);
+ entry->start = nn_packet_read_u64(packet);
+ entry->paused_at = nn_packet_read_u64(packet);
+ entry->offset = nn_packet_read_u64(packet);
+ wstr w;
+ nn_packet_read_wstr(packet, &w);
+ al_wstr_clone(&entry->name, &w);
+}
+
+static void parse_initial_user_state(struct cmc *c, struct nn_packet *packet)
+{
+ u32 count = nn_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
struct cmc_search *search = al_alloc_object(struct cmc_search);
search->id = nn_packet_read_s32(packet);
str s;
@@ -40,28 +53,46 @@ static void parse_user_state(struct cmc *c, struct nn_packet *packet)
al_array_init(search->pages);
al_array_push(c->searches, search);
}
+ count = nn_packet_read_u32(packet);
+ for (u32 i = 0; i < count; i++) {
+ struct cmc_list *list = al_alloc_object(struct cmc_list);
+ str s;
+ nn_packet_read_str(packet, &s);
+ al_str_clone(&list->name, &s);
+ list->current = nn_packet_read_s32(packet);
+ al_array_init(list->entries);
+ u32 inner_count = nn_packet_read_s32(packet);
+ for (u32 j = 0; j < inner_count; j++) {
+ struct cmc_list_entry entry;
+ parse_list_entry(packet, &entry);
+ al_array_push(list->entries, entry);
+ }
+ al_array_push(c->lists, list);
+ }
}
static void client_callback(void *userdata, u8 op, void *opaque)
{
struct cmc *c = (struct cmc *)userdata;
switch (op) {
- case CAMU_CLIENT_LOGIN: {
+ case CAMU_CLIENT_LOGGED_IN: {
struct nn_packet *packet = (struct nn_packet *)opaque;
switch (c->command) {
case CLI_OPEN_UI:
- parse_user_state(c, packet);
+ parse_initial_user_state(c, packet);
if (!cmc_ui_init(&c->ui, &c->loop, c)) {
nn_event_loop_break_one(&c->loop);
}
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ cmc_lp_add_list(&c->ui, list);
+ }
+ struct cmc_search *search;
+ al_array_foreach(c->searches, i, search) {
+ cmc_sp_add_search(&c->ui, search);
+ }
break;
case CLI_ADD: {
- str *arg;
- al_array_foreach_ptr(c->args, i, arg) {
- camu_client_add_from_path(&c->client, &al_str_c("default"), arg);
- al_str_free(arg);
- }
- camu_client_disconnect(&c->client);
break;
}
case CLI_SEARCH: {
@@ -81,32 +112,65 @@ static void client_callback(void *userdata, u8 op, void *opaque)
search->id = *(s32 *)opaque;
al_array_init(search->pages);
al_array_push(c->searches, search);
- camu_client_get_page(&c->client, search->id, 0);
+ cmc_sp_add_search(&c->ui, search);
+ break;
+ }
+ case CAMU_CLIENT_GOT_RESULTS: {
break;
}
- case CAMU_CLIENT_PAGE_RESULTS: {
+ case CAMU_CLIENT_GOT_META: {
struct nn_packet *packet = (struct nn_packet *)opaque;
- s32 id = nn_packet_read_s32(packet);
- struct cmc_search *search = get_search_by_id(c, id);
- if (!search) return;
- struct cmc_search_page *page = al_alloc_object(struct cmc_search_page);
- page->num = nn_packet_read_u32(packet);
- u32 posts = nn_packet_read_u32(packet);
- for (u32 i = 0; i < posts; i++) {
- struct camu_post post;
- nn_packet_read_post(packet, &post);
- camu_post_cache_push(&c->cache, &post);
+ u8 op = nn_packet_read_u8(packet);
+ switch (op) {
+ case LIANA_META_CURRENT_CHANGED: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ list->current = nn_packet_read_s32(packet);
+ struct cmc_list_entry *entry = &al_array_at(list->entries, list->current);
+ parse_list_entry(packet, entry);
+ break;
+ }
+ }
+ cmc_ui_queue_render(&c->ui);
+ break;
+ }
+ case LIANA_META_ADDED_ENTRY: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list_entry entry;
+ parse_list_entry(packet, &entry);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ al_array_push(list->entries, entry);
+ break;
+ }
+ }
+ cmc_ui_queue_render(&c->ui);
+ break;
+ }
+ case LIANA_META_ENTRY_SEEKED: {
+ str name;
+ nn_packet_read_str(packet, &name);
+ struct cmc_list *list;
+ al_array_foreach(c->lists, i, list) {
+ if (al_str_eq(&list->name, &name)) {
+ u32 id = nn_packet_peek_u32(packet);
+ struct cmc_list_entry *entry;
+ al_array_foreach_ptr(list->entries, j, entry) {
+ if (entry->id == id) {
+ parse_list_entry(packet, entry);
+ break;
+ }
+ }
+ }
+ }
+ break;
}
- u32 ids = nn_packet_read_u32(packet);
- for (u32 i = 0; i < ids; i++) {
- str unique_id;
- nn_packet_read_str(packet, &unique_id);
- str s;
- al_str_clone(&s, &unique_id);
- al_array_push(page->list, s);
}
- al_array_push(search->pages, page);
- camu_client_add_from_post(&c->client, &al_str_c("default"), &al_array_at(page->list, 0), 0);
break;
}
}
@@ -166,6 +230,7 @@ s32 main(s32 argc, char *argv[])
al_array_init(c.args);
if (!parse_cmd(argc, argv)) return EXIT_FAILURE;
+ al_array_init(c.lists);
camu_post_cache_init(&c.cache);
al_array_init(c.searches);
diff --git a/src/fruits/cmc/cmc.h b/src/fruits/cmc/cmc.h
index 96de27b..0a3cbca 100644
--- a/src/fruits/cmc/cmc.h
+++ b/src/fruits/cmc/cmc.h
@@ -21,11 +21,27 @@ struct cmc_search {
array(struct cmc_search_page *) pages;
};
+struct cmc_list_entry {
+ u32 id;
+ u64 duration;
+ u64 start;
+ u64 paused_at;
+ u64 offset;
+ wstr name;
+};
+
+struct cmc_list {
+ str name;
+ s32 current;
+ array(struct cmc_list_entry) entries;
+};
+
struct cmc {
struct nn_event_loop loop;
u8 command;
array(str) args;
struct camu_client client;
+ array(struct cmc_list *) lists;
struct camu_post_cache cache;
array(struct cmc_search *) searches;
struct cmc_ui ui;
diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build
index 76efff5..6b7060d 100644
--- a/src/fruits/cmc/meson.build
+++ b/src/fruits/cmc/meson.build
@@ -10,9 +10,10 @@ use_tui = true
if use_tui
cmc_src += [
'ui/ui.c',
+ 'ui/pane_list.c',
'ui/pane_search.c',
]
- cmc_deps += [dependency('notcurses')]
+ cmc_deps += [notcurses, notcurses_core]
endif
if is_windows and meson.is_cross_build()
diff --git a/src/fruits/cmc/ui/pane_list.c b/src/fruits/cmc/ui/pane_list.c
new file mode 100644
index 0000000..9a04211
--- /dev/null
+++ b/src/fruits/cmc/ui/pane_list.c
@@ -0,0 +1,157 @@
+#include <nnwt/thread.h>
+
+#include "../../liana/list.h"
+
+#include "../cmc.h"
+
+#include "ui.h"
+
+void cmc_lp_init(struct cmc_ui *ui)
+{
+ al_array_init(ui->lp.tabs);
+ ui->lp.current = 0;
+}
+
+void cmc_lp_layout(struct cmc_ui *ui, struct ncplane *parent)
+{
+ if (ui->lp.n) ncplane_destroy(ui->lp.n);
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = ncplane_dim_y(parent);
+ nopts.cols = ncplane_dim_x(parent);
+ ui->lp.n = ncplane_create(parent, &nopts);
+}
+
+void cmc_lp_add_list(struct cmc_ui *ui, struct cmc_list *list)
+{
+ struct cmc_list_tab tab = {
+ .list = list,
+ .selected = 0
+ };
+ al_array_push(ui->lp.tabs, tab);
+}
+
+bool cmc_lp_handle_input(struct cmc_ui *ui, struct ncinput *input)
+{
+ if (!ui->lp.tabs.count) {
+ return false;
+ }
+
+ struct cmc_list_tab *tab = &al_array_at(ui->lp.tabs, ui->lp.current);
+ u32 count = tab->list->entries.count;
+ switch (input->id) {
+ case 'j':
+ if (!cmc_ui_consider_input(input, true) || tab->selected >= count - 1) {
+ return false;
+ }
+ tab->selected++;
+ break;
+ case 'k':
+ if (!cmc_ui_consider_input(input, true) || tab->selected == 0) {
+ return false;
+ }
+ tab->selected--;
+ break;
+ case NCKEY_RETURN:
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ camu_client_skipto(&ui->c->client, &tab->list->name, tab->selected);
+ break;
+ case NCKEY_BUTTON1: {
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ u32 height = ncplane_dim_y(ui->lp.n);
+ u32 width = ncplane_dim_x(ui->lp.n);
+ if (input->x > 0 && input->y > 0) {
+ u32 x = (u32)input->x;
+ u32 y = (u32)input->y;
+ if (y > height - 3 && x >= 2 && x <= width) {
+ struct cmc_list_entry *entry = &al_array_at(tab->list->entries, tab->list->current);
+ f64 percent = (x - 2) / (f64)(width - 2);
+ camu_client_seek(&ui->c->client, &tab->list->name, entry->id, percent);
+ }
+ ui->last_mouse_x = x;
+ ui->last_mouse_y = y;
+ }
+ break;
+ }
+ }
+
+ return true;
+}
+
+void cmc_lp_erase(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->lp.n);
+}
+
+static void render_list_entries(struct cmc_ui *ui, struct cmc_list_tab *tab, u32 max_height)
+{
+ struct cmc_list *list = tab->list;
+ u32 count = list->entries.count;
+ u32 index = tab->selected - (tab->selected % max_height);
+ u32 end = MIN(index + max_height, count);
+ struct cmc_list_entry *entry;
+ for (u32 i = index; i < end; i++) {
+ entry = &al_array_at(list->entries, i);
+ if (i == tab->selected) {
+ ncplane_set_styles(ui->lp.n, NCSTYLE_BOLD);
+ ncplane_set_fg_palindex(ui->lp.n, 6);
+ } else {
+ ncplane_set_styles(ui->lp.n, NCSTYLE_NONE);
+ ncplane_set_fg_default(ui->lp.n);
+ }
+ if (list->current >= 0 && i == (u32)list->current) {
+ ncplane_putchar_yx(ui->lp.n, i - index, 1, '>');
+ cmc_ui_putnwstr_yx(ui->lp.n, i - index, 3, entry->name.length, &entry->name);
+ } else {
+ cmc_ui_putnwstr_yx(ui->lp.n, i - index, 0, entry->name.length, &entry->name);
+ }
+ }
+}
+
+static void render_now_playing(struct cmc_ui *ui, struct cmc_list_tab *tab, u32 y, u32 height)
+{
+ u32 width = ncplane_dim_x(ui->lp.n);
+ if (tab && tab->list->entries.count > 0) {
+ struct cmc_list *list = tab->list;
+ struct cmc_list_entry *entry = &al_array_at(list->entries, list->current);
+ f32 percent = 1.0;
+ if (entry->duration > 0) {
+ u64 pos = entry->offset;
+ u64 now = nn_get_timestamp();
+ if (entry->start != LIANA_TIMESTAMP_INVALID && now > entry->start) {
+ pos += now - entry->start;
+ }
+ percent = MIN(pos / (f32)entry->duration, 1.f);
+ }
+ for (u32 i = 0; i < (width - 2) * percent; i++) {
+ ncplane_putchar_yx(ui->lp.n, height - 2, i + 1, '-');
+ }
+ cmc_ui_putnwstr_yx(ui->lp.n, height - 4, 1, entry->name.length, &entry->name);
+ }
+ char buf[24];
+ al_snprintf(buf, sizeof(buf), "%u, %u | %u, %u", ui->last_mouse_x, ui->last_mouse_y, width, height);
+ ncplane_putstr_yx(ui->lp.n, height - 5, 1, buf);
+ u64 c = 0;
+ ncchannels_set_fg_default(&c);
+ ncplane_cursor_move_yx(ui->lp.n, y, 0);
+ ncplane_rounded_box(ui->lp.n, NCSTYLE_NONE, c, height - 1, width - 1, 0);
+}
+
+void cmc_lp_render(struct cmc_ui *ui)
+{
+ u32 height = ncplane_dim_y(ui->lp.n);
+
+ struct cmc_list_tab *tab = NULL;
+ if (ui->lp.tabs.count > 0) {
+ tab = &al_array_at(ui->lp.tabs, ui->lp.current);
+ }
+
+ if (tab) {
+ render_list_entries(ui, tab, height - 8);
+ }
+
+ render_now_playing(ui, tab, height - 8, height);
+}
diff --git a/src/fruits/cmc/ui/pane_search.c b/src/fruits/cmc/ui/pane_search.c
index c3d1165..021c42f 100644
--- a/src/fruits/cmc/ui/pane_search.c
+++ b/src/fruits/cmc/ui/pane_search.c
@@ -4,36 +4,181 @@
void cmc_sp_init(struct cmc_ui *ui)
{
- if (ui->c->searches.count > 0) {
- ui->sp.search = al_array_at(ui->c->searches, 0);
- }
- cmc_sp_layout(ui, notcurses_stdplane(ui->nc));
+ al_array_init(ui->sp.tabs);
+ ui->sp.current = 0;
}
void cmc_sp_layout(struct cmc_ui *ui, struct ncplane *parent)
{
if (ui->sp.n) ncplane_destroy(ui->sp.n);
- struct ncplane_options nopts = {
- .rows = ui->term_rows,
- .cols = ui->term_cols,
- .x = 0,
- .y = 0
- };
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = ncplane_dim_y(parent);
+ nopts.cols = ncplane_dim_x(parent);
ui->sp.n = ncplane_create(parent, &nopts);
}
+void cmc_sp_add_search(struct cmc_ui *ui, struct cmc_search *search)
+{
+ struct cmc_search_tab tab = {
+ .search = search,
+ };
+ struct ncplane_options nopts = { 0 };
+ nopts.x = 3;
+ nopts.y = 2;
+ nopts.rows = 10;
+ nopts.cols = ncplane_dim_x(ui->sp.n) - 3;
+ tab.input = ncplane_create(ui->sp.n, &nopts);
+ tab.cursor = 0;
+ tab.input_active = false;
+ tab.input_text = al_wstr_null();
+ tab.input_changed = false;
+ al_array_push(ui->sp.tabs, tab);
+}
+
+static bool handle_text_input(struct cmc_search_tab *tab, struct ncinput *input)
+{
+ switch (input->id) {
+ case NCKEY_BACKSPACE:
+ if (tab->cursor > 0) {
+ al_wstr_remove_at(&tab->input_text, tab->cursor);
+ tab->cursor--;
+ tab->input_changed = true;
+ }
+ break;
+ case NCKEY_LEFT:
+ if (tab->cursor > 0) {
+ tab->cursor--;
+ }
+ break;
+ case NCKEY_RIGHT:
+ if (tab->cursor < tab->input_text.length) {
+ tab->cursor++;
+ }
+ break;
+ case NCKEY_RETURN:
+ tab->input_active = false;
+ // Erase text on next render.
+ tab->input_changed = true;
+ return true;
+ default:
+ if (ncinput_ctrl_p(input)) {
+ switch (input->id) {
+ case 'A':
+ tab->cursor = 0;
+ break;
+ case 'E':
+ tab->cursor = tab->input_text.length;
+ break;
+ case 'B':
+ if (tab->cursor > 0) {
+ tab->cursor--;
+ }
+ break;
+ case 'F':
+ if (tab->cursor < tab->input_text.length) {
+ tab->cursor++;
+ }
+ break;
+ }
+ break;
+ }
+ // https://github.com/dankamongmen/notcurses/blob/c11efe877f2245901f5c9ce110de7a2c83cfa2ed/src/lib/reader.c#L404
+ for (s32 c = 0; input->eff_text[c] != 0; c++){
+ uchar egc[5] = { 0 };
+ if (notcurses_ucs32_to_utf8(&input->eff_text[c], 1, egc, 4) >= 0) {
+ // This probably doesn't work on Windows.
+ al_wstr_insert(&tab->input_text, tab->cursor, &al_wstr_w((wchar_t *)egc, 0, 1));
+ tab->cursor++;
+ tab->input_changed = true;
+ }
+ }
+ break;
+ }
+ return false;
+}
+
bool cmc_sp_handle_input(struct cmc_ui *ui, struct ncinput *input)
{
- (void)ui;
- (void)input;
+ struct cmc_search_tab *tab = NULL;
+ if (ui->sp.tabs.count > 0) {
+ tab = &al_array_at(ui->sp.tabs, ui->sp.current);
+ }
+
+ if (tab) {
+ if (tab->input_active) {
+ if (cmc_ui_consider_input(input, true)) {
+ if (handle_text_input(tab, input)) {
+ notcurses_cursor_disable(ui->nc);
+ str query;
+ al_wstr_to_str(&tab->input_text, &query);
+ camu_client_create_search(&ui->c->client, &al_str_c("youtube"), &query);
+ al_str_free(&query);
+ }
+ }
+ } else {
+ switch (input->id) {
+ case 'i':
+ if (!cmc_ui_consider_input(input, false)) {
+ return false;
+ }
+ tab->input_active = true;
+ tab->input_changed = true;
+ s32 absx = ncplane_abs_x(tab->input);
+ s32 absy = ncplane_abs_y(tab->input);
+ notcurses_cursor_enable(ui->nc, absy, absx + tab->cursor);
+ break;
+ }
+ }
+ }
+
return true;
}
+void cmc_sp_erase(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->sp.n);
+}
+
void cmc_sp_render(struct cmc_ui *ui)
{
- if (ui->sp.search) {
- char *c_str = al_str_to_c_str(&ui->sp.search->query);
- ncplane_putstr_yx(ui->sp.n, 0, 0, c_str);
- al_free(c_str);
+ struct cmc_search_tab *tab = NULL;
+ if (ui->sp.tabs.count > 0) {
+ tab = &al_array_at(ui->sp.tabs, ui->sp.current);
+ }
+
+ if (tab) {
+ if (tab->input_changed) {
+ ncplane_erase(tab->input);
+ if (tab->input_active) {
+ cmc_ui_putnwstr_yx(tab->input, 0, 0, tab->input_text.length, &tab->input_text);
+ }
+ tab->input_changed = false;
+ }
+ if (tab->input_active) {
+ s32 absx = ncplane_abs_x(tab->input);
+ s32 absy = ncplane_abs_y(tab->input);
+ notcurses_cursor_enable(ui->nc, absy, absx + tab->cursor);
+ }
+ }
+
+ /*
+ if (ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ u32 max_height = ncplane_dim_y(ui->oln) - 2;
+ u32 count = ui->c->searches.count;
+ u32 index = ui->sp.overlay_index - (ui->sp.overlay_index % max_height);
+ u32 end = MIN(index + max_height, count);
+ for (u32 i = index; i < end; i++) {
+ struct cmc_search *search = al_array_at(ui->c->searches, i);
+ if (i == ui->sp.overlay_index) {
+ ncplane_set_fg_palindex(ui->oln, 6);
+ ncplane_putchar_yx(ui->oln, i - index + 1, 2, '>');
+ cmc_ui_putstr_maxwidth_yx(ui->oln, i - index + 1, 4, search->query.length, &search->query);
+ } else {
+ ncplane_set_fg_default(ui->oln);
+ cmc_ui_putstr_maxwidth_yx(ui->oln, i - index + 1, 2, search->query.length, &search->query);
+ }
+ }
}
+ */
}
diff --git a/src/fruits/cmc/ui/ui.c b/src/fruits/cmc/ui/ui.c
index 72e4df2..84e514e 100644
--- a/src/fruits/cmc/ui/ui.c
+++ b/src/fruits/cmc/ui/ui.c
@@ -2,11 +2,27 @@
#include "ui.h"
-static s32 resize_cb(struct ncplane *p)
+void cmc_ui_putnwstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, wstr *w)
{
- struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p);
- (void)ui;
- return 0;
+ ncplane_cursor_move_yx(n, y, x);
+ u32 end = MIN(w->length, width);
+ for (u32 i = 0; i < end; i++) {
+ ncplane_putwc(n, al_wstr_at(w, i));
+ }
+}
+
+void cmc_ui_putnstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, str *s)
+{
+ ncplane_cursor_move_yx(n, y, x);
+ u32 end = MIN(s->length, width);
+ for (u32 i = 0; i < end; i++) {
+ ncplane_putchar(n, al_str_at(s, i));
+ }
+}
+
+bool cmc_ui_consider_input(struct ncinput *input, bool repeat)
+{
+ return (input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN || (repeat && input->evtype == NCTYPE_REPEAT));
}
static bool read_input(struct cmc_ui *ui, struct ncinput *input)
@@ -16,51 +32,217 @@ static bool read_input(struct cmc_ui *ui, struct ncinput *input)
return !(ret == (u32)-1 || ret == 0);
}
+static void erase_previous_pane(struct cmc_ui *ui)
+{
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ ncplane_erase(ui->lp.n);
+ break;
+ case CMC_PANE_SEARCH:
+ ncplane_erase(ui->sp.n);
+ break;
+ }
+}
+
+static void switch_to_pane(struct cmc_ui *ui, u8 pane)
+{
+ erase_previous_pane(ui);
+ ui->pane = pane;
+}
+
+void cmc_ui_queue_render(struct cmc_ui *ui)
+{
+ ncplane_erase(ui->n);
+
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ cmc_lp_erase(ui);
+ break;
+ case CMC_PANE_SEARCH:
+ cmc_sp_erase(ui);
+ break;
+ }
+
+ if (ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ }
+
+ notcurses_render(ui->nc);
+
+ switch (ui->pane) {
+ case CMC_PANE_LIST:
+ cmc_lp_render(ui);
+ break;
+ case CMC_PANE_SEARCH:
+ cmc_sp_render(ui);
+ break;
+ }
+
+ if (ui->overlay_shown) {
+ /*
+ struct nccell ncl;
+ nccell_init(&ncl);
+ nccell_load(ui->oln, &ncl, "I");
+ //nccell_set_bg_palindex(&ncl, 3);
+ nccell_set_fg_palindex(&ncl, 3);
+ ncplane_polyfill_yx(ui->oln, 0, 0, &ncl);
+ */
+ u32 width = ncplane_dim_x(ui->oln);
+ u32 height = ncplane_dim_y(ui->oln);
+ u64 c = 0;
+ ncchannels_set_fg_default(&c);
+ ncplane_cursor_move_yx(ui->oln, 0, 0);
+ ncplane_rounded_box(ui->oln, NCSTYLE_NONE, c, height - 1, width - 1, 0);
+ }
+
+ notcurses_render(ui->nc);
+}
+
+void cmc_ui_toggle_overlay(struct cmc_ui *ui)
+{
+ ui->overlay_shown = !ui->overlay_shown;
+ if (!ui->overlay_shown) {
+ ncplane_erase(ui->oln);
+ }
+}
+
static void input_poll_callback(void *userdata, s32 revents)
{
struct cmc_ui *ui = (struct cmc_ui *)userdata;
(void)revents;
+ bool do_render;
struct ncinput input;
for (;;) {
- if (!read_input(ui, &input)) break;
- bool do_render = false;
- if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ if (!read_input(ui, &input)) {
+ break;
+ }
+
+ do_render = false;
+
+ if (cmc_ui_consider_input(&input, false)) {
switch (input.id) {
case 'q':
nn_event_loop_break_one(ui->loop);
break;
+ case '1':
+ switch_to_pane(ui, CMC_PANE_LIST);
+ break;
+ case '2':
+ switch_to_pane(ui, CMC_PANE_SEARCH);
+ break;
+ case '3':
+ break;
+ case '4':
+ break;
+ case NCKEY_TAB:
+ cmc_ui_toggle_overlay(ui);
+ break;
}
}
+
switch (ui->pane) {
+ case CMC_PANE_LIST:
+ if (cmc_lp_handle_input(ui, &input)) {
+ do_render |= true;
+ }
+ break;
case CMC_PANE_SEARCH:
if (cmc_sp_handle_input(ui, &input)) {
- cmc_sp_render(ui);
do_render |= true;
}
+ break;
}
+
if (do_render) {
+ cmc_ui_queue_render(ui);
notcurses_render(ui->nc);
}
}
}
+static void render_timer_callback(void *userdata, struct nn_timer *timer)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)userdata;
+ (void)timer;
+ cmc_ui_queue_render(ui);
+ notcurses_render(ui->nc);
+ nn_timer_again(&ui->render_timer);
+}
+
+static bool relayout(struct cmc_ui *ui)
+{
+ struct ncplane *stdplane = notcurses_stdplane(ui->nc);
+
+ notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols);
+
+ if (ui->n) ncplane_destroy(ui->n);
+ struct ncplane_options nopts = { 0 };
+ nopts.x = 2;
+ nopts.y = 1;
+ nopts.margin_r = 4;
+ nopts.margin_b = 1;
+ nopts.flags = NCPLANE_OPTION_MARGINALIZED;
+ ui->n = ncplane_create(stdplane, &nopts);
+
+ cmc_lp_layout(ui, ui->n);
+ cmc_sp_layout(ui, ui->n);
+
+ if (ui->oln) ncplane_destroy(ui->oln);
+ u32 width = ncplane_dim_x(ui->n);
+ u32 height = ncplane_dim_y(ui->n);
+ nopts.margin_r = MAX(6u, (u32)(width / 1.5));
+ nopts.margin_b = MAX(2u, height / 3);
+ nopts.x = nopts.margin_r / 2;
+ nopts.y = nopts.margin_b / 2;
+ ui->oln = ncplane_create(ui->n, &nopts);
+
+ return true;
+}
+
+static s32 resize_cb(struct ncplane *p)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p);
+ relayout(ui);
+ return 0;
+}
+
bool cmc_ui_init(struct cmc_ui *ui, struct nn_event_loop *loop, struct cmc *c)
{
al_memset(ui, 0, sizeof(struct cmc_ui));
+
ui->c = c;
- if (!(ui->nc = notcurses_init(NULL, stdin))) {
+ struct notcurses_options opts = { 0 };
+ opts.flags = NCOPTION_INHIBIT_SETLOCALE;
+ if (!(ui->nc = notcurses_init(&opts, stdin))) {
return false;
}
+ notcurses_mice_enable(ui->nc, NCKEY_BUTTON1);
+
ui->loop = loop;
- notcurses_stddim_yx(ui->nc, &ui->term_rows, &ui->term_cols);
+
+ cmc_lp_init(ui);
cmc_sp_init(ui);
+
+ ui->pending_layout = false;
+ ui->pending_render = false;
+
+ ui->pane = CMC_PANE_SEARCH;
+ ui->overlay_shown = false;
+
+ relayout(ui);
+
struct ncplane *stdplane = notcurses_stdplane(ui->nc);
ncplane_set_resizecb(stdplane, resize_cb);
ncplane_set_userptr(stdplane, ui);
+
nn_poll_init(&ui->input_poll, input_poll_callback, ui);
nn_poll_set(&ui->input_poll, notcurses_inputready_fd(ui->nc), NNWT_POLL_READ);
nn_poll_start(&ui->input_poll, ui->loop);
- ui->pane = CMC_PANE_SEARCH;
+
+ nn_timer_init(&ui->render_timer, ui->loop, render_timer_callback, ui);
+ nn_timer_set_repeat(&ui->render_timer, NNWT_TS_FROM_USEC(300000));
+ nn_timer_again(&ui->render_timer);
+
return true;
}
diff --git a/src/fruits/cmc/ui/ui.h b/src/fruits/cmc/ui/ui.h
index 1c3b9d9..996bb73 100644
--- a/src/fruits/cmc/ui/ui.h
+++ b/src/fruits/cmc/ui/ui.h
@@ -1,11 +1,29 @@
#pragma once
#include <al/types.h>
+#include <al/array.h>
+#include <al/wstr.h>
#include <nnwt/event_loop.h>
+#include <nnwt/timer.h>
#include <notcurses/notcurses.h>
enum {
- CMC_PANE_SEARCH = 0,
+ CMC_PANE_LIST = 0,
+ CMC_PANE_SEARCH,
+};
+
+struct cmc_list_tab {
+ struct cmc_list *list;
+ u32 selected;
+};
+
+struct cmc_search_tab {
+ struct cmc_search *search;
+ struct ncplane *input;
+ u32 cursor;
+ bool input_active;
+ wstr input_text;
+ bool input_changed;
};
struct cmc;
@@ -15,19 +33,47 @@ struct cmc_ui {
u32 term_cols;
u32 term_rows;
bool pending_layout;
- struct nn_poll input_poll;
+ bool pending_render;
u8 pane;
+ struct ncplane *n; // window.
+ struct ncplane *oln; // overlay.
+ bool overlay_shown;
+ u32 last_mouse_x;
+ u32 last_mouse_y;
struct {
struct ncplane *n;
- struct cmc_search *search;
+ array(struct cmc_list_tab) tabs;
+ u32 current;
+ } lp; // list pane.
+ struct {
+ struct ncplane *n;
+ array(struct cmc_search_tab) tabs;
+ u32 current;
} sp; // search pane.
+ struct nn_poll input_poll;
+ struct nn_timer render_timer;
struct cmc *c;
};
+void cmc_ui_putnwstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, wstr *w);
+void cmc_ui_putnstr_yx(struct ncplane *n, u32 y, u32 x, u32 width, str *s);
+bool cmc_ui_consider_input(struct ncinput *input, bool repeat);
+
bool cmc_ui_init(struct cmc_ui *ui, struct nn_event_loop *loop, struct cmc *c);
+void cmc_ui_queue_render(struct cmc_ui *ui);
+void cmc_ui_toggle_overlay(struct cmc_ui *ui);
void cmc_ui_close(struct cmc_ui *ui);
+void cmc_lp_init(struct cmc_ui *ui);
+void cmc_lp_layout(struct cmc_ui *ui, struct ncplane *parent);
+void cmc_lp_add_list(struct cmc_ui *ui, struct cmc_list *list);
+bool cmc_lp_handle_input(struct cmc_ui *ui, struct ncinput *input);
+void cmc_lp_erase(struct cmc_ui *ui);
+void cmc_lp_render(struct cmc_ui *ui);
+
void cmc_sp_init(struct cmc_ui *ui);
void cmc_sp_layout(struct cmc_ui *ui, struct ncplane *parent);
+void cmc_sp_add_search(struct cmc_ui *ui, struct cmc_search *search);
bool cmc_sp_handle_input(struct cmc_ui *ui, struct ncinput *input);
+void cmc_sp_erase(struct cmc_ui *ui);
void cmc_sp_render(struct cmc_ui *ui);
diff --git a/src/fruits/cmsrv/cmsrv.c b/src/fruits/cmsrv/cmsrv.c
index 5b65275..2f8322c 100644
--- a/src/fruits/cmsrv/cmsrv.c
+++ b/src/fruits/cmsrv/cmsrv.c
@@ -120,16 +120,6 @@ static s32 log_callback(void *userdata, u8 level, char *message)
}
#endif
-static void server_meta_callback(void *userdata, u8 op, struct lia_list *list, struct lia_list_entry *entry)
-{
- struct cmsrv *s = (struct cmsrv *)userdata;
- (void)s;
- (void)list;
- if (op == LIANA_META_PLAYING) {
- al_log_info("cmsrv", "now playing: %ls.", entry->name.data);
- }
-}
-
static void quit_signal_callback(void *userdata)
{
struct cmsrv *s = (struct cmsrv *)userdata;
@@ -170,8 +160,6 @@ s32 main(s32 argc, char *argv[])
nn_signal_start(&s.quit_signal);
camu_server_init(&s.server, &s.loop);
- s.server.meta_callback = server_meta_callback;
- s.server.userdata = &s;
if (!camu_server_listen(&s.server, CAMU_TEST_TYPE, &CAMU_TEST_ADDR, CAMU_PORT)) {
return EXIT_FAILURE;
}
diff --git a/src/fruits/cmsrv/meson.build b/src/fruits/cmsrv/meson.build
index 2f33dee..dce3d94 100644
--- a/src/fruits/cmsrv/meson.build
+++ b/src/fruits/cmsrv/meson.build
@@ -5,7 +5,7 @@ cmsrv_args = []
use_tui = true
if use_tui
cmsrv_src += ['ui.c']
- cmsrv_deps += [dependency('notcurses-core')]
+ cmsrv_deps += [notcurses_core]
endif
if is_windows and meson.is_cross_build()
diff --git a/src/fruits/cmv/cmv.c b/src/fruits/cmv/cmv.c
index 65fc086..9926f32 100644
--- a/src/fruits/cmv/cmv.c
+++ b/src/fruits/cmv/cmv.c
@@ -26,18 +26,6 @@ struct cmv {
};
#ifndef CAMU_SINK_ONLY
-static void server_meta_callback(void *userdata, u8 op, struct lia_list *list, struct lia_list_entry *entry)
-{
- struct cmv *c = (struct cmv *)userdata;
- (void)c;
- (void)list;
- if (op == LIANA_META_PLAYING) {
- al_log_info("cmv", "now playing: %ls.", entry->name.data);
- }
-}
-#endif
-
-#ifndef CAMU_SINK_ONLY
static void exit_callback(void *userdata, struct camu_desktop *desktop)
{
struct cmv *c = (struct cmv *)userdata;
@@ -53,7 +41,6 @@ static nn_thread_result NNWT_THREADCALL event_loop_thread(void *userdata)
return 0;
}
-
static struct cmv c = { 0 };
static void sigint_handler(int signum)
@@ -106,8 +93,6 @@ s32 main(s32 argc, char *argv[])
type = NNWT_SOCKET_UNIX; addr = &CMV_UNIX_PATH;
#endif
camu_server_init(&c.server, &c.loop);
- c.server.meta_callback = server_meta_callback;
- c.server.userdata = &c;
#ifdef CAMU_DIRECT_MODE
camu_server_bind_direct(&c.server);
#else
diff --git a/src/fruits/meson.build b/src/fruits/meson.build
new file mode 100644
index 0000000..2ceeaa0
--- /dev/null
+++ b/src/fruits/meson.build
@@ -0,0 +1,35 @@
+if get_option('server').enabled() or get_option('client').enabled()
+ notcurses = dependency('notcurses', required: false, allow_fallback: false)
+ notcurses_core = dependency('notcurses-core', required: false, allow_fallback: false)
+ if not notcurses.found() or not notcurses_core.found()
+ notcurses_opts = cmake.subproject_options()
+ notcurses_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' })
+ notcurses_opts.add_cmake_defines({ 'USE_CXX': false })
+ notcurses_opts.add_cmake_defines({ 'USE_DOCTEST': false })
+ notcurses_opts.add_cmake_defines({ 'USE_PANDOC': false })
+ notcurses_opts.add_cmake_defines({ 'BUILD_EXECUTABLES': false })
+ notcurses_opts.add_cmake_defines({ 'BUILD_FFI_LIBRARY': false })
+ notcurses_opts.add_cmake_defines({ 'USE_POC': false })
+ notcurses_opts.add_cmake_defines({ 'USE_STATIC': false })
+ notcurses_opts.add_cmake_defines({ 'USE_MULTIMEDIA': 'ffmpeg' })
+ notcurses_proj = cmake.subproject('notcurses', options: notcurses_opts)
+ notcurses = notcurses_proj.dependency('notcurses')
+ notcurses_core = notcurses_proj.dependency('notcurses-core')
+ endif
+endif
+
+if get_option('server').enabled()
+ subdir('cmsrv')
+endif
+
+if get_option('client').enabled()
+ subdir('cmc')
+endif
+
+if get_option('sink').enabled()
+ if is_android
+ subdir('ctv')
+ else
+ subdir('cmv')
+ endif
+endif