summaryrefslogtreecommitdiff
path: root/src/fruits/cmc
diff options
context:
space:
mode:
Diffstat (limited to 'src/fruits/cmc')
-rw-r--r--src/fruits/cmc/cmc.c781
-rw-r--r--src/fruits/cmc/meson.build7
2 files changed, 788 insertions, 0 deletions
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
new file mode 100644
index 0000000..7fd8691
--- /dev/null
+++ b/src/fruits/cmc/cmc.c
@@ -0,0 +1,781 @@
+#include <al/lib.h>
+#include <al/wstr.h>
+#include <al/random.h>
+#include <aki/common.h>
+#include <aki/event_loop.h>
+#include <notcurses/notcurses.h>
+#include <notcurses/nckeys.h>
+
+#define STB_IMAGE_IMPLEMENTATION
+#include <stb_image.h>
+
+#include "../../libclient/client.h"
+#include "../../libclient/resource_client.h"
+#include "../../tree/common.h"
+
+enum {
+ CMC_LIST_SEARCHES = 0,
+ CMC_CREATE_SEARCH,
+ CMC_OPEN_SEARCH,
+ CMC_LIST_LISTS,
+ CMC_CREATE_LIST
+};
+
+struct cmc_view {
+ struct sho_post *post;
+ struct ncplane *container;
+ bool has_visual_data;
+};
+
+struct cmc_view_resource {
+ u16 id;
+ str unique_id;
+ u32 index;
+ struct ncvisual *v;
+ struct ncvgeom geom;
+ struct ncvisual_options vopts;
+};
+
+enum {
+ CMC_VIEW_NONE = 0,
+ CMC_VIEW_SINGLE,
+ CMC_VIEW_PARTITION
+};
+
+struct cmc_twitter_view {
+ struct cmc_view v;
+ struct sho_post *repost;
+ struct {
+ u32 width;
+ u32 height;
+ u32 y;
+ struct ncplane *p;
+ } text;
+ struct {
+ struct ncplane *p;
+ struct cmc_view_resource *resource;
+ } profile;
+ struct {
+ u8 mode;
+ u32 width;
+ u32 height;
+ u32 x;
+ struct ncplane *p[4];
+ struct cmc_view_resource *resources[4];
+ } preview;
+ struct {
+ struct ncplane *p;
+ } stats;
+};
+
+struct cmc {
+ u8 mode;
+ struct aki_event_loop loop;
+ struct camu_client client;
+ struct camu_resource_client resource_client;
+ u32 pending_requests;
+ struct notcurses *nc;
+ struct aki_poll input;
+ u32 rows;
+ u32 cols;
+ struct {
+ str provider;
+ str query;
+ s32 id;
+ struct camu_search *search;
+ } search;
+ struct {
+ struct ncplane *p;
+ array(struct cmc_view *) views;
+ u32 offset;
+ u32 roffset;
+ u32 prev;
+ u32 page;
+ u32 selected;
+ } ui;
+ array(struct cmc_view_resource *) resource_cache;
+};
+
+/*
+u64 color = 0;
+u32 background = 0;
+ncchannel_set_rgb8(&background, 255, 255, 255);
+ncchannel_set_alpha(&background, 0);
+//ncchannels_set_bchannel(&color, background);
+ncchannels_set_fchannel(&color, background);
+*/
+
+//struct nccell cell = NCCELL_INITIALIZER(L'█', ncplane_styles(view->v.container), ncplane_channels(view->v.container));
+//ncplane_vline(view->v.container, &cell, rows - 1);
+/*
+//ncplane_gradient(view->v.container, 0, 0, rows, width, "", 0, color, color, color, color);
+struct nccell ul = NCCELL_TRIVIAL_INITIALIZER, ur = NCCELL_TRIVIAL_INITIALIZER;
+struct nccell ll = NCCELL_TRIVIAL_INITIALIZER, lr = NCCELL_TRIVIAL_INITIALIZER;
+struct nccell hl = NCCELL_TRIVIAL_INITIALIZER, vl = NCCELL_TRIVIAL_INITIALIZER;
+nccells_ascii_box(view->v.container, 0, color, &ul, &ur, &ll, &lr, &hl, &vl);
+ncplane_box(view->v.container, &ul, &ur, &ll, &lr, &hl, &vl, rows, width, 0);
+nccell_release(view->v.container, &ul); nccell_release(view->v.container, &ur);
+nccell_release(view->v.container, &ll); nccell_release(view->v.container, &lr);
+nccell_release(view->v.container, &hl); nccell_release(view->v.container, &vl);
+//ncplane_double_box(view->v.container, NCSTYLE_NONE, color, rows, width, 0);
+*/
+
+static u32 count_wrapped_lines(wstr *w, u32 width)
+{
+ u32 counter = 0, wraps = 1;
+ for (u32 i = 0; i < w->len; i++) {
+ wchar_t wc = al_wstr_at(w, i);
+ s32 wcwidth = al_wchar_width(wc);
+ bool newline = wc == L'\n';
+ if (newline || counter + wcwidth > width) {
+ wraps++;
+ counter = 0;
+ }
+ if (newline) continue;
+ counter += wcwidth;
+ }
+ return wraps;
+}
+
+static void date_to_relative_date(struct sho_post *post, char *out)
+{
+ struct sho_post_date *created = NULL;
+ struct sho_post_date *date;
+ al_array_foreach_ptr(post->dates, i, date) {
+ if (date->type == SHOKI_DATE_CREATED) {
+ created = date;
+ break;
+ }
+ }
+ if (!created) return;
+ f64 seconds = (aki_get_timestamp() / 1000000.0 - created->timestamp);
+ s64 minutes = seconds / 60L;
+ s64 hours = minutes / 60L;
+ s64 days = hours / 24L;
+ if (days > 0) {
+ al_sprintf(out, "%lud", days);
+ } else if (hours > 0) {
+ al_sprintf(out, "%luh", hours);
+ } else if (minutes > 0) {
+ al_sprintf(out, "%lum", minutes);
+ } else {
+ al_sprintf(out, "%lus", (s64)seconds);
+ }
+}
+
+static void number_to_shorthand(s64 number, char *out)
+{
+ if (number >= 1000000) {
+ al_sprintf(out, "%.1fM", number / 1000000.f);
+ } else if (number >= 1000) {
+ al_sprintf(out, "%.1fK", number / 1000.f);
+ } else {
+ al_sprintf(out, "%ld", number);
+ }
+}
+
+#define TEXT_OFFSET 10
+
+#define PROFILE_SIZE 5
+#define PREVIEW_OFFSET_WIDTH 26
+
+/*
+static u32 calc_height_for_display(struct ncvgeom *geom, u32 width)
+{
+ f32 ratio = geom->pixx / (f32)geom->pixy;
+ if (ratio >= 1.f) {
+ return ceil((width / ratio) / 2.f) - 2;
+ } else {
+ return ceil((width / ratio) / 2.f) - 2;
+ }
+}
+*/
+
+static void resource_center_zoom(struct cmc_view_resource *resource, s32 width, s32 height)
+{
+ struct ncvgeom *geom = &resource->geom;
+ struct ncvisual_options *vopts = &resource->vopts;
+
+ f32 ratio = geom->pixx / (f32)geom->pixy;
+ f32 preview_ratio = width / (f32)height;
+ f32 diff;
+
+ f32 x0 = 0.f;
+ f32 y0 = 0.f;
+ f32 x1 = geom->pixx;
+ f32 y1 = geom->pixy;
+
+ // Not properly tested.
+ if (preview_ratio > ratio) {
+ diff = (geom->pixy - (geom->pixy / (preview_ratio * ratio))) / 2.f;
+ y0 += diff;
+ y1 -= diff;
+ } else {
+ diff = (geom->pixx - (geom->pixx / (preview_ratio / ratio))) / 2.f;
+ x0 += diff;
+ x1 -= diff;
+ }
+
+ vopts->begx = x0;
+ vopts->begy = y0;
+ vopts->lenx = x1 - x0;
+ vopts->leny = y1 - y0;
+}
+
+static void get_post_and_maybe_repost(struct cmc *c, str *unique_id, struct sho_post **post, struct sho_post **repost)
+{
+ *post = sho_post_cache_get(&c->client.cache, unique_id);
+ *repost = NULL;
+ if ((*post)->type == SHOKI_POST_REPOST) {
+ *repost = *post;
+ *post = sho_post_cache_get(&c->client.cache, &(*post)->post.unique_id);
+ }
+}
+
+static u32 layout_tweet(struct cmc *c, struct cmc_twitter_view *view, u32 width)
+{
+ struct sho_post *post = view->v.post;
+ struct sho_post *repost = view->repost;
+
+ u32 rows = 1;
+
+ view->text.width = width - TEXT_OFFSET;
+ view->text.height = 1 + count_wrapped_lines(&post->text, view->text.width);
+ if (repost) {
+ view->text.y = 1;
+ } else {
+ view->text.y = 0;
+ }
+ view->text.height += view->text.y;
+ rows += view->text.height;
+
+ view->v.has_visual_data = true;
+
+ for (u32 i = 0; i < 4; i++) {
+ if (view->preview.resources[i] && !view->preview.resources[i]->v) {
+ view->v.has_visual_data = false;
+ break;
+ }
+ }
+
+ if (view->preview.mode == CMC_VIEW_NONE || !view->v.has_visual_data) return rows;
+
+ view->preview.width = view->text.width;
+ u32 partition_width = view->preview.width / 2;
+ view->preview.x = TEXT_OFFSET + 1;
+ view->preview.height = 0;
+
+ struct ncplane_options opts = { 0 };
+ opts.cols = view->preview.width;
+ opts.rows = ncplane_dim_y(notcurses_stdplane(c->nc)) / 3;
+ opts.x = 0;
+ opts.y = 0;
+ struct ncplane *inter = ncplane_create(notcurses_stdplane(c->nc), &opts);
+
+ u32 left = 0, right = 0;
+ struct cmc_view_resource *resource;
+ switch (view->preview.mode) {
+ case CMC_VIEW_SINGLE: {
+ resource = view->preview.resources[0];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+ break;
+ }
+ case CMC_VIEW_PARTITION: {
+ ncplane_resize_simple(inter, opts.rows, partition_width);
+
+ resource = view->preview.resources[0];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+
+ resource = view->preview.resources[1];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ right += resource->geom.rcelly + 1;
+
+ if (view->preview.resources[2]) {
+ resource = view->preview.resources[2];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+ }
+
+ if (view->preview.resources[3]) {
+ resource = view->preview.resources[3];
+ resource->vopts.n = inter;
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ right += resource->geom.rcelly + 1;
+ }
+
+ break;
+ }
+ }
+ view->preview.height = AL_MAX(right, left);
+
+ ncplane_destroy(inter);
+
+ return rows + view->preview.height;
+}
+
+static char format_buf[32];
+
+static void draw_tweet(struct cmc *c, struct cmc_twitter_view *view, struct ncplane *container, u32 rows, u32 width)
+{
+ struct sho_post *post = view->v.post;
+ struct sho_post *repost = view->repost;
+
+ ncplane_resize_simple(container, rows, width);
+
+ if (view->v.has_visual_data) {
+ u32 partition_width = view->preview.width / 2;
+ struct cmc_view_resource *resource;
+ switch (view->preview.mode) {
+ case CMC_VIEW_NONE:
+ break;
+ case CMC_VIEW_SINGLE: {
+ resource = view->preview.resources[0];
+ ncplane_resize_simple(view->preview.p[0], resource->geom.rcelly + 1, view->preview.width);
+ ncplane_move_yx(view->preview.p[0], view->text.height, view->preview.x);
+ resource->vopts.n = view->preview.p[0];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ break;
+ }
+ case CMC_VIEW_PARTITION: {
+ resource = view->preview.resources[0];
+ resource->vopts.n = view->preview.p[0];
+ ncplane_resize_simple(view->preview.p[0], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[0], view->text.height, view->preview.x);
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+
+ resource = view->preview.resources[1];
+ ncplane_resize_simple(view->preview.p[1], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[1], view->text.height, (view->preview.x + view->preview.resources[0]->geom.rcellx) + 2);
+ resource->vopts.n = view->preview.p[1];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+
+ if (view->preview.resources[2]) {
+ resource = view->preview.resources[2];
+ ncplane_resize_simple(view->preview.p[2], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[2], view->preview.resources[0]->geom.rcelly + view->text.height + 2, view->preview.x);
+ resource->vopts.n = view->preview.p[2];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+
+ if (view->preview.resources[3]) {
+ resource = view->preview.resources[3];
+ ncplane_resize_simple(view->preview.p[3], resource->geom.rcelly + 1, partition_width);
+ ncplane_move_yx(view->preview.p[3], view->preview.resources[2]->geom.rcelly + view->text.height + 2, (view->preview.x + view->preview.resources[2]->geom.rcellx) + 2);
+ resource->vopts.n = view->preview.p[3];
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+
+ break;
+ }
+ }
+ }
+
+ ncplane_resize_simple(view->text.p, view->text.height, width);
+ ncplane_move_yx(view->text.p, 0, 0);
+
+ s32 offset = TEXT_OFFSET;
+
+ if (repost) {
+ const wchar_t *retweet = L"⮤⮧";
+ ncplane_set_styles(view->text.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->text.p, 0, offset - 3, retweet);
+ ncplane_set_styles(view->text.p, NCSTYLE_NONE);
+ ncplane_putwstr_yx(view->text.p, 0, offset, &al_wstr_at(&repost->author.display_name, 0));
+ }
+
+ ncplane_set_styles(view->text.p, NCSTYLE_BOLD);
+
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, &al_wstr_at(&post->author.display_name, 0));
+ offset += al_wstr_width(&post->author.display_name) + 1;
+
+ ncplane_set_styles(view->text.p, NCSTYLE_NONE);
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, &al_wstr_at(&post->author.username, 0));
+ offset += al_wstr_width(&post->author.username) + 1;
+
+ ncplane_putwstr_yx(view->text.p, view->text.y, offset, L"∙");
+ offset += 2;
+
+ date_to_relative_date(post, format_buf);
+ ncplane_putstr_yx(view->text.p, view->text.y, offset, format_buf);
+
+ u32 text_y = view->text.y + 1, text_x = TEXT_OFFSET;
+ for (u32 i = 0; i < post->text.len; i++) {
+ wchar_t wc = al_wstr_at(&post->text, i);
+ s32 wcwidth = al_wchar_width(wc);
+ bool newline = wc == L'\n';
+ if (newline || text_x + wcwidth > width) {
+ text_y++;
+ text_x = TEXT_OFFSET;
+ }
+ if (newline) continue;
+ ncplane_putwc_yx(view->text.p, text_y, text_x, wc);
+ text_x += wcwidth;
+ }
+
+ ncplane_resize_simple(view->stats.p, 1, view->text.width);
+ ncplane_move_yx(view->stats.p, rows - 1, TEXT_OFFSET);
+
+ u32 part = 10;
+ offset = 1;
+
+ if (post->comments.set) {
+ const wchar_t *comment = L"🗩";
+ //const wchar_t *comment = L"[]";
+ //const wchar_t *comment = L"。○";
+ //const wchar_t *comment = L"❑";
+ //const wchar_t *comment = L"。ロ";
+ //const wchar_t *comment = L".❍";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, comment);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->comments.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+ offset += part;
+
+ if (post->reposts.set) {
+ const wchar_t *retweet = L"⮤⮧";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, retweet);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->reposts.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+ offset += part;
+
+ if (post->likes.set) {
+ const wchar_t *like = L"❤";
+ ncplane_set_styles(view->stats.p, NCSTYLE_ITALIC);
+ ncplane_putwstr_yx(view->stats.p, 0, offset, like);
+ ncplane_set_styles(view->stats.p, NCSTYLE_NONE);
+ number_to_shorthand(post->likes.i, format_buf);
+ ncplane_putstr_yx(view->stats.p, 0, offset + 3, format_buf);
+ }
+
+ if (view->profile.resource && view->profile.resource->v) {
+ struct cmc_view_resource *resource = view->profile.resource;
+ ncplane_resize_simple(view->profile.p, (PROFILE_SIZE + 1) / 2, PROFILE_SIZE);
+ ncplane_move_yx(view->profile.p, view->text.y, PROFILE_SIZE - 2);
+ resource->vopts.n = view->profile.p;
+ ncvisual_blit(c->nc, resource->v, &resource->vopts);
+ }
+}
+
+static void layout_tweets(struct cmc *c)
+{
+ struct cmc_twitter_view *view;
+ for (u32 i = 0; i < c->ui.views.size; i++) {
+ view = (struct cmc_twitter_view *)al_array_at(c->ui.views, i);
+ ncplane_erase(view->v.container);
+ ncplane_erase(view->text.p);
+ ncplane_erase(view->profile.p);
+ for (u32 i = 0; i < 4; i++) {
+ ncplane_erase(view->preview.p[i]);
+ }
+ ncplane_erase(view->stats.p);
+ ncplane_reparent_family(view->v.container, view->v.container);
+ }
+
+ u32 y = 0;
+ u32 width = ncplane_dim_x(c->ui.p);
+ u32 rows = 0;
+ u32 i = c->ui.offset;
+ c->ui.page = 0;
+ for (; i < c->ui.views.size; i++) {
+ view = (struct cmc_twitter_view *)al_array_at(c->ui.views, i);
+ rows = layout_tweet(c, view, width);
+ if (y + rows > ncplane_dim_y(c->ui.p)) break;
+ c->ui.page++;
+ draw_tweet(c, view, view->v.container, rows, width);
+ ncplane_reparent_family(view->v.container, c->ui.p);
+ ncplane_move_yx(view->v.container, y, 0);
+ if (i - c->ui.offset == c->ui.selected) {
+ wchar_t box = L'█';
+ u32 i = view->repost ? 1 : 0;
+ for (; i < rows - 1; i++) {
+ ncplane_cursor_move_yx(view->v.container, i, 0);
+ ncplane_putwc(view->v.container, box);
+ }
+ }
+ y += rows + 1;
+ }
+
+ if (!c->ui.page) c->ui.page = 1;
+
+ if (c->ui.roffset != c->ui.offset) {
+ notcurses_refresh(c->nc, NULL, NULL);
+ }
+ notcurses_render(c->nc);
+
+ c->ui.roffset = c->ui.offset;
+
+ if (c->pending_requests == 0 && c->ui.offset + c->ui.page >= c->ui.views.size) {
+ camu_client_more_results(&c->client, c->search.search);
+ }
+}
+
+static void resource_client_callback(void *userdata, u16 id, struct aki_buffer *buffer)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ struct cmc_view_resource *resource;
+ al_array_foreach(c->resource_cache, i, resource) {
+ if (resource->id == id) {
+ s32 w, h, channels;
+ u8 *pixels = stbi_load_from_memory(aki_buffer_get_ptr(buffer, 0), buffer->size, &w, &h, &channels, 4);
+ resource->v = ncvisual_from_rgba(pixels, h, w * 4, w);
+ stbi_image_free(pixels);
+ ncvisual_geom(c->nc, resource->v, &resource->vopts, &resource->geom);
+ break;
+ }
+ }
+ if (--c->pending_requests == 0) layout_tweets(c);
+}
+
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ (void)revents;
+ struct ncinput input;
+ u32 ret;
+ bool layout = false;
+ do {
+ ret = notcurses_get_nblock(c->nc, &input);
+ if (ret == (u32)-1 || ret == 0) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ if (input.id == 'j') {
+ if (c->ui.selected + 1 >= c->ui.page) {
+ if (c->ui.offset + c->ui.page >= c->ui.views.size) {
+ return;
+ }
+ c->ui.offset += c->ui.page;
+ c->ui.prev = c->ui.page;
+ c->ui.page = 0;
+ c->ui.selected = 0;
+ } else {
+ c->ui.selected++;
+ }
+ layout = true;
+ } else if (input.id == 'k') {
+ if ((s32)c->ui.selected - 1 < 0) {
+ if (c->ui.offset == 0 || (s32)c->ui.offset - (s32)c->ui.prev < 0) {
+ return;
+ }
+ c->ui.offset -= c->ui.prev;
+ c->ui.selected = c->ui.prev - 1;
+ c->ui.prev = 0;
+ } else {
+ c->ui.selected--;
+ }
+ layout = true;
+ } else if (input.id == NCKEY_RETURN) {
+ if (c->ui.views.size > 0) {
+ u32 selected = c->ui.offset + c->ui.selected;
+ struct cmc_twitter_view *view = (struct cmc_twitter_view *)al_array_at(c->ui.views, selected);
+ camu_client_add(&c->client, &view->v.post->unique_id, 0);
+ }
+ } else if (input.id == 'q') {
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ }
+ } while (1);
+ if (layout) layout_tweets(c);
+}
+
+static struct cmc_view_resource *create_resource(struct cmc *c, str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource = al_alloc_object(struct cmc_view_resource);
+ al_array_push(c->resource_cache, resource);
+ resource->vopts.blitter = NCBLIT_PIXEL;
+ //resource->vopts.blitter = NCBLIT_2x1;
+ resource->vopts.scaling = NCSCALE_SCALE_HIRES;
+ //resource->vopts.scaling = NCSCALE_STRETCH;
+ resource->id = al_rand_u16();
+ al_str_clone(&resource->unique_id, unique_id);
+ resource->index = index;
+ c->pending_requests++;
+ camu_resource_request(&c->resource_client, unique_id, index, resource->id);
+ return resource;
+}
+
+static struct cmc_view_resource *get_resource_from_cache(struct cmc *c, str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource;
+ al_array_foreach(c->resource_cache, i, resource) {
+ if (al_str_eq(&resource->unique_id, unique_id) && resource->index == index) {
+ return resource;
+ }
+ }
+ return create_resource(c, unique_id, index);
+}
+
+static int resize_cb(struct ncplane *p)
+{
+ struct cmc *c = (struct cmc *)ncplane_userptr(p);
+ u32 width, height;
+ notcurses_stddim_yx(c->nc, &height, &width);
+ struct ncplane_options nopts = { 0 };
+ nopts.rows = height - 1;
+ nopts.cols = width - 4;
+ nopts.x = 3;
+ nopts.y = 1;
+ nopts.flags = 0;
+ struct ncplane *oldp = c->ui.p;
+ c->ui.p = ncplane_create(p, &nopts);
+ if (oldp) {
+ ncplane_destroy(oldp);
+ layout_tweets(c);
+ }
+ return 0;
+}
+
+static void client_callback(void *userdata, u8 op, void *opaque)
+{
+ struct cmc *c = (struct cmc *)userdata;
+ switch (op) {
+ case CAMU_CLIENT_STATUS_UPDATED: {
+ switch (c->mode) {
+ case CMC_LIST_SEARCHES: {
+ struct camu_search *search;
+ al_array_foreach(c->client.state.searches, i, search) {
+ al_printf("%i %.*s\n", search->id, AL_STR_PRINTF(&search->query));
+ }
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CMC_LIST_LISTS: {
+ struct camu_list *list;
+ al_array_foreach(c->client.state.lists, i, list) {
+ al_printf("%.*s\n", AL_STR_PRINTF(&list->name));
+ }
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CMC_CREATE_SEARCH: {
+ camu_client_create_search(&c->client, &c->search.provider, &c->search.query);
+ break;
+ }
+ case CMC_OPEN_SEARCH: {
+ if (!(c->nc = notcurses_init(NULL, stdin))) return;
+ if (c->client.state.searches.size == 0) return;
+ c->search.search = al_array_at(c->client.state.searches, 0);
+ struct ncplane *stdplane = notcurses_stdplane(c->nc);
+ ncplane_set_resizecb(stdplane, resize_cb);
+ ncplane_set_userptr(stdplane, c);
+ resize_cb(stdplane);
+ camu_client_resume_search(&c->client, c->search.search);
+ aki_poll_init(&c->input, input_poll_callback, c);
+ aki_poll_set(&c->input, notcurses_inputready_fd(c->nc), AKI_POLL_READ);
+ aki_poll_start(&c->input, &c->loop);
+ break;
+ }
+ }
+ break;
+ }
+ case CAMU_CLIENT_SEARCH_CREATED: {
+ aki_event_loop_break(&c->loop);
+ break;
+ }
+ case CAMU_CLIENT_RESULTS: {
+ struct camu_search_results *results = (struct camu_search_results *)opaque;
+ str *unique_id;
+ al_array_foreach_ptr(results->unique_ids, i, unique_id) {
+ struct cmc_twitter_view *view = al_alloc_object(struct cmc_twitter_view);
+ al_array_push(c->ui.views, (struct cmc_view *)view);
+ get_post_and_maybe_repost(c, unique_id, &view->v.post, &view->repost);
+
+ struct ncplane_options default_options = { 0 };
+ default_options.cols = 1;
+ default_options.rows = 1;
+ default_options.x = 0;
+ default_options.y = 0;
+
+ view->v.container = ncpile_create(c->nc, &default_options);
+ view->text.p = ncplane_create(view->v.container, &default_options);
+ view->profile.p = ncplane_create(view->v.container, &default_options);
+ for (u32 i = 0; i < 4; i++) {
+ view->preview.p[i] = ncplane_create(view->v.container, &default_options);
+ }
+ view->stats.p = ncplane_create(view->v.container, &default_options);
+
+ u32 prev_pending = c->pending_requests;
+
+ struct sho_post *post = view->v.post;
+ view->profile.resource = get_resource_from_cache(c, &post->author.unique_id, 0);
+ struct sho_post_media *media;
+ al_array_foreach_ptr(post->media, i, media) {
+ (void)media;
+ view->preview.resources[i] = get_resource_from_cache(c, &post->unique_id, i);
+ }
+
+ view->v.has_visual_data = prev_pending == c->pending_requests;
+
+ if (post->media.size == 0) {
+ view->preview.mode = CMC_VIEW_NONE;
+ } else if (post->media.size == 1) {
+ view->preview.mode = CMC_VIEW_SINGLE;
+ } else {
+ view->preview.mode = CMC_VIEW_PARTITION;
+ }
+ }
+ if (c->pending_requests == 0) layout_tweets(c);
+ break;
+ }
+ }
+}
+
+s32 main(s32 argc, char *argv[])
+{
+ if (argc < 2 || !aki_common_init()) return EXIT_FAILURE;
+
+ struct cmc c = { 0 };
+
+ al_array_init(c.ui.views);
+ al_array_init(c.resource_cache);
+ c.pending_requests = 0;
+ c.ui.offset = 0;
+
+ str *cmd = al_str_c(argv[1]);
+ if (al_str_eq(cmd, al_str_c("search"))) {
+ if (argc == 2) {
+ c.mode = CMC_LIST_SEARCHES;
+ } else {
+ c.mode = CMC_CREATE_SEARCH;
+ al_str_from(&c.search.provider, "twitter");
+ al_str_from(&c.search.query, argv[2]);
+ }
+ } else if (al_str_eq(cmd, al_str_c("open"))) {
+ c.mode = CMC_OPEN_SEARCH;
+ } else if (al_str_eq(cmd, al_str_c("list"))) {
+ if (argc == 2) {
+ c.mode = CMC_LIST_LISTS;
+ } else {
+ }
+ } else {
+ return EXIT_FAILURE;
+ }
+
+ aki_event_loop_init(&c.loop);
+
+ camu_client_init(&c.client, &c.loop, client_callback, &c);
+ camu_client_login(&c.client, al_str_c("andrew"), al_str_c("127.0.0.1"), TREE_PORT);
+
+ camu_resource_client_init(&c.resource_client, &c.loop, resource_client_callback, &c);
+ camu_resource_client_connect(&c.resource_client, al_str_c("127.0.0.1"), TREE_RESOURCE_PORT);
+
+ aki_event_loop_run(&c.loop);
+
+ if (c.mode == CMC_OPEN_SEARCH) notcurses_stop(c.nc);
+
+ camu_client_close(&c.client);
+
+ aki_common_close();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build
new file mode 100644
index 0000000..45ac9c2
--- /dev/null
+++ b/src/fruits/cmc/meson.build
@@ -0,0 +1,7 @@
+cmc_src = ['cmc.c']
+cmc_deps = [common_deps, libclient, cache, stb_image]
+
+notcurses = dependency('notcurses')
+cmc_deps += [notcurses]
+
+executable('cmc', cmc_src, dependencies: cmc_deps)