summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Opalach <andrew@akon.city> 2024-02-12 21:09:10 -0500
committerAndrew Opalach <andrew@akon.city> 2024-02-12 21:09:10 -0500
commitbbf3314165182e402ff25acccddc004a87f81ef0 (patch)
tree192eb958781a59e9601e7b7eef9d0eadcb9b8b11
parent0b7a612a9baf49765db8b127e696913100b70f5f (diff)
downloadcamu-bbf3314165182e402ff25acccddc004a87f81ef0.tar.gz
camu-bbf3314165182e402ff25acccddc004a87f81ef0.tar.bz2
camu-bbf3314165182e402ff25acccddc004a87f81ef0.zip
Initial cmc implementation, sink fixes
Signed-off-by: Andrew Opalach <andrew@akon.city>
-rw-r--r--.gitignore3
-rw-r--r--flake.lock6
-rw-r--r--src/bimu/vcr.c11
-rw-r--r--src/buffer/audio.c5
-rw-r--r--src/fruits/cmc/cmc.c1098
-rw-r--r--src/fruits/cmc/cmc.h27
-rw-r--r--src/fruits/cmc/meson.build11
-rw-r--r--src/fruits/cmc/tui/pager.c88
-rw-r--r--src/fruits/cmc/tui/pager.h33
-rw-r--r--src/fruits/cmc/tui/pane_search.c330
-rw-r--r--src/fruits/cmc/tui/pane_search.h29
-rw-r--r--src/fruits/cmc/tui/resources.c85
-rw-r--r--src/fruits/cmc/tui/resources.h31
-rw-r--r--src/fruits/cmc/tui/ui.c133
-rw-r--r--src/fruits/cmc/tui/ui.h38
-rw-r--r--src/fruits/cmc/tui/view.h24
-rw-r--r--src/fruits/cmc/tui/view_twitter.c370
-rw-r--r--src/fruits/cmc/tui/view_twitter.h35
-rw-r--r--src/libclient/client.c2
-rw-r--r--src/libclient/client.h2
-rw-r--r--src/libclient/resource_client.c4
-rw-r--r--src/libsink/sink.c17
-rw-r--r--src/screen/screen.c4
-rw-r--r--src/tree/common.h2
-rw-r--r--src/tree/tree.c16
-rw-r--r--src/tree/tree.h2
-rw-r--r--src/util/meson.build5
-rw-r--r--subprojects/BLAKE3.wrap6
-rw-r--r--subprojects/packagefiles/BLAKE3/CMakeLists.txt1
29 files changed, 1296 insertions, 1122 deletions
diff --git a/.gitignore b/.gitignore
index aea0bfc..06778dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ doc/**.html
env/bin/
old/
old_old/
+subprojects/BLAKE3/
subprojects/c89atomic/
subprojects/c89atomic.wrap
subprojects/ffmpeg-*/
@@ -17,6 +18,8 @@ subprojects/jansson/
subprojects/jansson.wrap
subprojects/libakiyo
subprojects/libalabaster
+subprojects/libakiyo/
+subprojects/libalabaster/
subprojects/libass/
subprojects/libcdio-paranoia/
subprojects/libcurl/
diff --git a/flake.lock b/flake.lock
index 06c1ded..f792602 100644
--- a/flake.lock
+++ b/flake.lock
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1706732774,
- "narHash": "sha256-hqJlyJk4MRpcItGYMF+3uHe8HvxNETWvlGtLuVpqLU0=",
+ "lastModified": 1707546158,
+ "narHash": "sha256-nYYJTpzfPMDxI8mzhQsYjIUX+grorqjKEU9Np6Xwy/0=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "b8b232ae7b8b144397fdb12d20f592e5e7c1a64d",
+ "rev": "d934204a0f8d9198e1e4515dd6fec76a139c87f0",
"type": "github"
},
"original": {
diff --git a/src/bimu/vcr.c b/src/bimu/vcr.c
index fcef62e..109edd7 100644
--- a/src/bimu/vcr.c
+++ b/src/bimu/vcr.c
@@ -26,8 +26,7 @@ static aki_thread_result AKI_THREADCALL vcr_stream_thread(void *userdata)
{
struct bmu_vcr_stream *stream = (struct bmu_vcr_stream *)userdata;
struct bmu_vcr *vcr = stream->vcr;
- do {
- if (!aki_packet_cache_wait(&stream->cache)) break;
+ while (aki_packet_cache_wait(&stream->cache)) {
struct aki_packet *packet = aki_packet_cache_pop(&stream->cache);
if (!packet) {
stream->client->handle_eof(stream->client);
@@ -47,7 +46,7 @@ static aki_thread_result AKI_THREADCALL vcr_stream_thread(void *userdata)
}
stream->client->handle_data_packet(stream->client, packet);
aki_packet_free(packet);
- } while (1);
+ }
return 0;
}
@@ -101,6 +100,8 @@ bool bmu_vcr_push_packet(struct bmu_vcr *vcr, struct aki_packet *packet)
aki_packet_free(packet);
al_log_warn("bimu", "Unhandled error packet.");
break;
+ default:
+ al_assert(false);
}
return true;
}
@@ -114,7 +115,9 @@ static void vcr_stream_close_internal(struct bmu_vcr_stream *stream)
aki_cond_signal(&stream->cond);
}
aki_mutex_unlock(&stream->mutex);
- aki_thread_join(&stream->thread);
+ if (stream->running) {
+ aki_thread_join(&stream->thread);
+ }
struct aki_packet *packet;
while ((packet = aki_packet_cache_pop(&stream->cache))) {
aki_packet_free(packet);
diff --git a/src/buffer/audio.c b/src/buffer/audio.c
index 037d977..e32ef1f 100644
--- a/src/buffer/audio.c
+++ b/src/buffer/audio.c
@@ -4,7 +4,7 @@
#include "common.h"
#define BUFFER_USEC (12 * 1000000L)
-#define BUFFER_WATERMARK_LOW (3 * 1000000L) // Must be a most half of the buffer size.
+#define BUFFER_WATERMARK_LOW (4 * 1000000L) // Must be a most half of the buffer size.
#define BUFFER_WATERMARK_HIGH (3 * 1000000L)
#define DESYNC_PTS 0.022
@@ -35,8 +35,7 @@ static bool setup_optimal_resampler(struct camu_audio_buffer *buf, struct camu_l
struct camu_lav_resample_fmt *fmt)
{
/*
- sw
- // Update node start?itch ((enum AVSampleFormat)fmt->in_format) {
+ switch ((enum AVSampleFormat)fmt->in_format) {
case AV_SAMPLE_FMT_S16P:
fmt->req_format = AV_SAMPLE_FMT_S16;
break;
diff --git a/src/fruits/cmc/cmc.c b/src/fruits/cmc/cmc.c
index f70cf8d..97ae15d 100644
--- a/src/fruits/cmc/cmc.c
+++ b/src/fruits/cmc/cmc.c
@@ -4,1068 +4,19 @@
#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 = 0,
- CMC_SEARCH
-};
-
-enum {
- CMC_ACTION_OPEN = 0,
- CMC_ACTION_LIST,
- CMC_ACTION_CREATE,
-};
-
-enum {
- CMC_VIEW_NONE = 0,
- CMC_VIEW_SINGLE,
- CMC_VIEW_PARTITION
-};
-
-struct cmc_view {
- struct sho_post *post;
- struct ncplane *container;
- u32 rows;
- u32 y;
- u32 pending_requests;
-};
-
-struct cmc_view_resource {
- u16 id;
- str unique_id;
- u32 index;
- array(struct cmc_view *) refs;
- struct ncvisual *v;
- struct ncvgeom geom;
- struct ncvisual_options vopts;
-};
-
-struct cmc_view_twitter {
- 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_view_page {
- u32 num;
- bool processed;
- array(struct cmc_view *) views;
-};
-
-enum {
- SEARCH_NONE = 0,
- SEARCH_UP,
- SEARCH_DOWN
-};
-
-struct cmc_search {
- struct camu_search *search;
- u8 find_anchor;
- s32 anchor;
- bool redraw;
- u32 count;
- s32 selected;
- array(struct cmc_view *) prev;
- u32 max_page_num;
- u32 min_page_num;
- s32 end_of_page;
- u32 pending_requests;
- array(struct cmc_view_page *) pages;
-};
-
-struct cmc_tui {
- struct notcurses *nc;
- u32 cols;
- u32 rows;
- struct ncplane *p;
- struct {
- struct ncplane *p;
- u32 cols;
- u32 rows;
- bool active;
- s32 selected;
- } overlay;
- struct aki_poll input;
- u32 req_index;
- array(struct cmc_search *) searches;
- struct cmc_search *search;
-};
-
-struct cmc {
- u8 type;
- u8 action;
- struct aki_event_loop loop;
- struct camu_client client;
- struct cmc_tui tui;
- u32 pending_requests;
- struct camu_resource_client resource_client;
- 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 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 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;
-}
-
-#define TEXT_OFFSET 10
-
-#define PROFILE_SIZE 5
-#define PREVIEW_OFFSET_WIDTH 26
-
-static u32 layout_tweet(struct cmc_tui *tui, struct cmc_view_twitter *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);
- view->text.y = repost ? 1 : 0;
- view->text.height += view->text.y;
- rows += view->text.height;
-
- if (view->preview.mode == CMC_VIEW_NONE) {
- return rows;
- }
-
- view->preview.width = view->text.width - 1;
- view->preview.x = TEXT_OFFSET;
- u32 partition_width = view->preview.width / 2;
-
- struct ncplane_options opts = { 0 };
- opts.cols = view->preview.width;
- opts.rows = ncplane_dim_y(notcurses_stdplane(tui->nc)) / 3;
- opts.x = 0;
- opts.y = 0;
- struct ncplane *inter = ncplane_create(notcurses_stdplane(tui->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(tui->nc, resource->v, &resource->vopts, &resource->geom);
- // rcellx == width case very untested.
- left += resource->geom.rcelly + (s32)((resource->geom.rcellx != view->preview.width) ? 1 : -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(tui->nc, resource->v, &resource->vopts, &resource->geom);
- left += resource->geom.rcelly + 1;
-
- resource = view->preview.resources[1];
- resource->vopts.n = inter;
- ncvisual_geom(tui->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(tui->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(tui->nc, resource->v, &resource->vopts, &resource->geom);
- right += resource->geom.rcelly + 1;
- }
-
- break;
- }
- }
-
- ncplane_destroy(inter);
-
- view->preview.height = AL_MAX(right, left);
-
- return rows + view->preview.height;
-}
-
-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 >= 1000000000) {
- al_sprintf(out, "%.1fB", number / 1000000000.f);
- } else 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);
- }
-}
-
-
-static void draw_tweet_resources(struct cmc_tui *tui, struct cmc_view_twitter *view)
-{
- 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(tui->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(tui->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) + 3);
- resource->vopts.n = view->preview.p[1];
- ncvisual_blit(tui->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(tui->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[1]->geom.rcelly + view->text.height + 2, (view->preview.x + view->preview.resources[2]->geom.rcellx) + 3);
- resource->vopts.n = view->preview.p[3];
- ncvisual_blit(tui->nc, resource->v, &resource->vopts);
- }
-
- break;
- }
- }
-
- 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(tui->nc, resource->v, &resource->vopts);
- }
-}
-
-static char format_buf[32];
-
-static void draw_tweet(struct cmc_view_twitter *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);
-
- 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 = 0;
-
- if (post->comments.set) {
- 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"❤";
- 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 + 2, format_buf);
- }
-}
-
-static struct cmc_view_page *get_page_from_num(struct cmc_search *search, u32 num)
-{
- struct cmc_view_page *page;
- al_array_foreach(search->pages, i, page) {
- if (page->num == num) return page;
- }
- return NULL;
-}
-
-static struct cmc_view_page *get_page_containing_index(struct cmc_search *search, u32 index, u32 *offset)
-{
- u32 count = 0, size = 0;
- struct cmc_view_page *page = NULL;
- for (u32 i = search->min_page_num; i <= search->max_page_num; i++) {
- if (!(page = get_page_from_num(search, i))) continue;
- size = page->views.size;
- count += size;
- if (count > index) {
- if (offset) *offset = size - (count - index);
- return page;
- }
- }
- return NULL;
-}
-
-static void request_page_internal(struct cmc *c, u32 num)
-{
- struct cmc_search *search = c->tui.search;
- search->pending_requests++;
- camu_client_get_page(&c->client, search->search, num);
-}
-
-// Given an index of a post, request the page to the offset of the page containing that post.
-static bool request_page_at_page_offset(struct cmc *c, struct cmc_search *search, u32 index, s32 offset)
-{
- struct cmc_view_page *page = get_page_containing_index(search, index, NULL);
- if (page) {
- s32 req = (s32)page->num + offset;
- if (req < 0) return false;
- request_page_internal(c, req);
- }
- return true;
-}
-
-static bool iterate_views_to_height(struct cmc_search *search, u32 height, u32 anchor, u32 *count,
- void (*func)(void *, struct cmc_view_twitter *, u32), void *userdata)
-{
- u32 y = 0, offset;
- struct cmc_view_page *page = get_page_containing_index(search, anchor, &offset);
- al_assert(page);
- struct cmc_view_twitter *view;
- do {
- if (!page->processed) {
- // If the page is not processed, return out of space as to not display
- // a discontinuity (if the page after happens to be processed).
- return true;
- }
- for (; offset < page->views.size; offset++) {
- view = (struct cmc_view_twitter *)al_array_at(page->views, offset);
- // If this post would draw past the height, return out of space.
- if (y + view->v.rows > height) return true;
- if (func) func(userdata, view, y);
- (*count)++;
- y += view->v.rows + 1;
- }
- } while ((offset = 0, (page = get_page_from_num(search, page->num + 1))));
- return false;
-}
-
-static void draw_tweet_func(void *userdata, struct cmc_view_twitter *view, u32 y)
-{
- struct cmc_tui *tui = (struct cmc_tui *)userdata;
- struct cmc_search *search = tui->search;
- ncplane_reparent_family(view->v.container, tui->p);
- ncplane_move_yx(view->v.container, y, 0);
- draw_tweet_resources(tui, view);
- view->v.y = y;
- al_array_push(search->prev, &view->v);
-}
-
-static void draw_selection_line(struct cmc_view *view, u32 y, u32 rows)
-{
- ncplane_move_yx(view->container, y, 0);
- wchar_t box = L'█';
- u32 k = ((struct cmc_view_twitter *)view)->repost ? 1 : 0;
- for (; k < rows - 1; k++) {
- ncplane_cursor_move_yx(view->container, k, 0);
- ncplane_putwc(view->container, box);
- }
-}
-
-static struct cmc_view *get_view_by_visual_index(struct cmc_search *search, s32 index)
-{
- u32 offset;
- struct cmc_view_page *page = get_page_containing_index(search, search->anchor + index, &offset);
- if (!page || !page->processed) return NULL;
- return al_array_at(page->views, offset);
-}
-
-static void draw_overlay(struct cmc_tui *tui)
-{
- ncplane_cursor_move_yx(tui->overlay.p, 0, 0);
- ncplane_erase(tui->overlay.p);
-
- u64 color = 0;
- u32 background = 0;
- ncchannel_set_rgb8(&background, 255, 255, 255);
- ncchannel_set_alpha(&background, 255);
- ncchannels_set_fchannel(&color, background);
-
- /*
- struct nccell ul, ur; nccell_init(&ul); nccell_init(&ur);
- struct nccell ll, lr; nccell_init(&ll); nccell_init(&lr);
- struct nccell hl, vl; nccell_init(&hl); nccell_init(&vl);
- //nccell_set_channels(&hl, color);
- //nccell_set_channels(&vl, color);
-
- ncplane_box(tui->overlay.p, &ul, &ur, &ll, &lr, &hl, &vl, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
-
- nccell_release(tui->overlay.p, &ul); nccell_release(tui->overlay.p, &ur);
- nccell_release(tui->overlay.p, &ll); nccell_release(tui->overlay.p, &lr);
- nccell_release(tui->overlay.p, &hl); nccell_release(tui->overlay.p, &vl);
- */
-
- //ncplane_double_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
- //ncplane_rounded_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
-
- //ncchannel_set_palindex(&background, 1);
- ncchannel_set_rgb8(&background, 124, 12, 53);
- ncchannels_set_bchannel(&color, background);
- ncchannels_set_fchannel(&color, background);
- ncplane_gradient(tui->overlay.p, 0, 0, tui->overlay.rows, tui->overlay.cols, "", 0, color, color, color, color);
-
- ncplane_set_bg_rgb8(tui->overlay.p, 124, 12, 53);
- //ncplane_set_fg_palindex(tui->overlay.p, 4);
- //ncplane_set_bg_alpha(tui->overlay.p, 0);
- //ncplane_cursor_move_yx(tui->overlay.p, 1, 1);
- //ncplane_cursor_move_yx(tui->overlay.p, 0, 1);
-
- struct cmc_search *search;
- al_array_foreach(tui->searches, i, search) {
- if ((s32)i == tui->overlay.selected) {
- ncplane_set_fg_palindex(tui->overlay.p, 4);
- } else {
- ncplane_set_fg_default(tui->overlay.p);
- }
- str *query = &search->search->query;
- for (u32 j = 0; j < query->len; j++) {
- ncplane_putchar_yx(tui->overlay.p, i, j + 1, al_str_at(query, j));
- }
- }
-}
-
-static void refresh_pane_search(struct cmc *c, struct cmc_tui *tui, struct cmc_search *search)
-{
- if (search->find_anchor == SEARCH_UP) {
- u32 count;
- // If find_anchor = SEARCH_UP, search->anchor is index of the requested view.
- s32 anchor = search->anchor;
- while (anchor >= 0) {
- count = 0;
- iterate_views_to_height(search, tui->rows, anchor, &count, NULL, NULL);
- // Is the requested view still visible?
- if ((s32)(anchor + count) - 1 < search->anchor) break;
- anchor--;
- // Load more posts if needed.
- if (anchor < 0 && request_page_at_page_offset(c, search, 0, -1)) return;
- }
- anchor++; // We want the anchor before the last attempt above.
- search->selected = abs(search->anchor - anchor);
- search->anchor = anchor;
- search->redraw = true;
- search->find_anchor = SEARCH_NONE;
- } else if (search->find_anchor == SEARCH_DOWN) {
- // When moving down we already know how many views are being displayed
- // so just move the anchor 1 past that.
- search->selected = 0;
- search->anchor += search->count;
- search->redraw = true;
- search->find_anchor = SEARCH_NONE;
- }
-
- if (tui->overlay.active) draw_overlay(tui);
-
- struct cmc_view *view;
- bool out_of_space = true;
- if (search->redraw) {
- al_array_foreach(search->prev, j, view) {
- ncplane_erase(view->container);
- ncplane_reparent_family(view->container, view->container);
- }
- search->prev.size = 0;
- search->count = 0;
- out_of_space = iterate_views_to_height(search, tui->rows, search->anchor, &search->count, draw_tweet_func, tui);
- } else {
- al_array_foreach(search->prev, j, view) {
- ncplane_erase(view->container);
- }
- }
- if ((view = get_view_by_visual_index(search, search->selected))) {
- draw_selection_line(view, view->y, view->rows);
- }
- if (search->redraw) {
- search->redraw = false;
- notcurses_refresh(tui->nc, NULL, NULL);
- }
- notcurses_render(tui->nc);
- if (search->pending_requests == 0 && search->count > 0 && !out_of_space && search->end_of_page == -1) {
- request_page_at_page_offset(c, search, (search->anchor + search->count) - 1, 1);
- }
-}
-
-static void maybe_process_pages(struct cmc *c)
-{
- struct cmc_tui *tui = &c->tui;
- bool do_redraw = false;
- struct cmc_search *search;
- al_array_foreach(tui->searches, i, search) {
- struct cmc_view_page *page;
- al_array_foreach(search->pages, j, page) {
- if (page->processed) continue;
- struct cmc_view *view;
- bool any_pending_requests = false;
- al_array_foreach(page->views, k, view) {
- if (view->pending_requests > 0) {
- any_pending_requests = true;
- break;
- }
- }
- if (any_pending_requests) continue;
- do_redraw = true;
- al_array_foreach(page->views, k, view) {
- view->rows = layout_tweet(tui, (struct cmc_view_twitter *)view, tui->cols);
- draw_tweet((struct cmc_view_twitter *)view, view->container, view->rows, tui->cols);
- }
- page->processed = true;
- }
- }
- search = tui->search;
- if (do_redraw) {
- search->redraw = true;
- refresh_pane_search(c, tui, 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, 0);
- if (channels == 3) {
- resource->v = ncvisual_from_rgb_packed(pixels, h, w * channels, w, 255);
- } else if (channels == 4) {
- resource->v = ncvisual_from_rgba(pixels, h, w * channels, w);
- } else {
- al_log_warn("cmc", "Image has unsupported channel count.");
- }
- stbi_image_free(pixels);
- ncvisual_geom(c->tui.nc, resource->v, &resource->vopts, &resource->geom);
- struct cmc_view *view;
- al_array_foreach(resource->refs, j, view) {
- view->pending_requests--;
- }
- al_array_free(resource->refs);
- break;
- }
- }
- maybe_process_pages(c);
-}
-
-static void add_selected_from_index(struct cmc *c, struct cmc_search *search, u32 index)
-{
- struct cmc_view *view = get_view_by_visual_index(search, search->selected);
- if (view && view->post->media.size > index) {
- camu_client_add(&c->client, &view->post->unique_id, index);
- camu_client_skip(&c->client, 1);
- }
-}
-
-#define ON_LAST_PAGE(search) \
- (search->end_of_page >= 0 && search->anchor + search->count > (u32)search->end_of_page)
-
-static bool handle_input_search(struct cmc *c, struct cmc_search *search, struct ncinput *input)
-{
- if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false;
- if (input->id == 'j') {
- if (search->selected + 1 >= (s32)search->count) {
- if (search->pending_requests > 0 || search->find_anchor != SEARCH_NONE) return false;
- if (ON_LAST_PAGE(search)) return false;
- u32 index = search->anchor + search->count;
- search->find_anchor = SEARCH_DOWN;
- struct cmc_view_page *page = get_page_containing_index(search, index, NULL);
- if (!page) {
- request_page_at_page_offset(c, search, index - 1, 1);
- return false;
- }
- } else {
- search->selected++;
- }
- return true;
- } else if (input->id == 'k') {
- if (search->selected - 1 < 0) {
- if (search->pending_requests > 0 || search->find_anchor != SEARCH_NONE) return false;
- if (search->anchor - 1 < 0 && !request_page_at_page_offset(c, search, 0, -1)) {
- // The reason to do this here instead of letting it resolve in
- // refresh_pane_search() is so we can return early if
- // we are at the top of the page.
- return false;
- }
- search->anchor--;
- search->find_anchor = SEARCH_UP;
- // Wait for the requested previous page to come in.
- if (search->anchor < 0) return false;
- } else {
- search->selected--;
- }
- return true;
- } else if (input->id == NCKEY_RETURN || input->id == '1') {
- add_selected_from_index(c, search, 0);
- } else if (input->id == '2') {
- add_selected_from_index(c, search, 1);
- } else if (input->id == '3') {
- add_selected_from_index(c, search, 2);
- } else if (input->id == '4') {
- add_selected_from_index(c, search, 3);
- }
- return false;
-}
-
-static void toggle_overlay_internal(struct cmc_tui *tui)
-{
- if (tui->overlay.active) {
- ncplane_reparent_family(tui->overlay.p, tui->overlay.p);
- } else {
- ncplane_reparent_family(tui->overlay.p, tui->p);
- }
- tui->overlay.active = !tui->overlay.active;
-}
-
-static bool handle_input_overlay(struct cmc *c, struct cmc_tui *tui, struct ncinput *input)
-{
- if (input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN) {
- if (input->id == 'j') {
- if (tui->overlay.selected + 1 < (s32)tui->searches.size) {
- tui->overlay.selected++;
- return true;
- }
- } else if (input->id == 'k') {
- if (tui->overlay.selected - 1 >= 0) {
- tui->overlay.selected--;
- return true;
- }
- } else if (input->id == NCKEY_RETURN) {
- toggle_overlay_internal(tui);
- struct cmc_search *search = tui->search;
- struct cmc_view *view;
- al_array_foreach(search->prev, j, view) {
- ncplane_erase(view->container);
- ncplane_reparent_family(view->container, view->container);
- }
- search->prev.size = 0;
- tui->search = al_array_at(tui->searches, tui->overlay.selected);
- search = tui->search;
- search->redraw = true;
- u32 last_page = search->search->last_page;
- if (!get_page_from_num(search, last_page)) {
- request_page_internal(c, last_page);
- } else {
- return true;
- }
- }
- }
- return false;
-}
-
-static void input_poll_callback(void *userdata, s32 revents)
-{
- struct cmc *c = (struct cmc *)userdata;
- struct cmc_tui *tui = &c->tui;
- (void)revents;
- struct ncinput input;
- u32 ret;
- bool do_refresh = false;
- do {
- ret = notcurses_get_nblock(c->tui.nc, &input);
- if (ret == (u32)-1 || ret == 0) break;
- if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
- if (input.id == 'q') {
- aki_event_loop_break(&c->loop);
- break;
- } else if (input.id == NCKEY_TAB) {
- toggle_overlay_internal(tui);
- do_refresh = true;
- tui->search->redraw = true;
- continue;
- }
- if (tui->overlay.active) do_refresh |= handle_input_overlay(c, tui, &input);
- else if (tui->search) do_refresh |= handle_input_search(c, tui->search, &input);
- }
- } while (1);
- if (do_refresh) refresh_pane_search(c, tui, tui->search);
-}
-
-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.scaling = NCSCALE_SCALE;
- resource->id = al_rand_u16();
- al_str_clone(&resource->unique_id, unique_id);
- resource->index = index;
- al_array_init(resource->refs);
- 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, struct cmc_view *view,
- 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;
- }
- }
- resource = create_resource(c, unique_id, index);
- view->pending_requests++;
- al_array_push(resource->refs, view);
- return resource;
-}
-
-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 void strip_link_from_tweet(struct sho_post *post)
-{
- wstr *check = al_wstr_c(L"https://t.co/");
- s32 index = al_wstr_rfind(&post->text, L':');
- if (index < 5) return;
- index -= 5; // [h]ttps:// (if matched).
- if (al_wstr_cmp(&post->text, check, index, check->len) == 0) {
- al_wstr_at(&post->text, index) = L'\0';
- post->text.len = index;
- }
-}
-
-static struct cmc_view_twitter *create_view_twitter(struct cmc *c, str *unique_id)
-{
- struct cmc_view_twitter *view = al_alloc_object(struct cmc_view_twitter);
- get_post_and_maybe_repost(c, unique_id, &view->v.post, &view->repost);
-
- if (view->v.post->media.size > 0) {
- strip_link_from_tweet(view->v.post);
- }
-
- view->v.rows = 0;
- view->v.y = 0;
- view->v.pending_requests = 0;
-
- 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->tui.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);
-
- struct sho_post *post = view->v.post;
- view->profile.resource = get_resource_from_cache(c, &view->v, &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, &view->v, &post->unique_id, i);
- }
-
- switch (post->media.size) {
- case 0:
- view->preview.mode = CMC_VIEW_NONE;
- break;
- case 1:
- view->preview.mode = CMC_VIEW_SINGLE;
- break;
- default:
- view->preview.mode = CMC_VIEW_PARTITION;
- break;
- }
-
- return view;
-}
-
-static struct cmc_search *create_search(struct camu_search *s)
-{
- struct cmc_search *search = al_alloc_object(struct cmc_search);
- search->search = s;
- search->find_anchor = SEARCH_NONE;
- search->anchor = 0;
- search->redraw = true;
- search->selected = 0;
- al_array_init(search->prev);
- search->max_page_num = s->last_page;
- search->min_page_num = s->last_page;
- search->end_of_page = -1;
- search->pending_requests = 0;
- al_array_init(search->pages);
- return search;
-}
-
-static s32 resize_cb(struct ncplane *p)
-{
- struct cmc_tui *tui = (struct cmc_tui *)ncplane_userptr(p);
- u32 width, height;
- notcurses_stddim_yx(tui->nc, &height, &width);
- struct ncplane_options nopts = { 0 };
- tui->cols = nopts.cols = width - 4;
- tui->rows = nopts.rows = height - 1;
- nopts.x = 3;
- nopts.y = 1;
- nopts.flags = 0;
- tui->p = ncplane_create(p, &nopts);
- tui->overlay.cols = nopts.cols = 35;
- tui->overlay.rows = nopts.rows = 20;
- nopts.x = (tui->cols / 2) - (tui->overlay.cols / 2);
- nopts.y = (tui->rows / 2) - (tui->overlay.rows / 2);
- tui->overlay.p = ncpile_create(tui->nc, &nopts);
- //ncplane_reparent_family(tui->overlay.p, tui->p);
- return 0;
-}
-
-static struct cmc_search *get_search_from_id(struct cmc_tui *tui, s32 id)
-{
- struct cmc_search *search;
- al_array_foreach(tui->searches, i, search) {
- if (search->search->id == id) return search;
- }
- return NULL;
-}
+#include "cmc.h"
static void client_callback(void *userdata, u8 op, void *opaque)
{
struct cmc *c = (struct cmc *)userdata;
- struct cmc_tui *tui = &c->tui;
switch (op) {
case CAMU_CLIENT_STATE_UPDATED: {
switch (c->action) {
case CMC_ACTION_OPEN: {
- if (!(tui->nc = notcurses_init(NULL, stdin))) return;
- aki_poll_init(&tui->input, input_poll_callback, c);
- aki_poll_set(&tui->input, notcurses_inputready_fd(tui->nc), AKI_POLL_READ);
- aki_poll_start(&tui->input, &c->loop);
- al_array_init(tui->searches);
- struct camu_search *s;
- al_array_foreach(c->client.state.searches, i, s) {
- al_array_push(tui->searches, create_search(s));
- }
- if (tui->searches.size > tui->req_index) {
- tui->search = al_array_at(tui->searches, tui->req_index);
- u32 last_page = tui->search->search->last_page;
- request_page_internal(c, last_page);
- } else {
- tui->search = NULL;
- }
- tui->overlay.active = false;
- tui->overlay.selected = 0;
- struct ncplane *stdplane = notcurses_stdplane(tui->nc);
- ncplane_set_resizecb(stdplane, resize_cb);
- ncplane_set_userptr(stdplane, tui);
- resize_cb(stdplane);
+ cmc_ui_open(&c->ui, c);
break;
}
case CMC_ACTION_LIST: {
@@ -1105,41 +56,11 @@ static void client_callback(void *userdata, u8 op, void *opaque)
break;
}
case CAMU_CLIENT_RESULTS: {
- struct camu_result_list *results = (struct camu_result_list *)opaque;
- struct cmc_search *search = get_search_from_id(tui, results->search->id);
- if (!search) return;
- search->pending_requests--;
-
- struct cmc_view_page *page = al_alloc_object(struct cmc_view_page);
- al_array_push(search->pages, page);
- page->num = results->num;
- page->processed = false;
- if (page->num > search->max_page_num) search->max_page_num = page->num;
- if (page->num < search->min_page_num) search->min_page_num = page->num;
- if (search->find_anchor == SEARCH_UP) {
- search->anchor += results->unique_ids.size;
- if (search->end_of_page >= 0) {
- search->end_of_page += results->unique_ids.size;
- }
- }
-
- str *unique_id;
- struct cmc_view_twitter *view;
- al_array_foreach_ptr(results->unique_ids, i, unique_id) {
- view = create_view_twitter(c, unique_id);
- al_array_push(page->views, &view->v);
- }
-
- maybe_process_pages(c);
-
+ cmc_ui_handle_results(&c->ui.search, (struct camu_result_list *)opaque);
break;
}
case CAMU_CLIENT_END_OF_RESULTS: {
- struct cmc_search *search = get_search_from_id(tui, *((s32 *)opaque));
- if (!search) return;
- search->pending_requests--;
- search->end_of_page = (search->anchor + search->count) - 1;
- maybe_process_pages(c);
+ cmc_ui_handle_end_of_results(&c->ui.search, *((s32 *)opaque));
break;
}
case CAMU_CLIENT_DISCONNECTED:
@@ -1169,11 +90,6 @@ s32 main(s32 argc, char *argv[])
}
} else if (al_str_eq(cmd, al_str_c("open"))) {
c.action = CMC_ACTION_OPEN;
- if (argc > 2) {
- c.tui.req_index = al_str_to_long(al_str_c(argv[2]), 10);
- } else {
- c.tui.req_index = 0;
- }
} else if (al_str_eq(cmd, al_str_c("list"))) {
c.type = CMC_LIST;
if (argc == 2) {
@@ -1196,15 +112,11 @@ s32 main(s32 argc, char *argv[])
if (c.type == CMC_SEARCH) {
camu_client_create_search(&c.client, &module, &query);
}
- } else if (c.action == CMC_ACTION_OPEN) {
- c.pending_requests = 0;
- camu_resource_client_init(&c.resource_client, &c.loop, resource_client_callback, &c);
- camu_resource_client_connect(&c.resource_client, TREE_SERVER_IP, TREE_RESOURCE_PORT);
}
aki_event_loop_run(&c.loop);
- if (c.action == CMC_ACTION_OPEN && c.tui.nc) notcurses_stop(c.tui.nc);
+ if (c.action == CMC_ACTION_OPEN && c.ui.nc) notcurses_stop(c.ui.nc);
camu_client_close(&c.client);
diff --git a/src/fruits/cmc/cmc.h b/src/fruits/cmc/cmc.h
new file mode 100644
index 0000000..625d256
--- /dev/null
+++ b/src/fruits/cmc/cmc.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <al/types.h>
+#include <aki/event_loop.h>
+
+#include "../../libclient/client.h"
+
+#include "tui/ui.h"
+
+enum {
+ CMC_LIST = 0,
+ CMC_SEARCH
+};
+
+enum {
+ CMC_ACTION_OPEN = 0,
+ CMC_ACTION_LIST,
+ CMC_ACTION_CREATE,
+};
+
+struct cmc {
+ u8 type;
+ u8 action;
+ struct aki_event_loop loop;
+ struct camu_client client;
+ struct cmc_ui ui;
+};
diff --git a/src/fruits/cmc/meson.build b/src/fruits/cmc/meson.build
index 45ac9c2..6dd3960 100644
--- a/src/fruits/cmc/meson.build
+++ b/src/fruits/cmc/meson.build
@@ -1,5 +1,12 @@
-cmc_src = ['cmc.c']
-cmc_deps = [common_deps, libclient, cache, stb_image]
+cmc_src = [
+ 'cmc.c',
+ 'tui/ui.c',
+ 'tui/resources.c',
+ 'tui/pager.c',
+ 'tui/view_twitter.c',
+ 'tui/pane_search.c'
+]
+cmc_deps = [common_deps, libclient, subproject('stb').get_variable('stb')]
notcurses = dependency('notcurses')
cmc_deps += [notcurses]
diff --git a/src/fruits/cmc/tui/pager.c b/src/fruits/cmc/tui/pager.c
new file mode 100644
index 0000000..00c5665
--- /dev/null
+++ b/src/fruits/cmc/tui/pager.c
@@ -0,0 +1,88 @@
+#include "pager.h"
+
+void cmc_pager_init(struct cmc_pager *pager, u32 page)
+{
+ pager->find_anchor = PAGE_NONE;
+ pager->anchor = 0;
+ pager->redraw = true;
+ pager->selected = 0;
+ al_array_init(pager->prev);
+ pager->max_page_num = page;
+ pager->min_page_num = page;
+ pager->end_of_page = -1;
+ pager->pending_requests = 0;
+ al_array_init(pager->pages);
+}
+
+static bool any_pending_requests(struct cmc_view_page *page)
+{
+ struct cmc_view *view;
+ al_array_foreach(page->views, j, view) {
+ if (view->pending_requests > 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool cmc_pager_maybe_process_pages(struct cmc_pager *pager, void (*func)(void *, struct cmc_view_page *), void *userdata)
+{
+ bool any_page_processed = false;
+ struct cmc_view_page *page;
+ al_array_foreach(pager->pages, i, page) {
+ if (page->processed) continue;
+ if (!any_pending_requests(page)) {
+ page->processed = true;
+ func(userdata, page);
+ any_page_processed = true;
+ }
+ }
+ return any_page_processed;
+}
+
+struct cmc_view_page *cmc_get_page_from_num(struct cmc_pager *pager, u32 num)
+{
+ struct cmc_view_page *page;
+ al_array_foreach(pager->pages, i, page) {
+ if (page->num == num) return page;
+ }
+ return NULL;
+}
+
+struct cmc_view_page *cmc_pager_page_containing_index(struct cmc_pager *pager, u32 index, u32 *offset)
+{
+ u32 count = 0, size = 0;
+ struct cmc_view_page *page = NULL;
+ for (u32 i = pager->min_page_num; i <= pager->max_page_num; i++) {
+ if (!(page = cmc_get_page_from_num(pager, i))) continue;
+ size = page->views.size;
+ count += size;
+ if (count > index) {
+ if (offset) *offset = size - (count - index);
+ return page;
+ }
+ }
+ return NULL;
+}
+
+bool cmc_pager_iterate_to_height(struct cmc_pager *pager, u32 height, u32 anchor, u32 *count,
+ void (*func)(void *, struct cmc_view *, u32), void *userdata)
+{
+ u32 y = 0, offset;
+ struct cmc_view_page *page = cmc_pager_page_containing_index(pager, anchor, &offset);
+ al_assert(page);
+ struct cmc_view *view;
+ do {
+ // If the page is not processed, return out of space as to not display a discontinuity.
+ if (!page->processed) return true;
+ for (; offset < page->views.size; offset++) {
+ view = al_array_at(page->views, offset);
+ // If this post would draw past the height, return out of space.
+ if (y + view->rows > height) return true;
+ if (func) func(userdata, view, y);
+ (*count)++;
+ y += view->rows + 1;
+ }
+ } while ((offset = 0, (page = cmc_get_page_from_num(pager, page->num + 1))));
+ return false;
+}
diff --git a/src/fruits/cmc/tui/pager.h b/src/fruits/cmc/tui/pager.h
new file mode 100644
index 0000000..6aeeb9c
--- /dev/null
+++ b/src/fruits/cmc/tui/pager.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <al/types.h>
+#include <al/array.h>
+
+#include "view.h"
+
+enum {
+ PAGE_NONE = 0,
+ PAGE_UP,
+ PAGE_DOWN
+};
+
+struct cmc_pager {
+ u8 find_anchor;
+ s32 anchor;
+ bool redraw;
+ u32 count;
+ s32 selected;
+ array(struct cmc_view *) prev;
+ u32 max_page_num;
+ u32 min_page_num;
+ s32 end_of_page;
+ u32 pending_requests;
+ array(struct cmc_view_page *) pages;
+};
+
+void cmc_pager_init(struct cmc_pager *pager, u32 page);
+bool cmc_pager_maybe_process_pages(struct cmc_pager *pager, void (*func)(void *, struct cmc_view_page *), void *userdata);
+struct cmc_view_page *cmc_get_page_from_num(struct cmc_pager *pager, u32 num);
+struct cmc_view_page *cmc_pager_page_containing_index(struct cmc_pager *pager, u32 index, u32 *offset);
+bool cmc_pager_iterate_to_height(struct cmc_pager *pager, u32 height, u32 anchor, u32 *count,
+ void (*func)(void *, struct cmc_view *, u32), void *userdata);
diff --git a/src/fruits/cmc/tui/pane_search.c b/src/fruits/cmc/tui/pane_search.c
new file mode 100644
index 0000000..4c14939
--- /dev/null
+++ b/src/fruits/cmc/tui/pane_search.c
@@ -0,0 +1,330 @@
+#include <al/lib.h>
+#include <notcurses/nckeys.h>
+
+#include "../cmc.h"
+
+#include "pane_search.h"
+#include "view_twitter.h"
+#include "ui.h"
+
+static void request_page_internal(struct cmc *c, struct cmc_search *search, u32 num)
+{
+ search->pager.pending_requests++;
+ camu_client_get_page(&c->client, search->search, num);
+}
+
+void cmc_pane_search_init(struct cmc_pane_search *pane, struct cmc_ui *ui)
+{
+ pane->ui = ui;
+ al_array_init(pane->searches);
+ pane->search = NULL;
+ struct camu_search *s;
+ al_array_foreach(pane->ui->c->client.state.searches, i, s) {
+ struct cmc_search *search = al_alloc_object(struct cmc_search);
+ search->search = s;
+ cmc_pager_init(&search->pager, s->page);
+ al_array_push(pane->searches, search);
+ }
+ if (pane->searches.size > 0) {
+ pane->search = al_array_at(pane->searches, 0);
+ request_page_internal(pane->ui->c, pane->search, pane->search->search->page);
+ }
+}
+
+static struct cmc_view *get_view_by_visual_index(struct cmc_pager *pager, s32 index)
+{
+ u32 offset;
+ struct cmc_view_page *page = cmc_pager_page_containing_index(pager, pager->anchor + index, &offset);
+ if (!page || !page->processed) return NULL;
+ return al_array_at(page->views, offset);
+}
+
+// Given an index of a post, request the page to the offset of the page containing that post.
+static bool request_page_at_page_offset(struct cmc_pane_search *pane, struct cmc_pager *pager, u32 index, s32 offset)
+{
+ struct cmc_view_page *page = cmc_pager_page_containing_index(pager, index, NULL);
+ if (page) {
+ s32 req = (s32)page->num + offset;
+ if (req < 0) return false;
+ request_page_internal(pane->ui->c, pane->search, req);
+ }
+ return true;
+}
+
+static void add_selected_from_index(struct cmc *c, struct cmc_pager *pager, u32 index)
+{
+ struct cmc_view *view = get_view_by_visual_index(pager, pager->selected);
+ if (view && view->post->media.size > index) {
+ camu_client_add(&c->client, &view->post->unique_id, index);
+ camu_client_skip(&c->client, 1);
+ }
+}
+
+#define ON_LAST_PAGE(pager) \
+ (pager->end_of_page >= 0 && pager->anchor + pager->count > (u32)pager->end_of_page)
+
+bool cmc_pane_search_handle_input(struct cmc_pane_search *pane, struct ncinput *input)
+{
+ struct cmc_pager *pager = &pane->search->pager;
+ if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false;
+ if (input->id == 'j') {
+ if (pager->selected + 1 >= (s32)pager->count) {
+ if (pager->pending_requests > 0 || pager->find_anchor != PAGE_NONE) return false;
+ if (ON_LAST_PAGE(pager)) return false;
+ u32 index = pager->anchor + pager->count;
+ pager->find_anchor = PAGE_DOWN;
+ struct cmc_view_page *page = cmc_pager_page_containing_index(pager, index, NULL);
+ if (!page) {
+ request_page_at_page_offset(pane, pager, index - 1, 1);
+ return false;
+ }
+ } else {
+ pager->selected++;
+ }
+ return true;
+ } else if (input->id == 'k') {
+ if (pager->selected - 1 < 0) {
+ if (pager->pending_requests > 0 || pager->find_anchor != PAGE_NONE) return false;
+ if (pager->anchor - 1 < 0 && !request_page_at_page_offset(pane, pager, 0, -1)) {
+ // The reason to do this here instead of letting it resolve in
+ // refresh_pane_pager() is so we can return early if
+ // we are at the top of the page.
+ return false;
+ }
+ pager->anchor--;
+ pager->find_anchor = PAGE_UP;
+ // Wait for the requested previous page to come in.
+ if (pager->anchor < 0) return false;
+ } else {
+ pager->selected--;
+ }
+ return true;
+ } else if (input->id == NCKEY_RETURN || input->id == '1') {
+ add_selected_from_index(pane->ui->c, pager, 0);
+ } else if (input->id == '2') {
+ add_selected_from_index(pane->ui->c, pager, 1);
+ } else if (input->id == '3') {
+ add_selected_from_index(pane->ui->c, pager, 2);
+ } else if (input->id == '4') {
+ add_selected_from_index(pane->ui->c, pager, 3);
+ }
+ return false;
+}
+
+bool cmc_pane_search_handle_input_overlay(struct cmc_pane_search *pane, struct ncinput *input)
+{
+ if (!(input->evtype == NCTYPE_PRESS || input->evtype == NCTYPE_UNKNOWN)) return false;
+ struct cmc_ui *ui = pane->ui;
+ struct cmc_pager *pager = &pane->search->pager;
+ if (input->id == 'j') {
+ if (ui->overlay.selected + 1 < (s32)pane->searches.size) {
+ ui->overlay.selected++;
+ return true;
+ }
+ } else if (input->id == 'k') {
+ if (ui->overlay.selected - 1 >= 0) {
+ ui->overlay.selected--;
+ return true;
+ }
+ } else if (input->id == NCKEY_RETURN) {
+ cmc_ui_toggle_overlay(ui);
+ struct cmc_view *view;
+ al_array_foreach(pager->prev, j, view) {
+ ncplane_erase(view->container);
+ ncplane_reparent_family(view->container, view->container);
+ }
+ pager->prev.size = 0;
+ pane->search = al_array_at(pane->searches, ui->overlay.selected);
+ pager = &pane->search->pager;
+ pager->redraw = true;
+ u32 page = pane->search->search->page;
+ if (!cmc_get_page_from_num(pager, page)) {
+ request_page_internal(ui->c, pane->search, page);
+ } else {
+ return true;
+ }
+ }
+ return false;
+}
+
+void cmc_pane_search_maybe_draw_overlay(struct cmc_pane_search *pane)
+{
+ struct cmc_ui *ui = pane->ui;
+
+ if (!ui->overlay.active) return;
+
+ ncplane_cursor_move_yx(ui->overlay.p, 0, 0);
+ ncplane_erase(ui->overlay.p);
+
+ u64 color = 0;
+ u32 background = 0;
+ ncchannel_set_rgb8(&background, 255, 255, 255);
+ ncchannel_set_alpha(&background, 255);
+ ncchannels_set_fchannel(&color, background);
+
+ ncchannel_set_rgb8(&background, 124, 12, 53);
+ ncchannels_set_bchannel(&color, background);
+ ncchannels_set_fchannel(&color, background);
+ ncplane_gradient(ui->overlay.p, 0, 0, ui->overlay.rows, ui->overlay.cols, "", 0, color, color, color, color);
+
+ ncplane_set_bg_rgb8(ui->overlay.p, 124, 12, 53);
+
+ struct cmc_search *search;
+ al_array_foreach(ui->search.searches, i, search) {
+ if ((s32)i == ui->overlay.selected) {
+ ncplane_set_fg_palindex(ui->overlay.p, 4);
+ } else {
+ ncplane_set_fg_default(ui->overlay.p);
+ }
+ str *query = &search->search->query;
+ for (u32 j = 0; j < query->len; j++) {
+ ncplane_putchar_yx(ui->overlay.p, i, j + 1, al_str_at(query, j));
+ }
+ }
+}
+
+static void draw_selection_line(struct cmc_view *view, u32 y, u32 rows)
+{
+ ncplane_move_yx(view->container, y, 0);
+ wchar_t box = L'█';
+ u32 k = ((struct cmc_view_twitter *)view)->repost ? 1 : 0;
+ for (; k < rows - 1; k++) {
+ ncplane_cursor_move_yx(view->container, k, 0);
+ ncplane_putwc(view->container, box);
+ }
+}
+
+void cmc_pane_search_refresh(struct cmc_pane_search *pane)
+{
+ struct cmc_pager *pager = &pane->search->pager;
+ if (pager->find_anchor == PAGE_UP) {
+ u32 count;
+ // If find_anchor = PAGE_UP, search->anchor is index of the requested view.
+ s32 anchor = pager->anchor;
+ while (anchor >= 0) {
+ count = 0;
+ cmc_pager_iterate_to_height(pager, pane->ui->rows, anchor, &count, NULL, NULL);
+ // Is the requested view still visible?
+ if ((s32)(anchor + count) - 1 < pager->anchor) break;
+ anchor--;
+ // Load more posts if needed.
+ if (anchor < 0 && request_page_at_page_offset(pane, pager, 0, -1)) return;
+ }
+ anchor++; // We want the anchor before the last attempt above.
+ pager->selected = abs(pager->anchor - anchor);
+ pager->anchor = anchor;
+ pager->redraw = true;
+ pager->find_anchor = PAGE_NONE;
+ } else if (pager->find_anchor == PAGE_DOWN) {
+ // When moving down we already know how many views are being displayed
+ // so just move the anchor 1 past that.
+ pager->selected = 0;
+ pager->anchor += pager->count;
+ pager->redraw = true;
+ pager->find_anchor = PAGE_NONE;
+ }
+
+ cmc_pane_search_maybe_draw_overlay(pane);
+
+ struct cmc_view *view;
+ bool out_of_space = true;
+ if (pager->redraw) {
+ al_array_foreach(pager->prev, j, view) {
+ ncplane_erase(view->container);
+ ncplane_reparent_family(view->container, view->container);
+ }
+ pager->prev.size = 0;
+ pager->count = 0;
+ out_of_space = cmc_pager_iterate_to_height(pager, pane->ui->rows, pager->anchor, &pager->count, cmc_draw_tweet_func, pane->ui);
+ } else {
+ al_array_foreach(pager->prev, j, view) {
+ ncplane_erase(view->container);
+ }
+ }
+ if ((view = get_view_by_visual_index(pager, pager->selected))) {
+ draw_selection_line(view, view->y, view->rows);
+ }
+ if (pager->redraw) {
+ pager->redraw = false;
+ notcurses_refresh(pane->ui->nc, NULL, NULL);
+ }
+ notcurses_render(pane->ui->nc);
+ if (pager->pending_requests == 0 && pager->count > 0 && !out_of_space && pager->end_of_page == -1) {
+ request_page_at_page_offset(pane, pager, (pager->anchor + pager->count) - 1, 1);
+ }
+}
+
+static struct cmc_search *get_search_from_id(struct cmc_pane_search *pane, s32 id)
+{
+ struct cmc_search *search;
+ al_array_foreach(pane->searches, i, search) {
+ if (search->search->id == id) return search;
+ }
+ return NULL;
+}
+
+static void process_page_func(void *userdata, struct cmc_view_page *page)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)userdata;
+ struct cmc_view *view;
+ al_array_foreach(page->views, i, view) {
+ view->rows = cmc_layout_tweet(ui, (struct cmc_view_twitter *)view, ui->cols);
+ cmc_draw_tweet((struct cmc_view_twitter *)view, view->container, view->rows, ui->cols);
+ }
+}
+
+void cmc_ui_resources_callback(void *userdata)
+{
+ struct cmc_pane_search *pane = (struct cmc_pane_search *)userdata;
+ struct cmc_pager *pager = &pane->search->pager;
+ if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) {
+ pager->redraw = true;
+ cmc_pane_search_refresh(pane);
+ }
+}
+
+void cmc_ui_handle_results(struct cmc_pane_search *pane, struct camu_result_list *results)
+{
+ struct cmc_search *search = get_search_from_id(pane, results->search->id);
+ if (!search) return;
+ struct cmc_pager *pager = &search->pager;
+ pager->pending_requests--;
+
+ struct cmc_view_page *page = al_alloc_object(struct cmc_view_page);
+ al_array_push(pager->pages, page);
+ page->num = results->num;
+ page->processed = false;
+ if (page->num > pager->max_page_num) pager->max_page_num = page->num;
+ if (page->num < pager->min_page_num) pager->min_page_num = page->num;
+ if (pager->find_anchor == PAGE_UP) {
+ pager->anchor += results->unique_ids.size;
+ if (pager->end_of_page >= 0) {
+ pager->end_of_page += results->unique_ids.size;
+ }
+ }
+
+ str *unique_id;
+ struct cmc_view_twitter *view;
+ al_array_foreach_ptr(results->unique_ids, i, unique_id) {
+ view = cmc_create_view_twitter(pane->ui, unique_id);
+ al_array_push(page->views, &view->v);
+ }
+
+ if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) {
+ pager->redraw = true;
+ cmc_pane_search_refresh(pane);
+ }
+}
+
+void cmc_ui_handle_end_of_results(struct cmc_pane_search *pane, s32 id)
+{
+ struct cmc_search *search = get_search_from_id(pane, id);
+ if (!search) return;
+ struct cmc_pager *pager = &search->pager;
+ pager->pending_requests--;
+ pager->end_of_page = (pager->anchor + pager->count) - 1;
+ if (cmc_pager_maybe_process_pages(pager, process_page_func, pane->ui)) {
+ pager->redraw = true;
+ cmc_pane_search_refresh(pane);
+ }
+}
diff --git a/src/fruits/cmc/tui/pane_search.h b/src/fruits/cmc/tui/pane_search.h
new file mode 100644
index 0000000..b7c5e2c
--- /dev/null
+++ b/src/fruits/cmc/tui/pane_search.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <notcurses/notcurses.h>
+
+#include "../../libclient/client.h"
+
+#include "pager.h"
+
+struct cmc_search {
+ struct camu_search *search;
+ struct cmc_pager pager;
+};
+
+struct cmc_ui;
+struct cmc_pane_search {
+ array(struct cmc_search *) searches;
+ struct cmc_search *search;
+ struct cmc_ui *ui;
+};
+
+void cmc_pane_search_init(struct cmc_pane_search *pane, struct cmc_ui *ui);
+bool cmc_pane_search_handle_input(struct cmc_pane_search *pane, struct ncinput *input);
+bool cmc_pane_search_handle_input_overlay(struct cmc_pane_search *pane, struct ncinput *input);
+void cmc_pane_search_draw_overlay(struct cmc_pane_search *pane);
+void cmc_pane_search_refresh(struct cmc_pane_search *pane);
+
+void cmc_ui_resources_callback(void *userdata);
+void cmc_ui_handle_results(struct cmc_pane_search *pane, struct camu_result_list *results);
+void cmc_ui_handle_end_of_results(struct cmc_pane_search *pane, s32 id);
diff --git a/src/fruits/cmc/tui/resources.c b/src/fruits/cmc/tui/resources.c
new file mode 100644
index 0000000..19513bf
--- /dev/null
+++ b/src/fruits/cmc/tui/resources.c
@@ -0,0 +1,85 @@
+#include <al/log.h>
+#include <al/random.h>
+#define STB_IMAGE_IMPLEMENTATION
+#define STB_IMAGE_STATIC
+#define STBI_NO_STDIO
+#define STBI_NO_FAILURE_STRINGS
+#define STBI_MALLOC al_malloc
+#define STBI_FREE al_free
+#define STBI_REALLOC al_realloc
+#include <stb_image.h>
+
+#include "resources.h"
+#include "view.h"
+
+static void resource_client_callback(void *userdata, u16 id, struct aki_buffer *buffer)
+{
+ struct cmc_resources *resources = (struct cmc_resources *)userdata;
+ struct cmc_view_resource *resource;
+ al_array_foreach(resources->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, 0);
+ if (channels == 3) {
+ resource->v = ncvisual_from_rgb_packed(pixels, h, w * channels, w, 255);
+ } else if (channels == 4) {
+ resource->v = ncvisual_from_rgba(pixels, h, w * channels, w);
+ } else {
+ al_log_warn("cmc", "Image has unsupported channel count.");
+ }
+ stbi_image_free(pixels);
+ ncvisual_geom(resources->nc, resource->v, &resource->vopts, &resource->geom);
+ struct cmc_view *view;
+ al_array_foreach(resource->refs, j, view) {
+ view->pending_requests--;
+ }
+ al_array_free(resource->refs);
+ break;
+ }
+ }
+ resources->callback(resources->userdata);
+}
+
+bool cmc_resources_connect(struct cmc_resources *resources, struct notcurses *nc,
+ struct aki_event_loop *loop, str *addr, s32 port)
+{
+ resources->nc = nc;
+ resources->pending_requests = 0;
+ camu_resource_client_init(&resources->client, loop, resource_client_callback, resources);
+ return camu_resource_client_connect(&resources->client, addr, port);
+}
+
+void cmc_resources_set_callback(struct cmc_resources *resources, void (*callback)(void *), void *userdata)
+{
+ resources->callback = callback;
+ resources->userdata = userdata;
+}
+
+static struct cmc_view_resource *create_resource(struct cmc_resources *resources, str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource = al_alloc_object(struct cmc_view_resource);
+ al_array_push(resources->cache, resource);
+ resource->vopts.blitter = NCBLIT_PIXEL;
+ resource->vopts.scaling = NCSCALE_SCALE;
+ resource->id = al_rand_u16();
+ al_str_clone(&resource->unique_id, unique_id);
+ resource->index = index;
+ al_array_init(resource->refs);
+ camu_resource_request(&resources->client, unique_id, index, resource->id);
+ return resource;
+}
+
+struct cmc_view_resource *cmc_get_resource_from_cache(struct cmc_resources *resources, struct cmc_view *view,
+ str *unique_id, u32 index)
+{
+ struct cmc_view_resource *resource;
+ al_array_foreach(resources->cache, i, resource) {
+ if (al_str_eq(&resource->unique_id, unique_id) && resource->index == index) {
+ return resource;
+ }
+ }
+ resource = create_resource(resources, unique_id, index);
+ view->pending_requests++;
+ al_array_push(resource->refs, view);
+ return resource;
+}
diff --git a/src/fruits/cmc/tui/resources.h b/src/fruits/cmc/tui/resources.h
new file mode 100644
index 0000000..f12d861
--- /dev/null
+++ b/src/fruits/cmc/tui/resources.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <notcurses/notcurses.h>
+
+#include "../../libclient/resource_client.h"
+
+struct cmc_view;
+struct cmc_view_resource {
+ u16 id;
+ str unique_id;
+ u32 index;
+ array(struct cmc_view *) refs;
+ struct ncvisual *v;
+ struct ncvgeom geom;
+ struct ncvisual_options vopts;
+};
+
+struct cmc_resources {
+ struct camu_resource_client client;
+ u32 pending_requests;
+ struct notcurses *nc;
+ array(struct cmc_view_resource *) cache;
+ void (*callback)(void *);
+ void *userdata;
+};
+
+bool cmc_resources_connect(struct cmc_resources *resources, struct notcurses *nc,
+ struct aki_event_loop *loop, str *addr, s32 port);
+void cmc_resources_set_callback(struct cmc_resources *resources, void (*callback)(void *), void *userdata);
+struct cmc_view_resource *cmc_get_resource_from_cache(struct cmc_resources *resources, struct cmc_view *view,
+ str *unique_id, u32 index);
diff --git a/src/fruits/cmc/tui/ui.c b/src/fruits/cmc/tui/ui.c
new file mode 100644
index 0000000..dac84a7
--- /dev/null
+++ b/src/fruits/cmc/tui/ui.c
@@ -0,0 +1,133 @@
+#include "../../tree/common.h"
+
+#include "../cmc.h"
+
+#include "ui.h"
+
+/*
+struct nccell ul, ur; nccell_init(&ul); nccell_init(&ur);
+struct nccell ll, lr; nccell_init(&ll); nccell_init(&lr);
+struct nccell hl, vl; nccell_init(&hl); nccell_init(&vl);
+//nccell_set_channels(&hl, color);
+//nccell_set_channels(&vl, color);
+
+ncplane_box(tui->overlay.p, &ul, &ur, &ll, &lr, &hl, &vl, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
+
+nccell_release(tui->overlay.p, &ul); nccell_release(tui->overlay.p, &ur);
+nccell_release(tui->overlay.p, &ll); nccell_release(tui->overlay.p, &lr);
+nccell_release(tui->overlay.p, &hl); nccell_release(tui->overlay.p, &vl);
+*/
+
+//ncplane_double_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
+//ncplane_rounded_box(tui->overlay.p, NCSTYLE_NONE, color, tui->overlay.rows - 1, tui->overlay.cols - 1, 0);
+
+void cmc_ui_toggle_overlay(struct cmc_ui *ui)
+{
+ if (ui->overlay.active) {
+ ncplane_reparent_family(ui->overlay.p, ui->overlay.p);
+ } else {
+ ncplane_reparent_family(ui->overlay.p, ui->p);
+ }
+ ui->overlay.active = !ui->overlay.active;
+}
+
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)userdata;
+ (void)revents;
+ struct ncinput input;
+ u32 ret;
+ bool do_refresh = false;
+ do {
+ ret = notcurses_get_nblock(ui->nc, &input);
+ if (ret == (u32)-1 || ret == 0) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ if (input.id == 'q') {
+ aki_event_loop_break(&ui->c->loop);
+ break;
+ } else if (input.id == NCKEY_TAB) {
+ cmc_ui_toggle_overlay(ui);
+ do_refresh = true;
+ ui->search.search->pager.redraw = true;
+ continue;
+ }
+ switch (ui->pane) {
+ case CMC_PANE_LIBRARY:
+ break;
+ case CMC_PANE_LISTS:
+ break;
+ case CMC_PANE_BROWSE:
+ break;
+ case CMC_PANE_SEARCH:
+ if (ui->overlay.active) {
+ do_refresh |= cmc_pane_search_handle_input_overlay(&ui->search, &input);
+ } else {
+ do_refresh |= cmc_pane_search_handle_input(&ui->search, &input);
+ }
+ break;
+ }
+ }
+ } while (1);
+ if (!do_refresh) return;
+ switch (ui->pane) {
+ case CMC_PANE_LIBRARY:
+ break;
+ case CMC_PANE_LISTS:
+ break;
+ case CMC_PANE_BROWSE:
+ break;
+ case CMC_PANE_SEARCH:
+ cmc_pane_search_refresh(&ui->search);
+ break;
+ }
+}
+
+static s32 resize_cb(struct ncplane *p)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)ncplane_userptr(p);
+ notcurses_stddim_yx(ui->nc, &ui->rows, &ui->cols);
+ ui->cols -= 4;
+ ui->rows -= 1;
+ struct ncplane_options nopts = { 0 };
+ nopts.cols = ui->cols;
+ nopts.rows = ui->rows;
+ nopts.x = 3;
+ nopts.y = 1;
+ nopts.flags = 0;
+ ui->p = ncplane_create(p, &nopts);
+ ui->pane = CMC_PANE_SEARCH;
+ ui->overlay.cols = nopts.cols = 35;
+ ui->overlay.rows = nopts.rows = 20;
+ nopts.x = (ui->cols / 2) - (ui->overlay.cols / 2);
+ nopts.y = (ui->rows / 2) - (ui->overlay.rows / 2);
+ ui->overlay.p = ncpile_create(ui->nc, &nopts);
+ return 0;
+}
+
+bool cmc_ui_open(struct cmc_ui *ui, struct cmc *c)
+{
+ if (!(ui->nc = notcurses_init(NULL, stdin))) return false;
+
+ ui->c = c;
+
+ cmc_pane_search_init(&ui->search, ui);
+
+ if (!cmc_resources_connect(&ui->resources, ui->nc, &ui->c->loop, TREE_SERVER_IP, TREE_RESOURCE_PORT)) {
+ return false;
+ }
+ cmc_resources_set_callback(&ui->resources, cmc_ui_resources_callback, &ui->search);
+
+ aki_poll_init(&ui->input, input_poll_callback, ui);
+ aki_poll_set(&ui->input, notcurses_inputready_fd(ui->nc), AKI_POLL_READ);
+ aki_poll_start(&ui->input, &c->loop);
+
+ ui->overlay.active = false;
+ ui->overlay.selected = 0;
+
+ struct ncplane *stdplane = notcurses_stdplane(ui->nc);
+ ncplane_set_resizecb(stdplane, resize_cb);
+ ncplane_set_userptr(stdplane, ui);
+ resize_cb(stdplane);
+
+ return true;
+}
diff --git a/src/fruits/cmc/tui/ui.h b/src/fruits/cmc/tui/ui.h
new file mode 100644
index 0000000..2f81392
--- /dev/null
+++ b/src/fruits/cmc/tui/ui.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <aki/event_loop.h>
+#include <notcurses/notcurses.h>
+
+#include "resources.h"
+#include "pane_search.h"
+
+enum {
+ CMC_PANE_LIBRARY = 0,
+ CMC_PANE_LISTS,
+ CMC_PANE_BROWSE,
+ CMC_PANE_SEARCH
+};
+
+struct cmc;
+struct cmc_ui {
+ struct notcurses *nc;
+ u32 cols;
+ u32 rows;
+ struct ncplane *p;
+ u8 pane;
+ struct {
+ struct ncplane *p;
+ u32 cols;
+ u32 rows;
+ bool active;
+ s32 selected;
+ } overlay;
+ struct aki_poll input;
+ struct cmc_resources resources;
+ struct cmc_pane_search search;
+ struct cmc *c;
+};
+
+bool cmc_ui_open(struct cmc_ui *ui, struct cmc *c);
+void cmc_ui_toggle_overlay(struct cmc_ui *ui);
+void cmc_ui_maybe_draw_overlay(struct cmc_ui *ui);
diff --git a/src/fruits/cmc/tui/view.h b/src/fruits/cmc/tui/view.h
new file mode 100644
index 0000000..3c521c3
--- /dev/null
+++ b/src/fruits/cmc/tui/view.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <al/types.h>
+#include <sho/post.h>
+
+enum {
+ CMC_VIEW_NONE = 0,
+ CMC_VIEW_SINGLE,
+ CMC_VIEW_PARTITION
+};
+
+struct cmc_view {
+ struct sho_post *post;
+ struct ncplane *container;
+ u32 pending_requests;
+ u32 rows;
+ u32 y;
+};
+
+struct cmc_view_page {
+ u32 num;
+ bool processed;
+ array(struct cmc_view *) views;
+};
diff --git a/src/fruits/cmc/tui/view_twitter.c b/src/fruits/cmc/tui/view_twitter.c
new file mode 100644
index 0000000..b1070a4
--- /dev/null
+++ b/src/fruits/cmc/tui/view_twitter.c
@@ -0,0 +1,370 @@
+#include "../cmc.h"
+
+#include "view_twitter.h"
+
+#define TEXT_OFFSET 10
+
+#define PROFILE_SIZE 5
+#define PREVIEW_OFFSET_WIDTH 26
+
+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;
+}
+
+u32 cmc_layout_tweet(struct cmc_ui *ui, struct cmc_view_twitter *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);
+ view->text.y = repost ? 1 : 0;
+ view->text.height += view->text.y;
+ rows += view->text.height;
+
+ if (view->preview.mode == CMC_VIEW_NONE) {
+ return rows;
+ }
+
+ view->preview.width = view->text.width - 1;
+ view->preview.x = TEXT_OFFSET;
+ u32 partition_width = view->preview.width / 2;
+
+ struct ncplane_options opts = { 0 };
+ opts.cols = view->preview.width;
+ opts.rows = ncplane_dim_y(notcurses_stdplane(ui->nc)) / 3;
+ opts.x = 0;
+ opts.y = 0;
+ struct ncplane *inter = ncplane_create(notcurses_stdplane(ui->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(ui->nc, resource->v, &resource->vopts, &resource->geom);
+ // rcellx == width case very untested.
+ left += resource->geom.rcelly + (s32)((resource->geom.rcellx != view->preview.width) ? 1 : -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(ui->nc, resource->v, &resource->vopts, &resource->geom);
+ left += resource->geom.rcelly + 1;
+
+ resource = view->preview.resources[1];
+ resource->vopts.n = inter;
+ ncvisual_geom(ui->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(ui->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(ui->nc, resource->v, &resource->vopts, &resource->geom);
+ right += resource->geom.rcelly + 1;
+ }
+
+ break;
+ }
+ }
+
+ ncplane_destroy(inter);
+
+ view->preview.height = AL_MAX(right, left);
+
+ return rows + view->preview.height;
+}
+
+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 >= 1000000000) {
+ al_sprintf(out, "%.1fB", number / 1000000000.f);
+ } else 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);
+ }
+}
+
+static void draw_tweet_resources(struct cmc_ui *ui, struct cmc_view_twitter *view)
+{
+ 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(ui->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(ui->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) + 3);
+ resource->vopts.n = view->preview.p[1];
+ ncvisual_blit(ui->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(ui->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[1]->geom.rcelly + view->text.height + 2, (view->preview.x + view->preview.resources[2]->geom.rcellx) + 3);
+ resource->vopts.n = view->preview.p[3];
+ ncvisual_blit(ui->nc, resource->v, &resource->vopts);
+ }
+
+ break;
+ }
+ }
+
+ 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(ui->nc, resource->v, &resource->vopts);
+ }
+}
+
+void cmc_draw_tweet_func(void *userdata, struct cmc_view *view, u32 y)
+{
+ struct cmc_ui *ui = (struct cmc_ui *)userdata;
+ struct cmc_search *search = ui->search.search;
+ ncplane_reparent_family(view->container, ui->p);
+ ncplane_move_yx(view->container, y, 0);
+ draw_tweet_resources(ui, (struct cmc_view_twitter *)view);
+ view->y = y;
+ al_array_push(search->pager.prev, view);
+}
+
+static char format_buf[32];
+
+void cmc_draw_tweet(struct cmc_view_twitter *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);
+
+ 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 = 0;
+
+ if (post->comments.set) {
+ 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"❤";
+ 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 + 2, format_buf);
+ }
+}
+
+static void get_post_and_maybe_repost(struct cmc_ui *ui, str *unique_id, struct sho_post **post, struct sho_post **repost)
+{
+ struct sho_post_cache *cache = &ui->c->client.cache;
+ *post = sho_post_cache_get(cache, unique_id);
+ *repost = NULL;
+ if ((*post)->type == SHOKI_POST_REPOST) {
+ *repost = *post;
+ *post = sho_post_cache_get(cache, &(*post)->post.unique_id);
+ }
+}
+
+static void strip_link_from_tweet(struct sho_post *post)
+{
+ wstr *check = al_wstr_c(L"https://t.co/");
+ s32 index = al_wstr_rfind(&post->text, L':');
+ if (index < 5) return;
+ index -= 5; // [h]ttps:// (if matched).
+ if (al_wstr_cmp(&post->text, check, index, check->len) == 0) {
+ al_wstr_at(&post->text, index) = L'\0';
+ post->text.len = index;
+ }
+}
+
+struct cmc_view_twitter *cmc_create_view_twitter(struct cmc_ui *ui, str *unique_id)
+{
+ struct cmc_view_twitter *view = al_alloc_object(struct cmc_view_twitter);
+ get_post_and_maybe_repost(ui, unique_id, &view->v.post, &view->repost);
+
+ if (view->v.post->media.size > 0) {
+ strip_link_from_tweet(view->v.post);
+ }
+
+ view->v.rows = 0;
+ view->v.y = 0;
+ view->v.pending_requests = 0;
+
+ 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(ui->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);
+
+ struct sho_post *post = view->v.post;
+ view->profile.resource = cmc_get_resource_from_cache(&ui->resources, &view->v, &post->author.unique_id, 0);
+ struct sho_post_media *media;
+ al_array_foreach_ptr(post->media, i, media) {
+ (void)media;
+ view->preview.resources[i] = cmc_get_resource_from_cache(&ui->resources, &view->v, &post->unique_id, i);
+ }
+
+ switch (post->media.size) {
+ case 0:
+ view->preview.mode = CMC_VIEW_NONE;
+ break;
+ case 1:
+ view->preview.mode = CMC_VIEW_SINGLE;
+ break;
+ default:
+ view->preview.mode = CMC_VIEW_PARTITION;
+ break;
+ }
+
+ return view;
+}
diff --git a/src/fruits/cmc/tui/view_twitter.h b/src/fruits/cmc/tui/view_twitter.h
new file mode 100644
index 0000000..9792826
--- /dev/null
+++ b/src/fruits/cmc/tui/view_twitter.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "ui.h"
+#include "view.h"
+
+struct cmc_view_twitter {
+ 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_view_twitter *cmc_create_view_twitter(struct cmc_ui *ui, str *unique_id);
+void cmc_draw_tweet(struct cmc_view_twitter *view, struct ncplane *container, u32 rows, u32 width);
+u32 cmc_layout_tweet(struct cmc_ui *ui, struct cmc_view_twitter *view, u32 width);
+void cmc_draw_tweet_func(void *userdata, struct cmc_view *view, u32 y);
diff --git a/src/libclient/client.c b/src/libclient/client.c
index 1194994..bf22fee 100644
--- a/src/libclient/client.c
+++ b/src/libclient/client.c
@@ -68,7 +68,7 @@ static bool update_state_command_callback(void *userdata, struct aki_rpc_connect
al_str_clone(&search->module, &s);
aki_packet_read_str(packet, &s);
al_str_clone(&search->query, &s);
- search->last_page = aki_packet_read_u32(packet);
+ search->page = aki_packet_read_u32(packet);
al_array_push(client->state.searches, search);
}
diff --git a/src/libclient/client.h b/src/libclient/client.h
index edb7881..68b0826 100644
--- a/src/libclient/client.h
+++ b/src/libclient/client.h
@@ -31,7 +31,7 @@ struct camu_search {
s32 id;
str module;
str query;
- u32 last_page;
+ u32 page;
array(struct camu_result_list *) lists;
};
diff --git a/src/libclient/resource_client.c b/src/libclient/resource_client.c
index 0318305..a016fa6 100644
--- a/src/libclient/resource_client.c
+++ b/src/libclient/resource_client.c
@@ -60,8 +60,8 @@ void camu_resource_client_init(struct camu_resource_client *client,
client->userdata = userdata;
camu_queue_init(client->queue);
aki_signal_init(&client->signal, signal_callback, client);
- aki_packet_stream_init(&client->client, AKI_SOCKET_TCP, connection_callback,
- connection_closed_callback, packet_callback, packet_sent_callback, client);
+ aki_packet_stream_init(&client->client, AKI_SOCKET_TCP, connection_callback, connection_closed_callback,
+ packet_callback, packet_sent_callback, client);
}
bool camu_resource_client_connect(struct camu_resource_client *client, str *addr, s32 port)
diff --git a/src/libsink/sink.c b/src/libsink/sink.c
index 55485b0..3468a48 100644
--- a/src/libsink/sink.c
+++ b/src/libsink/sink.c
@@ -321,18 +321,24 @@ static void audio_buffer_callback(void *userdata, u8 op)
aki_mutex_unlock(&entry->sink->mutex);
break;
case CAMU_BUFFER_CORK:
+ bmu_vcr_stream_cork(entry->audio.stream);
+ /*
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = CORK,
.value.i = CAMU_SINK_AUDIO,
.opaque = entry
});
+ */
break;
case CAMU_BUFFER_UNCORK:
+ bmu_vcr_stream_uncork(entry->audio.stream);
+ /*
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = UNCORK,
.value.i = CAMU_SINK_AUDIO,
.opaque = entry
});
+ */
break;
case CAMU_BUFFER_PAUSED:
queue_cmd(entry->sink, (struct camu_sink_cmd){
@@ -378,18 +384,24 @@ static void video_buffer_callback(void *userdata, u8 op)
aki_mutex_unlock(&entry->sink->mutex);
break;
case CAMU_BUFFER_CORK:
+ bmu_vcr_stream_cork(entry->video.stream);
+ /*
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = CORK,
.value.i = CAMU_SINK_VIDEO,
.opaque = entry
});
+ */
break;
case CAMU_BUFFER_UNCORK:
+ bmu_vcr_stream_uncork(entry->video.stream);
+ /*
queue_cmd(entry->sink, (struct camu_sink_cmd){
.op = UNCORK,
.value.i = CAMU_SINK_VIDEO,
.opaque = entry
});
+ */
break;
case CAMU_BUFFER_EOF:
queue_cmd(entry->sink, (struct camu_sink_cmd){
@@ -636,7 +648,6 @@ static bool buffer_command_callback(void *userdata, struct aki_rpc_connection *c
return false;
}
-
static bool set_command_callback(void *userdata, struct aki_rpc_connection *conn,
struct aki_packet *packet, struct aki_packet *rpacket)
{
@@ -694,6 +705,8 @@ static bool queue_command_callback(void *userdata, struct aki_rpc_connection *co
sink->queued = ensure_entry_buffered_internal(sink, &addr, port, node_id);
aki_mutex_unlock(&sink->mutex);
+ aki_packet_free(packet);
+
return false;
}
@@ -802,8 +815,8 @@ void camu_sink_close(struct camu_sink *sink)
struct camu_sink_entry *entry;
al_array_foreach(sink->entries, i, entry) {
bmu_client_close(&entry->client);
- al_array_remove_at_iter(sink->entries, i);
}
+ sink->entries.size = 0;
aki_mutex_unlock(&sink->mutex);
}
diff --git a/src/screen/screen.c b/src/screen/screen.c
index 938e01d..604a357 100644
--- a/src/screen/screen.c
+++ b/src/screen/screen.c
@@ -308,12 +308,12 @@ static void run_queue_internal(struct camu_screen *scr)
struct camu_video_buffer *buf;
al_array_foreach(scr->add_queue, i, buf) {
add_buffer_internal(scr, buf);
- al_array_remove_at_iter(scr->add_queue, i);
}
+ scr->add_queue.size = 0;
al_array_foreach(scr->rem_queue, i, buf) {
remove_buffer_internal(scr, buf);
- al_array_remove_at_iter(scr->rem_queue, i);
}
+ scr->rem_queue.size = 0;
}
#endif
diff --git a/src/tree/common.h b/src/tree/common.h
index de6258b..f740a4e 100644
--- a/src/tree/common.h
+++ b/src/tree/common.h
@@ -9,6 +9,8 @@
#define TREE_SERVER_IP al_str_c("127.0.0.1")
//#define TREE_SERVER_IP al_str_c("108.52.160.112")
+#define CAMU_DB_PATH al_str_c("/mnt/store/files/camu_db")
+
enum {
TREE_NODE = 0,
TREE_CLIENT,
diff --git a/src/tree/tree.c b/src/tree/tree.c
index 1be4891..c57d56c 100644
--- a/src/tree/tree.c
+++ b/src/tree/tree.c
@@ -1,5 +1,6 @@
#include <al/log.h>
#include <aki/file.h>
+#include <blake3.h>
#include <jansson.h>
#include "tree.h"
@@ -97,7 +98,7 @@ static void send_current_state(struct tree_server *server, struct tree_user *use
aki_packet_write_s32(packet, search->id);
aki_packet_write_str(packet, &search->module);
aki_packet_write_str(packet, &search->query);
- aki_packet_write_u32(packet, search->last_page);
+ aki_packet_write_u32(packet, search->page);
}
aki_rpc_connection_command(conn, packet, NULL, NULL);
}
@@ -326,9 +327,14 @@ static bool open_user(struct tree_server *tree, struct aki_dir_entry *dir)
str s;
aki_file_read_as_str(&file, &s);
json_error_t error;
- json_t *json = json_loadb(s.data, s.len, 0, &error);
+ json_t *root = json_loadb(s.data, s.len, 0, &error);
+ if (!root) {
+ al_log_error("tree", "Failed to parse json: %.*s:%d:%d (%s).", AL_STR_PRINTF(&dir->path),
+ error.line, error.column, error.text);
+ return false;
+ }
struct tree_user *user = al_alloc_object(struct tree_user);
- al_str_from(&user->username, json_string_value(json_object_get(json, "username")));
+ al_str_from(&user->username, json_string_value(json_object_get(root, "username")));
sho_client_init(&user->search, &tree->resources.cache);
al_array_init(user->lists);
struct tree_list *default_list = al_alloc_object(struct tree_list);
@@ -409,9 +415,7 @@ s32 main(void)
al_array_init(tree.sinks);
al_array_init(tree.users);
- if (!open_db(&tree, al_str_c(CAMU_DB_PATH))) {
- return EXIT_FAILURE;
- }
+ if (!open_db(&tree, CAMU_DB_PATH)) return EXIT_FAILURE;
sho_post_cache_init(&tree.resources.cache);
diff --git a/src/tree/tree.h b/src/tree/tree.h
index 9f8a19a..b8edaad 100644
--- a/src/tree/tree.h
+++ b/src/tree/tree.h
@@ -15,8 +15,6 @@
#include "resource_manager.h"
#include "list.h"
-#define CAMU_DB_PATH "/home/andrew/c/camu/data/camu_db_test"
-
struct tree_node {
struct aki_rpc_connection *conn;
};
diff --git a/src/util/meson.build b/src/util/meson.build
index 3cdaf28..eff0fe6 100644
--- a/src/util/meson.build
+++ b/src/util/meson.build
@@ -1,3 +1,6 @@
util_src = ['color_palette.c', 'blocking_ring_buffer.c']
-util_deps = []
+blake3_opts = cmake.subproject_options()
+blake3_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' })
+blake3 = cmake.subproject('BLAKE3').dependency('blake3')
+util_deps = [blake3]
util = declare_dependency(sources: util_src, dependencies: util_deps)
diff --git a/subprojects/BLAKE3.wrap b/subprojects/BLAKE3.wrap
new file mode 100644
index 0000000..0a26602
--- /dev/null
+++ b/subprojects/BLAKE3.wrap
@@ -0,0 +1,6 @@
+[wrap-git]
+url = https://github.com/BLAKE3-team/BLAKE3.git
+revision = 8fc36186b84385d36d8339606e4d1ea6ff471965
+depth = 1
+patch_directory = BLAKE3
+method = cmake
diff --git a/subprojects/packagefiles/BLAKE3/CMakeLists.txt b/subprojects/packagefiles/BLAKE3/CMakeLists.txt
new file mode 100644
index 0000000..20bc4cc
--- /dev/null
+++ b/subprojects/packagefiles/BLAKE3/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(c)