summaryrefslogtreecommitdiff
path: root/src/fruits/cmc/ui/panes
diff options
context:
space:
mode:
authorAndrew Opalach <andrew@akon.city> 2025-04-25 15:56:56 -0400
committerAndrew Opalach <andrew@akon.city> 2025-04-25 15:56:56 -0400
commitb1985414e54edf1eebe597551a48609ad08c6cfa (patch)
tree4a4def499348e0c9ccb52472e7c862f814993931 /src/fruits/cmc/ui/panes
parent76fc9fe33dd58926e552b6bd3b79d798b452c6ed (diff)
downloadcamu-b1985414e54edf1eebe597551a48609ad08c6cfa.tar.gz
camu-b1985414e54edf1eebe597551a48609ad08c6cfa.tar.bz2
camu-b1985414e54edf1eebe597551a48609ad08c6cfa.zip
Partial cmc ui refactor
Signed-off-by: Andrew Opalach <andrew@akon.city>
Diffstat (limited to 'src/fruits/cmc/ui/panes')
-rw-r--r--src/fruits/cmc/ui/panes/list.c688
-rw-r--r--src/fruits/cmc/ui/panes/list.h35
-rw-r--r--src/fruits/cmc/ui/panes/log.c65
-rw-r--r--src/fruits/cmc/ui/panes/log.h18
-rw-r--r--src/fruits/cmc/ui/panes/search.c226
5 files changed, 1032 insertions, 0 deletions
diff --git a/src/fruits/cmc/ui/panes/list.c b/src/fruits/cmc/ui/panes/list.c
new file mode 100644
index 0000000..0e853db
--- /dev/null
+++ b/src/fruits/cmc/ui/panes/list.c
@@ -0,0 +1,688 @@
+#include <nnwt/thread.h>
+
+#include "../../cmc.h"
+
+#include "list.h"
+
+void cmc_list_pane_init(struct cmc_list_pane *lp, struct cmc_ui *ui)
+{
+ al_array_init(lp->tabs);
+ lp->selected = -1;
+ cmc_now_playing_init(&lp->np);
+ lp->ui = ui;
+}
+
+bool cmc_list_pane_layout(struct cmc_list_pane *lp, struct ncplane *parent)
+{
+ if (lp->n) ncplane_destroy(lp->n);
+ struct ncplane_options nopts = { 0 };
+ u32 height = ncplane_dim_y(parent);
+ nopts.rows = height;
+ nopts.cols = ncplane_dim_x(parent);
+ lp->n = ncplane_create(parent, &nopts);
+ lp->page_length = height - 9;
+ cmc_now_playing_layout(&lp->np, lp->n, lp->page_length);
+ return true;
+}
+
+void cmc_list_pane_add_list(struct cmc_list_pane *lp, struct cmc_list *list)
+{
+ struct cmc_list_tab tab = { 0 };
+ tab.list = list;
+ tab.selected = list->current;
+ al_array_push(lp->tabs, tab);
+ if (lp->selected == -1) {
+ lp->selected = lp->tabs.count - 1;
+ }
+}
+
+bool cmc_list_pane_handle_input(struct cmc_list_pane *lp, struct ncinput *input)
+{
+ if (lp->selected == -1) return false;
+
+ struct cmc_list_tab *tab = &al_array_at(lp->tabs, lp->selected);
+ s32 count = (s32)tab->list->entries.count;
+
+ bool leader = tab->leader_pressed;
+ if (cmc_ui_consider_input(input, false)) {
+ tab->leader_pressed = false;
+ }
+
+ switch (input->id) {
+ case 'g':
+ CONSIDER_INPUT();
+ if (leader) {
+ if (tab->move.active) {
+ if (tab->move.offset != -tab->move.selected) {
+ tab->move.offset = -tab->move.selected;
+ return true;
+ }
+ } else if (tab->selected != 0) {
+ tab->selected = 0;
+ return true;
+ }
+ } else {
+ tab->leader_pressed = true;
+ }
+ break;
+ case 'G':
+ CONSIDER_INPUT();
+ if (tab->move.active) {
+ if (tab->move.offset != count - 1 - tab->move.selected) {
+ tab->move.offset = count - 1 - tab->move.selected;
+ return true;
+ }
+ } else if (tab->selected != count - 1) {
+ tab->selected = count - 1;
+ return true;
+ }
+ break;
+ case 'f':
+ CONSIDER_INPUT_REPEAT();
+ if (tab->move.active) {
+ if (tab->selected + tab->move.offset < count - lp->page_length) {
+ tab->move.offset += lp->page_length;
+ return true;
+ } else if (tab->move.offset != count - 1 - tab->move.selected) {
+ tab->move.offset = count - 1 - tab->move.selected;
+ return true;
+ }
+ } else {
+ if (tab->selected < count - lp->page_length) {
+ tab->selected += lp->page_length;
+ return true;
+ } else if (tab->selected != count - 1) {
+ tab->selected = count - 1;
+ return true;
+ }
+ }
+ break;
+ case 'b':
+ CONSIDER_INPUT_REPEAT();
+ if (tab->move.active) {
+ if (tab->selected + tab->move.offset > lp->page_length) {
+ tab->move.offset -= lp->page_length;
+ return true;
+ } else if (tab->move.offset != -tab->selected) {
+ tab->move.offset = -tab->selected;
+ return true;
+ }
+ } else {
+ if (tab->selected > lp->page_length) {
+ tab->selected -= lp->page_length;
+ return true;
+ } else if (tab->selected != 0) {
+ tab->selected = 0;
+ return true;
+ }
+ }
+ break;
+ case 'j':
+ CONSIDER_INPUT_REPEAT();
+ if (tab->move.active) {
+ if (tab->selected + tab->move.offset < count - 1) {
+ tab->move.offset++;
+ return true;
+ }
+ } else {
+ if (tab->selected < count - 1) {
+ tab->selected++;
+ return true;
+ }
+ }
+ break;
+ case 'k':
+ CONSIDER_INPUT_REPEAT();
+ if (tab->move.active) {
+ if (tab->selected + tab->move.offset > 0) {
+ tab->move.offset--;
+ return true;
+ }
+ } else {
+ if (tab->selected > 0) {
+ tab->selected--;
+ return true;
+ }
+ }
+ break;
+ case 'i':
+ CONSIDER_INPUT();
+ if (!tab->move.active) {
+ tab->move.active = true;
+ tab->move.selected = tab->selected;
+ tab->move.offset = 0;
+ return true;
+ }
+ break;
+ case NCKEY_ESC:
+ CONSIDER_INPUT();
+ if (tab->move.active) {
+ tab->move.active = false;
+ return true;
+ }
+ break;
+ case NCKEY_RETURN:
+ CONSIDER_INPUT();
+ if (tab->move.active) {
+ tab->move.active = false;
+ return true;
+ } else if (tab->selected != tab->list->current) {
+ camu_client_skipto(&lp->ui->c->client, &tab->list->name, tab->selected);
+ }
+ break;
+ case NCKEY_BUTTON1:
+ /*
+ CONSIDER_INPUT();
+ if (input->x < 0 || input->y < 0) return false;
+ u32 x = (u32)input->x;
+ u32 y = (u32)input->y;
+ ui->last_mouse_x = x;
+ ui->last_mouse_y = y;
+ u64 now = nn_get_timestamp();
+ u64 last = ui->last_mouse_ts;
+ if (last != (u64)-1 && now - last < 100000) {
+ return false;
+ }
+ ui->last_mouse_ts = now;
+ u32 width = ncplane_dim_x(ui->lp.n);
+ u32 height = ncplane_dim_y(ui->lp.n);
+ if (x >= 2 && x <= width && y > height - 3) { // @TODO: Make this defined, more to nowplaying.
+ struct cmc_list_entry *entry = al_array_at(tab->list->entries, tab->list->current);
+ u64 pos = ((x - 2) / (f64)(width - 2)) * entry->duration;
+ camu_client_seek(&ui->c->client, &tab->list->name, entry->id, pos);
+ }
+ */
+ break;
+ }
+
+ return false;
+}
+
+static struct cmc_list_entry *tab_current_entry(struct cmc_list_tab *tab)
+{
+ if (tab->list->current >= 0) {
+ return al_array_at(tab->list->entries, tab->list->current);
+ }
+ return NULL;
+}
+
+bool cmc_list_pane_tick(struct cmc_list_pane *lp)
+{
+ bool tick = false;
+ if (lp->selected != -1) {
+ struct cmc_list_tab *tab = &al_array_at(lp->tabs, lp->selected);
+ if (tab->list->meta_changed) {
+ tab->list->meta_changed = false;
+ tick = true;
+ }
+ tick |= cmc_now_playing_tick(&lp->np, tab_current_entry(tab));
+ }
+ return tick;
+}
+
+void cmc_list_pane_erase(struct cmc_list_pane *lp)
+{
+ ncplane_erase(lp->n);
+ cmc_now_playing_erase(&lp->np);
+}
+
+static void render_list_entries(struct cmc_list_pane *lp, struct cmc_list_tab *tab, s32 max_height)
+{
+ struct cmc_list *list = tab->list;
+ struct ncplane *n = lp->n;
+ s32 tracked = tab->move.active ? tab->move.selected + tab->move.offset : tab->selected;
+ s32 count = (s32)list->entries.count;
+ s32 midpoint = max_height / 2;
+ s32 rounded = (max_height + 1) / 2;
+ s32 top, end;
+ if (tracked < midpoint) {
+ top = 0;
+ end = MIN(top + max_height, count);
+ } else if (tracked + rounded >= count) {
+ top = MAX(count - max_height, 0);
+ end = count;
+ } else {
+ top = tracked - midpoint;
+ end = tracked + rounded;
+ }
+ s32 index;
+ struct cmc_list_entry *entry;
+ for (s32 i = top; i < end; i++) {
+ index = i;
+ if (tab->move.active) {
+ if (index == tracked) {
+ index = tab->move.selected;
+ } else {
+ if (index > tracked) index--;
+ if (index >= tab->move.selected) index++;
+ }
+ }
+ entry = al_array_at(list->entries, index);
+ bool on_current = list->current >= 0 && index == list->current;
+ bool styled = i == tracked;
+ if (styled) {
+ ncplane_set_styles(n, NCSTYLE_BOLD);
+ ncplane_set_fg_palindex(n, 4);
+ }
+ s32 y = i - top;
+ u32 length = entry->name.length;
+ if (tab->move.active && i == tracked) {
+ cmc_ui_putnwstr_yx(n, y, 0, length, &entry->name);
+ } else if (on_current) {
+ ncplane_putchar_yx(n, y, 1, '>');
+ cmc_ui_putnwstr_yx(n, y, 3, length, &entry->name);
+ } else {
+ cmc_ui_putnwstr_yx(n, y, 1, length, &entry->name);
+ }
+ if (styled) {
+ ncplane_set_styles(n, NCSTYLE_NONE);
+ ncplane_set_fg_default(n);
+ }
+ }
+}
+
+void cmc_list_pane_render(struct cmc_list_pane *lp)
+{
+ if (lp->selected == -1) return;
+ struct cmc_list_tab *tab = &al_array_at(lp->tabs, lp->selected);
+ render_list_entries(lp, tab, lp->page_length);
+ cmc_now_playing_render(&lp->np, tab_current_entry(tab));
+}
+
+//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 = list->current,
+// .leader_pressed = false
+// };
+// 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);
+// s32 count = (s32)tab->list->entries.count;
+// bool leader = tab->leader_pressed;
+// if (cmc_ui_consider_input(input, false)) {
+// tab->leader_pressed = false;
+// }
+//
+// switch (input->id) {
+// case 'g':
+// CONSIDER_INPUT();
+// if (leader) {
+// if (tab->move.active) {
+// tab->move.offset = -tab->move.selected;
+// } else {
+// tab->selected = 0;
+// }
+// } else {
+// tab->leader_pressed = true;
+// }
+// break;
+// case 'G':
+// CONSIDER_INPUT();
+// if (tab->move.active) {
+// tab->move.offset = count - 1 - tab->move.selected;
+// } else {
+// tab->selected = count - 1;
+// }
+// break;
+// case 'f':
+// //CONSIDER_INPUT_MOD_CTRL();
+// CONSIDER_INPUT_REPEAT();
+// if (tab->move.active) {
+// if (tab->selected + tab->move.offset < count - tab->page_length) {
+// tab->move.offset += tab->page_length;
+// } else {
+// tab->move.offset = count - 1 - tab->move.selected;
+// }
+// } else {
+// if (tab->selected < count - tab->page_length) {
+// tab->selected += tab->page_length;
+// } else {
+// tab->selected = count - 1;
+// }
+// }
+// break;
+// case 'b':
+// //CONSIDER_INPUT_MOD_CTRL();
+// CONSIDER_INPUT_REPEAT();
+// if (tab->move.active) {
+// if (tab->selected + tab->move.offset > tab->page_length) {
+// tab->move.offset -= tab->page_length;
+// } else {
+// tab->move.offset = -tab->selected;
+// }
+// } else {
+// if (tab->selected > tab->page_length) {
+// tab->selected -= tab->page_length;
+// } else {
+// tab->selected = 0;
+// }
+// }
+// break;
+// case 'j':
+// CONSIDER_INPUT_REPEAT();
+// if (tab->move.active) {
+// if (tab->selected + tab->move.offset < count - 1) {
+// tab->move.offset++;
+// }
+// } else {
+// if (tab->selected < count - 1) {
+// tab->selected++;
+// }
+// }
+// break;
+// case 'k':
+// CONSIDER_INPUT_REPEAT();
+// if (tab->move.active) {
+// if (tab->selected + tab->move.offset > 0) {
+// tab->move.offset--;
+// }
+// } else {
+// if (tab->selected > 0) {
+// tab->selected--;
+// }
+// }
+// break;
+// case 'i':
+// CONSIDER_INPUT();
+// if (!tab->move.active) {
+// tab->move.active = true;
+// tab->move.selected = tab->selected;
+// tab->move.offset = 0;
+// }
+// break;
+// case NCKEY_ESC:
+// CONSIDER_INPUT();
+// if (tab->move.active) {
+// tab->move.active = false;
+// }
+// break;
+// case NCKEY_RETURN:
+// CONSIDER_INPUT();
+// if (tab->move.active) {
+// tab->move.active = false;
+// } else if (tab->selected != tab->list->current) {
+// camu_client_skipto(&ui->c->client, &tab->list->name, tab->selected);
+// }
+// break;
+// case NCKEY_BUTTON1:
+// CONSIDER_INPUT();
+// if (input->x < 0 || input->y < 0) return false;
+// u32 x = (u32)input->x;
+// u32 y = (u32)input->y;
+// ui->last_mouse_x = x;
+// ui->last_mouse_y = y;
+// u64 now = nn_get_timestamp();
+// u64 last = ui->last_mouse_ts;
+// if (last != (u64)-1 && now - last < 100000) {
+// return false;
+// }
+// ui->last_mouse_ts = now;
+// u32 width = ncplane_dim_x(ui->lp.n);
+// u32 height = ncplane_dim_y(ui->lp.n);
+// if (x >= 2 && x <= width && y > height - 3) { // @TODO: Make this defined, more to nowplaying.
+// struct cmc_list_entry *entry = al_array_at(tab->list->entries, tab->list->current);
+// u64 pos = ((x - 2) / (f64)(width - 2)) * entry->duration;
+// camu_client_seek(&ui->c->client, &tab->list->name, entry->id, pos);
+// }
+// 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, s32 max_height)
+//{
+// struct cmc_list *list = tab->list;
+// struct ncplane *n = ui->lp.n;
+// s32 tracked = tab->move.active ? tab->move.selected + tab->move.offset : tab->selected;
+// s32 count = (s32)list->entries.count;
+// s32 midpoint = max_height / 2;
+// s32 rounded = (max_height + 1) / 2;
+// s32 top, end;
+// if (tracked < midpoint) {
+// top = 0;
+// end = MIN(top + max_height, count);
+// } else if (tracked + rounded >= count) {
+// top = count - max_height;
+// end = count;
+// } else {
+// top = tracked - midpoint;
+// end = tracked + rounded;
+// }
+// s32 index;
+// struct cmc_list_entry *entry;
+// for (s32 i = top; i < end; i++) {
+// index = i;
+// if (tab->move.active) {
+// if (index == tracked) {
+// index = tab->move.selected;
+// } else {
+// if (index > tracked) index--;
+// if (index >= tab->move.selected) index++;
+// }
+// }
+// entry = al_array_at(list->entries, index);
+// bool on_current = list->current >= 0 && index == list->current;
+// bool styled = i == tracked;
+// if (styled) {
+// ncplane_set_styles(n, NCSTYLE_BOLD);
+// ncplane_set_fg_palindex(n, 4);
+// }
+// s32 y = i - top;
+// u32 length = entry->name.length;
+// if (tab->move.active && i == tracked) {
+// cmc_ui_putnwstr_yx(n, y, 0, length, &entry->name);
+// } else if (on_current) {
+// ncplane_putchar_yx(n, y, 1, '>');
+// cmc_ui_putnwstr_yx(n, y, 3, length, &entry->name);
+// } else {
+// cmc_ui_putnwstr_yx(n, y, 1, length, &entry->name);
+// }
+// if (styled) {
+// ncplane_set_styles(n, NCSTYLE_NONE);
+// ncplane_set_fg_default(n);
+// }
+// }
+//}
+//
+//static void render_now_playing(struct cmc_ui *ui, struct cmc_list_tab *tab, u32 y, u32 height)
+//{
+// struct ncplane *n = ui->lp.n;
+// u32 width = ncplane_dim_x(n);
+//
+// struct cmc_list_entry *entry = NULL;
+// if (tab->list->entries.count > (u32)tab->list->current) {
+// entry = al_array_at(tab->list->entries, tab->list->current);
+// }
+//
+// u64 pos = 0;
+// u64 duration = 0;
+// u64 remaining = 0;
+// u32 y_off = height - 7;
+//
+// ncplane_set_styles(n, NCSTYLE_BOLD);
+// if (entry) {
+// pos = entry->offset;
+// duration = entry->duration;
+// u64 now = nn_get_timestamp();
+// bool paused = entry->start == LIANA_TIMESTAMP_INVALID;
+// if (!paused && now > entry->start) {
+// pos += now - entry->start;
+// }
+// pos = MIN(pos, duration);
+// remaining = duration - pos;
+// //ncplane_putstr_yx(n, y_off, 2, "▸");
+// //ncplane_putstr_yx(n, y_off, 2, "▶");
+// //ncplane_putstr_yx(n, y_off, 2, "⏵ ");
+// //ncplane_putstr_yx(n, y_off, 2, "|");
+// //ncplane_putstr_yx(n, y_off, 2, "|");
+// //ncplane_putstr_yx(n, y_off, 2, "⏵");
+// //ncplane_putstr_yx(n, y_off, 2, "⏸");
+// if (remaining == 0) {
+// ncplane_putstr_yx(n, y_off - 1, 2, "∨");
+// } else {
+// ncplane_putstr_yx(n, y_off - 1, 2, paused ? "^": ">");
+// }
+// } else {
+// ncplane_putstr_yx(n, y_off - 1, 2, "∨");
+// }
+// ncplane_set_styles(n, NCSTYLE_NONE);
+//
+// f32 percent = 1.f;
+// if (duration > 0) percent = MIN(pos / (f32)duration, 1.f);
+// for (u32 i = 0; i < (width - 2) * percent; i++) {
+// ncplane_putchar_yx(n, y_off, i + 1, '-');
+// }
+//
+// y_off--;
+//
+// char timestr[16] = { 0 }; // max = 829128280:01:49\0
+// s32 offset = camu_print_time(timestr, sizeof(timestr), pos, true, 4);
+// ncplane_putstr_yx(n, y_off, 4, timestr);
+// ncplane_putstr_yx(n, y_off, 4 + offset, "⎹");
+//
+// if (entry) {
+// cmc_ui_putnwstr_yx(n, y_off, 6 + offset, MIN(entry->name.length, (u32)60), &entry->name);
+// }
+//
+// bool show_remaining = false;
+// if (show_remaining) {
+// offset = camu_print_time(timestr, sizeof(timestr), remaining, true, 4);
+// } else {
+// offset = camu_print_time(timestr, sizeof(timestr), duration, true, 4);
+// }
+// ncplane_putstr_yx(n, y_off, width - 4 - offset, "⎸");
+// // "=" Duration, "+" Segment, "-" Remaining.
+// if (show_remaining) {
+// ncplane_putstr_yx(n, y_off, width - 3 - offset, "-");
+// } else {
+// ncplane_putstr_yx(n, y_off, width - 3 - offset, "=");
+// }
+// ncplane_putstr_yx(n, y_off, width - 2 - offset, timestr);
+//
+// /*
+// ncplane_putchar_yx(n, height - 5, 2, 'A');
+// ncplane_putchar_yx(n, height - 4, 2, 'A');
+// ncplane_putchar_yx(n, height - 3, 2, 'A');
+// ncplane_putchar_yx(n, height - 2, 2, 'A');
+// */
+//
+// if (entry && entry->raw.data != NULL) {
+// u32 length = width - 2;
+// u32 top = height - 6;
+// u32 wave_height = 5;
+// if (entry->raw.drawing == NULL || entry != ui->lp.blitted) {
+// entry->raw.drawing = al_malloc(sizeof(char **) * wave_height);
+// for (u32 j = 0; j < wave_height; j++) {
+// entry->raw.drawing[j] = al_malloc(sizeof(char *) * length);
+// for (u32 i = 0; i < length; i++) {
+// entry->raw.drawing[j][i] = al_calloc(1, 6);
+// }
+// }
+// if (!ui->lp.w) {
+// struct ncplane_options opts = { 0 };
+// opts.x = 1;
+// opts.y = top;
+// opts.cols = length;
+// opts.rows = wave_height;
+// ui->lp.w = ncplane_create(n, &opts);
+// }
+// /*
+// struct nccell ncl;
+// nccell_init(&ncl);
+// nccell_load(w, &ncl, " ");
+// nccell_set_bg_palindex(&ncl, 3);
+// ncplane_polyfill_yx(w, 0, 0, &ncl);
+// */
+// u32 *rgba;
+// u32 pixel_width, pixel_height;
+// ncplane_pixel_geom(ui->lp.w, &pixel_height, &pixel_width, NULL, NULL, NULL, NULL);
+// //cmc_draw_waveform(entry, length, wave_height, false, entry->raw.drawing, &rgba);
+// cmc_draw_waveform(entry, pixel_width, pixel_height, false, entry->raw.drawing, &rgba);
+// if (ui->lp.vis) {
+// ncvisual_destroy(ui->lp.vis);
+// }
+// ui->lp.vis = ncvisual_from_rgba(rgba, pixel_height, pixel_width * 4, pixel_width);
+// }
+//
+// if (entry != ui->lp.blitted) {
+// ncplane_erase(ui->lp.w);
+// struct ncvisual_options vopts = { 0 };
+// vopts.blitter = NCBLIT_PIXEL;
+// vopts.scaling = NCSCALE_STRETCH;
+// vopts.flags = NCVISUAL_OPTION_NOINTERPOLATE;
+// vopts.n = ui->lp.w;
+// vopts.x = 0;
+// vopts.y = 0;
+// ncvisual_blit(ui->nc, ui->lp.vis, &vopts);
+// ui->lp.blitted = entry;
+// }
+//
+// /*
+// ncplane_set_fg_palindex(n, 5);
+// for (u32 j = 0; j < wave_height; j++) {
+// for (u32 i = 0; i < length; i++) {
+// char *cell = entry->raw.drawing[j][i];
+// if (cell[0] == 'R') {
+// ncplane_set_bg_palindex(n, 2);
+// }
+// ncplane_putstr_yx(n, top + j, 1 + i, cell + 1);
+// if (cell[0] == 'R') {
+// ncplane_set_bg_default(n);
+// }
+// }
+// }
+// ncplane_set_fg_default(n);
+// */
+// }
+//
+// /*
+// 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(n, height - 5, 5, buf);
+// */
+//
+// u64 c = 0;
+// ncchannels_set_fg_default(&c);
+// ncplane_cursor_move_yx(n, y, 0);
+// ncplane_light_box(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);
+//
+// if (ui->lp.tabs.count <= ui->lp.current) return;
+// struct cmc_list_tab *tab = &al_array_at(ui->lp.tabs, ui->lp.current);
+//
+// tab->page_length = height - 9;
+// render_list_entries(ui, tab, tab->page_length);
+// render_now_playing(ui, tab, tab->page_length, height);
+//}
diff --git a/src/fruits/cmc/ui/panes/list.h b/src/fruits/cmc/ui/panes/list.h
new file mode 100644
index 0000000..bfeed59
--- /dev/null
+++ b/src/fruits/cmc/ui/panes/list.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <al/array.h>
+#include <notcurses/notcurses.h>
+
+#include "../widgets/now_playing.h"
+
+struct cmc_list;
+struct cmc_list_tab {
+ struct cmc_list *list;
+ s32 selected;
+ bool leader_pressed;
+ struct {
+ bool active;
+ s32 selected;
+ s32 offset;
+ } move;
+};
+
+struct cmc_list_pane {
+ struct ncplane *n;
+ array(struct cmc_list_tab) tabs;
+ s32 selected;
+ s32 page_length;
+ struct cmc_now_playing np;
+ struct cmc_ui *ui;
+};
+
+void cmc_list_pane_init(struct cmc_list_pane *lp, struct cmc_ui *ui);
+bool cmc_list_pane_layout(struct cmc_list_pane *lp, struct ncplane *parent);
+void cmc_list_pane_add_list(struct cmc_list_pane *lp, struct cmc_list *list);
+bool cmc_list_pane_handle_input(struct cmc_list_pane *lp, struct ncinput *input);
+bool cmc_list_pane_tick(struct cmc_list_pane *lp);
+void cmc_list_pane_erase(struct cmc_list_pane *lp);
+void cmc_list_pane_render(struct cmc_list_pane *lp);
diff --git a/src/fruits/cmc/ui/panes/log.c b/src/fruits/cmc/ui/panes/log.c
new file mode 100644
index 0000000..be9702c
--- /dev/null
+++ b/src/fruits/cmc/ui/panes/log.c
@@ -0,0 +1,65 @@
+#include "log.h"
+
+void cmc_log_pane_init(struct cmc_log_pane *lp)
+{
+ lp->new_message = false;
+ al_array_init(lp->messages);
+}
+
+bool cmc_log_pane_layout(struct cmc_log_pane *lp, struct ncplane *parent)
+{
+ if (lp->n) ncplane_destroy(lp->n);
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = ncplane_dim_y(parent);
+ nopts.cols = ncplane_dim_x(parent);
+ lp->n = ncplane_create(parent, &nopts);
+ return true;
+}
+
+void cmc_log_pane_push_message(struct cmc_log_pane *lp, char *message)
+{
+ al_array_push(lp->messages, message);
+ lp->new_message = true;
+}
+
+bool cmc_log_pane_handle_input(struct cmc_log_pane *lp, struct ncinput *input)
+{
+ (void)lp;
+ (void)input;
+ return false;
+}
+
+bool cmc_log_pane_tick(struct cmc_log_pane *lp)
+{
+ if (lp->new_message) {
+ lp->new_message = false;
+ return true;
+ }
+ return false;
+}
+
+void cmc_log_pane_erase(struct cmc_log_pane *lp)
+{
+ ncplane_erase(lp->n);
+}
+
+void cmc_log_pane_render(struct cmc_log_pane *lp)
+{
+ struct ncplane *n = lp->n;
+
+ u32 max_height = ncplane_dim_y(n);
+
+ u32 count = lp->messages.count;
+ u32 index = (count > max_height) ? count - max_height : 0;
+ for (u32 i = index; i < count; i++) {
+ // We can't use putnstr to control the width because these messages will have wide characters.
+ ncplane_putstr_yx(n, i - index, 0, al_array_at(lp->messages, i));
+ }
+
+ if (index > 0) {
+ //for (u32 i = 0; i < index; i++) {
+ // al_free(al_array_at(lp->messages, i));
+ //}
+ //al_array_remove_range(lp->messages, 0, index);
+ }
+}
diff --git a/src/fruits/cmc/ui/panes/log.h b/src/fruits/cmc/ui/panes/log.h
new file mode 100644
index 0000000..d79c7e9
--- /dev/null
+++ b/src/fruits/cmc/ui/panes/log.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <al/array.h>
+#include <notcurses/notcurses.h>
+
+struct cmc_log_pane {
+ struct ncplane *n;
+ bool new_message;
+ array(char *) messages;
+};
+
+void cmc_log_pane_init(struct cmc_log_pane *lp);
+bool cmc_log_pane_layout(struct cmc_log_pane *lp, struct ncplane *parent);
+void cmc_log_pane_push_message(struct cmc_log_pane *lp, char *message);
+bool cmc_log_pane_handle_input(struct cmc_log_pane *lp, struct ncinput *input);
+bool cmc_log_pane_tick(struct cmc_log_pane *lp);
+void cmc_log_pane_erase(struct cmc_log_pane *lp);
+void cmc_log_pane_render(struct cmc_log_pane *lp);
diff --git a/src/fruits/cmc/ui/panes/search.c b/src/fruits/cmc/ui/panes/search.c
new file mode 100644
index 0000000..e46c27d
--- /dev/null
+++ b/src/fruits/cmc/ui/panes/search.c
@@ -0,0 +1,226 @@
+#include "../../cmc.h"
+
+#include "../ui.h"
+
+AL_ASSERT_TYPE_SIZE(wchar_t, 4);
+
+//void cmc_sp_init(struct cmc_ui *ui)
+//{
+// 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 = { 0 };
+// nopts.rows = ncplane_dim_y(parent) - 1;
+// nopts.cols = ncplane_dim_x(parent) - 1;
+// ui->sp.n = ncplane_create(parent, &nopts);
+// struct cmc_search_tab tab = {
+// .search = NULL,
+// };
+// 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);
+//}
+//
+//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)
+//{
+// bool entered = false;
+//
+// switch (input->id) {
+// case NCKEY_BACKSPACE:
+// if (tab->cursor > 0) {
+// al_wstr_remove_at(&tab->input_text, tab->cursor - 1);
+// 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:
+// entered = true;
+// // fallthrough
+// case NCKEY_ESC:
+// tab->input_active = false;
+// tab->input_changed = true; // Erase text on next render.
+// break;
+// 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;
+// case 'U':
+// for (u32 i = 1; i <= tab->cursor; i++) {
+// al_wstr_remove_at(&tab->input_text, tab->cursor - i);
+// }
+// tab->cursor = 0;
+// tab->input_changed = true;
+// break;
+// }
+// break;
+// }
+//
+// if (ncinput_alt_p(input) || nckey_synthesized_p(input->id)) {
+// break;
+// }
+//
+// al_wstr_insert(&tab->input_text, tab->cursor, &al_wstr_w((wchar_t *)input->eff_text, 0, 1));
+// tab->cursor++;
+// tab->input_changed = true;
+//
+// /*
+// // 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));
+// }
+// }
+// */
+//
+// break;
+// }
+//
+// tab->visual_cursor = 0;
+// if (tab->cursor > 0) {
+// for (u32 i = 0; i < tab->cursor; i++) {
+// tab->visual_cursor += al_wchar_width(al_wstr_at(&tab->input_text, i));
+// }
+// }
+//
+// return entered;
+//}
+//
+//bool cmc_sp_handle_input(struct cmc_ui *ui, struct ncinput *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)) {
+// str query;
+// al_str_from_wstr(&query, &tab->input_text);
+// camu_client_create_search(&ui->c->client, &al_str_c("youtube"), &query);
+// al_str_free(&query);
+// }
+// if (!tab->input_active) notcurses_cursor_disable(ui->nc);
+// }
+// } else {
+// switch (input->id) {
+// case 'i':
+// CONSIDER_INPUT();
+// 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->visual_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)
+//{
+// 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->visual_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);
+// }
+// }
+// }
+// */
+//}