summaryrefslogtreecommitdiff
path: root/src/fruits
diff options
context:
space:
mode:
Diffstat (limited to 'src/fruits')
-rw-r--r--src/fruits/cap/cap.c59
-rw-r--r--src/fruits/cap/cap.h9
-rw-r--r--src/fruits/cap/meson.build6
-rw-r--r--src/fruits/cmc/cmc.c781
-rw-r--r--src/fruits/cmc/meson.build7
-rw-r--r--src/fruits/cmv/cmv2.c88
-rw-r--r--src/fruits/cmv/meson.build30
-rw-r--r--src/fruits/cmv/tui.c99
-rw-r--r--src/fruits/cmv/tui.h14
9 files changed, 971 insertions, 122 deletions
diff --git a/src/fruits/cap/cap.c b/src/fruits/cap/cap.c
index 317abf4..3f5f5e8 100644
--- a/src/fruits/cap/cap.c
+++ b/src/fruits/cap/cap.c
@@ -1,20 +1,20 @@
#include <al/random.h>
-#include "cap.h"
-
-#if CAP_USE_PYTHON
-#include "../../shoki/src/search.h"
-#endif
#include "../../cache/handlers/file.h"
#ifdef AKIYO_HAS_CURL
#include "../../cache/handlers/http.h"
#endif
+#include "cap.h"
+
void cap_init(struct cap_runner *cap, bool (*callback)(void *, u8, str *, str *, void *), void *userdata)
{
cap->callback = callback;
cap->userdata = userdata;
al_array_init(cap->lists);
+#if CAP_USE_PYTHON
+ sho_client_init(&cap->search, NULL);
+#endif
}
static struct cap_list *list_from_name(struct cap_runner *cap, str *name)
@@ -28,7 +28,7 @@ static struct cap_list *list_from_name(struct cap_runner *cap, str *name)
return NULL;
}
-static struct cch_entry *entry_for_unique_id(str *unique_id)
+static struct cch_entry *entry_for_unique_id(struct cap_runner *cap, str *unique_id)
{
struct cch_entry *entry = NULL;
#ifdef AKIYO_HAS_CURL
@@ -36,16 +36,21 @@ static struct cch_entry *entry_for_unique_id(str *unique_id)
if (is_search || al_str_cmp(unique_id, al_str_c("https://"), 0, 8) == 0 ||
al_str_cmp(unique_id, al_str_c("http://"), 0, 7) == 0) {
#if CAP_USE_PYTHON
- struct sho_search s;
- sho_search_init(&s);
+ str provider;
str query;
+ al_str_from(&provider, "");
al_str_from(&query, "");
if (al_str_cmp(unique_id, al_str_c("https://twitter.com"), 0, 19) == 0 ||
al_str_cmp(unique_id, al_str_c("https://x.com"), 0, 13) == 0) {
al_str_cat(&query, al_str_c("tweet:"));
al_str_cat(&query, unique_id);
- if (!sho_search_more_results(&s, al_str_c("twitter"), &query)) {
+ al_str_cat(&provider, al_str_c("twitter"));
+ } else if (al_str_cmp(unique_id, al_str_c("https://instagram.com"), 0, 21) == 0) {
+ s32 slash = al_str_rfind(unique_id, '/');
+ if (slash >= 0) {
+ al_str_cat(&query, al_str_substr(unique_id, slash + 1, unique_id->len));
}
+ al_str_cat(&provider, al_str_c("instagram"));
} else {
if (is_search) {
al_str_cat(&query, al_str_substr(unique_id, 1, unique_id->len));
@@ -53,26 +58,27 @@ static struct cch_entry *entry_for_unique_id(str *unique_id)
al_str_cat(&query, al_str_c("link:"));
al_str_cat(&query, unique_id);
}
- if (!sho_search_more_results(&s, al_str_c("youtube"), &query)) {
- }
+ al_str_cat(&provider, al_str_c("youtube"));
}
+ s32 id = sho_client_create_search(&cap->search, &provider, &query);
+ al_str_free(&provider);
al_str_free(&query);
- entry = NULL;
- struct sho_result_page *page;
- al_array_foreach_ptr(s.result.pages, k, page) {
- struct sho_post *post;
- al_array_foreach_ptr(page->posts, i, post) {
- struct sho_post_media *media;
- al_array_foreach_ptr(post->media, j, media) {
- if (media->url.len > 0) {
- entry = cch_handler_http_create(&media->url);
- break;
- }
+ if (id < 0) return NULL;
+ struct sho_search *search = sho_client_get_search(&cap->search, id);
+ if (!search || !sho_search_from_page(search, 0)) return NULL;
+ struct sho_result_page *page = &al_array_at(search->pages, 0);
+ struct sho_post *post;
+ al_array_foreach_ptr(page->posts, i, post) {
+ struct sho_post_media *media;
+ al_array_foreach_ptr(post->media, j, media) {
+ if (media->url.len > 0) {
+ entry = cch_handler_http_create(&media->url);
+ break;
}
- if (entry) break;
}
+ if (entry) break;
}
- sho_search_free(&s);
+ sho_client_discard_search(&cap->search, id);
#else
if (!is_search) entry = cch_handler_http_create(unique_id);
#endif
@@ -88,9 +94,8 @@ static bool buffer_entry(struct cap_runner *cap, struct cap_list *list, struct c
{
if (entry->errored) return false;
if (entry->buffer_requested) return true;
- entry->entry = entry_for_unique_id(&entry->unique_id);
- if (!entry->entry) return false;
- if (!cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, entry->entry)) {
+ entry->entry = entry_for_unique_id(cap, &entry->unique_id);
+ if (!entry->entry || !cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, entry->entry)) {
entry->errored = true;
return false;
}
diff --git a/src/fruits/cap/cap.h b/src/fruits/cap/cap.h
index 57f6889..69d4d6d 100644
--- a/src/fruits/cap/cap.h
+++ b/src/fruits/cap/cap.h
@@ -1,11 +1,15 @@
#pragma once
-#define CAP_USE_PYTHON 1
+#define CAP_USE_PYTHON 0
#include <al/array.h>
#include <al/str.h>
#include <aki/thread.h>
+#if CAP_USE_PYTHON
+#include "../../shoki/src/search.h"
+#endif
+
struct cap_list_entry {
str unique_id;
struct cch_entry *entry;
@@ -34,6 +38,9 @@ enum {
struct cap_runner {
array(struct cap_list) lists;
+#if CAP_USE_PYTHON
+ struct sho_client search;
+#endif
bool (*callback)(void *, u8, str *, str *, void *);
void *userdata;
};
diff --git a/src/fruits/cap/meson.build b/src/fruits/cap/meson.build
index b5930ac..f85ab00 100644
--- a/src/fruits/cap/meson.build
+++ b/src/fruits/cap/meson.build
@@ -1,4 +1,8 @@
cap_src = ['cap.c']
-cap_deps = [common_deps, shoki]
+cap_deps = [common_deps]
+use_python = true
+if use_python
+ cap_deps += [shoki]
+endif
cap = declare_dependency(sources: cap_src,
dependencies: cap_deps)
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)
diff --git a/src/fruits/cmv/cmv2.c b/src/fruits/cmv/cmv2.c
index 74a4113..89e3427 100644
--- a/src/fruits/cmv/cmv2.c
+++ b/src/fruits/cmv/cmv2.c
@@ -1,7 +1,8 @@
-#define CMV_USE_TUI 1
-#define CMV_USE_SOCKET 1
+#define CMV_USE_TUI 0
+#define CMV_USE_SOCKET 0
#include <al/log.h>
+#include <aki/event_loop.h>
#if CMV_USE_SOCKET
#include <aki/line_processor.h>
#endif
@@ -14,7 +15,8 @@
#include "../../render/renderer_libplacebo.h"
//#include "../../render/renderer_tiger.h"
-#include "../../libsink/sink.h"
+#include "../../libsink/sink2.h"
+#include "../../tree/common.h"
#include "../../shoki/src/search.h"
@@ -162,14 +164,7 @@ static void screen_callback(void *userdata, u8 op, f64 float0)
}
}
-static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata)
-{
- struct cmv *c = (struct cmv *)userdata;
- aki_event_loop_run(&c->loop);
- aki_event_loop_destroy(&c->loop);
- return 0;
-}
-
+/*
static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void *opaque)
{
struct cmv *c = (struct cmv *)userdata;
@@ -190,6 +185,7 @@ static bool cap_callback(void *userdata, u8 op, str *name, str *unique_id, void
}
return true;
}
+*/
#if CMV_USE_SOCKET
static u8 line_callback(void *userdata, str *line)
@@ -214,7 +210,7 @@ static void timer_callback(void *userdata, struct aki_timer *timer)
struct cmv *c = (struct cmv *)userdata;
struct camu_sink_entry *entry = camu_sink_get_current(&c->sink);
if (entry) {
- tui_draw(&c->tui, &entry->runner, &entry->clock);
+ tui_draw(&c->tui, &((struct camu_sink_local *)entry)->runner, &entry->clock);
} else {
tui_draw(&c->tui, NULL, NULL);
}
@@ -233,22 +229,39 @@ static s32 log_callback(void *userdata, char *s)
static void lav_log_callback(void *userdata, s32 level, const char *fmt, va_list args)
{
(void)userdata;
- if (level < AV_LOG_VERBOSE) al_logv(warn, "lav_internal", (char *)fmt, args);
+ if (level < AV_LOG_VERBOSE) al_logv("warn", "lav_internal", (char *)fmt, args);
}
#endif
#endif
+static aki_thread_result AKI_THREADCALL event_loop_thread(void *userdata)
+{
+ struct cmv *c = (struct cmv *)userdata;
+ aki_event_loop_run(&c->loop);
+ aki_event_loop_destroy(&c->loop);
+ return 0;
+}
+
+static struct cmv c = { 0 };
+
+static void sigint_handler(s32 signum)
+{
+ (void)signum;
+ c.quit = 1;
+}
+
s32 main(s32 argc, char *argv[])
{
- aki_common_init();
+ if (!aki_common_init()) return EXIT_FAILURE;
- struct cmv c = { };
+ signal(SIGINT, sigint_handler);
+
+ aki_event_loop_init(&c.loop);
#if CMV_USE_TUI
- if (!tui_init(&c.tui)) {
- aki_common_close();
- return EXIT_FAILURE;
- }
+ if (!tui_init(&c.tui, &c.loop)) {
+ goto err;
+ };
al_set_print(log_callback, &c);
#ifdef HAVE_FFMPEG
camu_lav_set_log_callback(lav_log_callback);
@@ -257,37 +270,35 @@ s32 main(s32 argc, char *argv[])
c.scr.callback = screen_callback;
c.scr.userdata = &c;
- if (!camu_screen_init(&c.scr)) {
- aki_common_close();
- return EXIT_FAILURE;
- }
-
- if (!camu_screen_create_window(&c.scr, "cmv")) {
- aki_common_close();
- return EXIT_FAILURE;
+ if (!camu_screen_init(&c.scr) || !camu_screen_create_window(&c.scr, "cmv")) {
+ goto err;
}
c.renderer = camu_renderer_lp_create();
//c.renderer = camu_renderer_tiger_create();
- camu_screen_create_renderer(&c.scr, c.renderer);
+ if (!camu_screen_create_renderer(&c.scr, c.renderer)) {
+ goto err;
+ }
+ c.renderer->render(c.renderer, &c.scr);
#if CAP_USE_PYTHON
- bool py_init = sho_python_init();
+ bool py_init = argc == 1 ? sho_python_init() : false;
#endif
camu_mixer_init(&c.mixer, (struct camu_audio *)&audio_plugin_miniaudio);
c.mixer.audio->configure_stream(c.mixer.audio, NULL);
- aki_event_loop_init(&c.loop);
-
+ camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer);
c.sink.callback = sink_callback;
c.sink.userdata = &c;
- camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer);
+ camu_sink_connect(&c.sink, al_str_c("127.0.0.1"), TREE_PORT);
+ /*
cap_init(&c.cap, cap_callback, &c);
cap_make_list(&c.cap, default_list);
for (s32 i = 1; i < argc; i++) {
cap_list_add(&c.cap, default_list, al_str_c(argv[i]));
}
+ */
#if CMV_USE_SOCKET
c.socket.type = AKI_SOCKET_UNIX;
@@ -304,7 +315,7 @@ s32 main(s32 argc, char *argv[])
#if CMV_USE_TUI
aki_timer_init(&c.timer, &c.loop, timer_callback, &c);
- aki_timer_set_repeat(&c.timer, 0.10);
+ aki_timer_set_repeat(&c.timer, 0.05);
aki_timer_again(&c.timer);
#endif
@@ -326,10 +337,10 @@ s32 main(s32 argc, char *argv[])
aki_thread_join(&thread0);
camu_mixer_close(&c.mixer);
- camu_screen_close(&c.scr);
c.renderer->free(&c.renderer);
+ camu_screen_close(&c.scr);
- cap_close(&c.cap);
+ //cap_close(&c.cap);
#if CAP_USE_PYTHON
if (py_init) sho_python_close();
@@ -342,4 +353,11 @@ s32 main(s32 argc, char *argv[])
aki_common_close();
return EXIT_SUCCESS;
+err:
+#if CMV_USE_TUI
+ tui_close(&c.tui);
+#endif
+ aki_event_loop_destroy(&c.loop);
+ aki_common_close();
+ return EXIT_FAILURE;
}
diff --git a/src/fruits/cmv/meson.build b/src/fruits/cmv/meson.build
index b604104..1c9ed2d 100644
--- a/src/fruits/cmv/meson.build
+++ b/src/fruits/cmv/meson.build
@@ -1,26 +1,10 @@
-cmv2_src = ['cmv2.c', 'tui.c']
-#cmv2_src = ['cmv2.c']
+cmv_src = ['cmv2.c']
cmv_deps = [common_deps, buffer, cache, av, screen, render, mixer, bimu, libsink, cap]
-miniaudio_args = [
- '-DMA_NO_JACK',
- '-DMA_NO_ALSA',
- '-DMA_NO_DECODING',
- '-DMA_NO_ENCODING',
- '-DMA_NO_GENERATION',
- '-DMA_NO_RESOURCE_MANAGER',
- '-DMA_NO_NODE_GRAPH',
- '-DMA_NO_ENGINE',
- '-DMA_NO_RUNTIME_LINKING'
-]
+use_tui = true
+if use_tui
+ cmv_src += ['tui.c']
+ cmv_deps += [dependency('notcurses')]
+endif
-sekihi_args = [
- '-DSEKIHI_WINDOW_WAYLAND',
-# '-DSEKIHI_WINDOW_GLFW',
- '-DSEKIHI_API_OPENGL',
-# '-DSEKIHI_API_VULKAN',
- '-DSEKIHI_POLL_INLINE',
- '-DSEKIHI_PAUSE'
-]
-
-executable('cmv2', cmv2_src, dependencies: cmv_deps, c_args: [miniaudio_args, sekihi_args])
+executable('cmv', cmv_src, dependencies: cmv_deps)
diff --git a/src/fruits/cmv/tui.c b/src/fruits/cmv/tui.c
index 21aeac8..8315a13 100644
--- a/src/fruits/cmv/tui.c
+++ b/src/fruits/cmv/tui.c
@@ -1,6 +1,7 @@
#include <aki/file.h>
#include <al/log.h>
#include <sys/ioctl.h>
+#include <notcurses/direct.h>
#include "tui.h"
@@ -23,16 +24,39 @@ static void handle_winch(s32 sig)
aki_mutex_unlock(&tui->mutex);
}
-bool tui_init(struct cmv_tui *tui)
+static void input_poll_callback(void *userdata, s32 revents)
+{
+ struct cmv_tui *tui = (struct cmv_tui *)userdata;
+ (void)revents;
+ struct ncinput input;
+ u32 ret;
+ while (1) {
+ ret = ncdirect_get_nblock(tui->dir, &input);
+ if (ret == (u32)-1 || ret == 0) break;
+ if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) {
+ }
+ }
+}
+
+bool tui_init(struct cmv_tui *tui, struct aki_event_loop *loop)
{
tui->update_size = true;
tui->width = 0;
tui->height = 0;
- //tui->prev_lines = 0;
+ tui->prev_lines = 0;
- tui->fd = fileno(stdin);
+ tui->fd = fileno(stdout);
tui->out = fdopen(tui->fd, "w");
if (!tui->out) return false;
+ tui->buffer = al_malloc(256);
+
+ //if (!(tui->dir = ncdirect_init(NULL, tui->out, 0))) {
+ // fclose(tui->out);
+ // return false;
+ //}
+
+ //aki_poll_init(&tui->poll, ncdirect_inputready_fd(tui->dir), AKI_POLL_READ, input_poll_callback, tui);
+ //aki_poll_start(&tui->poll, loop);
tui_global = tui;
aki_mutex_init(&tui->mutex);
@@ -46,81 +70,96 @@ bool tui_init(struct cmv_tui *tui)
tui->term.c_lflag &= ~ECHO;
tcsetattr(tui->fd, 0, &tui->term);
- al_array_init(tui->log_buffer);
- tui->last_pts = 0.0;
+ al_queue_init(&tui->log_buffer, 256);
return true;
}
void tui_push_log_msg(struct cmv_tui *tui, char *msg)
{
- aki_mutex_lock(&tui->mutex);
- al_array_push(tui->log_buffer, msg);
- aki_mutex_unlock(&tui->mutex);
+ al_queue_push(&tui->log_buffer, msg);
}
-static char buffer[128];
-
-static void draw_now_playing(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner)
+static void draw_now_playing(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock)
{
+ char *buffer = tui->buffer;
f64 pts = camu_clock_get_pts(clock);
- if (pts == -1) pts = tui->last_pts;
- else tui->last_pts = pts;
- s32 parts = 0;
f64 duration = bmu_local_get_duration(runner);
+ s32 text = 0;
+ s64 minute = (s64)(pts / 60L);
+ s64 hour = minute / 60L;
+ minute -= hour * 60L;
+ text += al_sprintf(buffer + text, "[");
+ if (hour > 0) text += al_sprintf(buffer + text, "%.2ld:", hour);
+ text += al_sprintf(buffer + text, "%.2ld:%.2ld/", minute, (s64)pts % 60L);
+ minute = (s64)(duration / 60L);
+ hour = minute / 60L;
+ minute -= hour * 60L;
+ if (hour > 0) text += al_sprintf(buffer + text, "%.2ld:", hour);
+ text += al_sprintf(buffer + text, "%.2ld:%.2ld", minute, (s64)duration % 60L);
+ text += al_sprintf(buffer + text, "]");
+ s32 parts = 0;
if (pts > 0.0 && duration != 0.0) {
if (pts > duration) pts = duration;
f64 percent = pts / duration;
- parts = tui->width * percent;
+ parts = ((tui->width - text) * percent);
} else if (duration == 0.0) {
- parts = tui->width;
+ parts = (tui->width - text);
}
- al_memset(buffer, '-', parts);
- buffer[parts] = '\0';
+ al_memset(buffer + text, '-', parts);
+ buffer[text + parts] = '\0';
fprintf(tui->out, "%s", buffer);
}
void tui_draw(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock)
{
aki_mutex_lock(&tui->mutex);
-
if (tui->update_size) {
update_term_size(tui);
tui->update_size = false;
}
+ aki_mutex_unlock(&tui->mutex);
+ // Erase line.
fprintf(tui->out, "\r\033[K");
- //for (u32 i = 1; i < tui->prev_lines; i++) {
- // fprintf(tui->out, "\033[A\r\033[K");
- //}
+ for (u32 i = 0; i < tui->prev_lines; i++) {
+ // Up one, erase line.
+ fprintf(tui->out, "\033[A\r\033[K");
+ }
char *msg;
- for (u32 i = 0; i < tui->log_buffer.size; i++) {
- al_array_pop_at(tui->log_buffer, 0, msg);
+ while (al_queue_pop(&tui->log_buffer, (void **)&msg)) {
fprintf(tui->out, "%s\n", msg);
al_free(msg);
}
- //tui->prev_lines = 0;
+ // Simply printing each log message with a newline will leave
+ // a one character high space in the bottom of the terminal window.
+ // We use that to display a very basic status line (like mpv).
+ // Trying to use more than that one line space without a more complete
+ // terminal interface gets scuffed very fast.
+
+ tui->prev_lines = 0;
if (runner) {
- draw_now_playing(tui, clock, runner);
+ draw_now_playing(tui, runner, clock);
}
-
- aki_mutex_unlock(&tui->mutex);
}
void tui_close(struct cmv_tui *tui)
{
+ if (!tui->out) return;
+ al_free(tui->buffer);
char *msg;
- al_array_foreach(tui->log_buffer, i, msg) {
+ while (al_queue_pop(&tui->log_buffer, (void **)&msg)) {
al_free(msg);
}
- al_array_free(tui->log_buffer);
+ al_queue_free(&tui->log_buffer);
aki_mutex_destroy(&tui->mutex);
fprintf(tui->out, "\n");
fflush(tui->out);
tui->term.c_lflag |= ECHO;
tcsetattr(tui->fd, 0, &tui->term);
+ //ncdirect_stop(tui->dir);
fclose(tui->out);
}
diff --git a/src/fruits/cmv/tui.h b/src/fruits/cmv/tui.h
index fb4215e..6898539 100644
--- a/src/fruits/cmv/tui.h
+++ b/src/fruits/cmv/tui.h
@@ -1,24 +1,28 @@
#pragma once
+#include <al/queue.h>
+#include <aki/event_loop.h>
#include <termios.h>
#include "../../bimu/local.h"
-#include "../../buffer/clock.h"
+#include "../../buffer/video.h"
struct cmv_tui {
s32 fd;
FILE *out;
+ char *buffer;
+ struct ncdirect *dir;
+ struct aki_poll poll;
struct aki_mutex mutex;
struct termios term;
bool update_size;
u32 width;
u32 height;
- //u32 prev_lines;
- array(char *) log_buffer;
- f64 last_pts;
+ u32 prev_lines;
+ queue log_buffer;
};
-bool tui_init(struct cmv_tui *tui);
+bool tui_init(struct cmv_tui *tui, struct aki_event_loop *loop);
void tui_push_log_msg(struct cmv_tui *tui, char *msg);
void tui_draw(struct cmv_tui *tui, struct bmu_local *runner, struct camu_clock *clock);
void tui_close(struct cmv_tui *tui);