From 130a0edc0405e53b45f8b2f8da0b94356480a644 Mon Sep 17 00:00:00 2001 From: Andrew Opalach Date: Mon, 1 Jan 2024 11:23:22 -0500 Subject: wip Signed-off-by: Andrew Opalach --- src/fruits/cap/cap.c | 59 ++-- src/fruits/cap/cap.h | 9 +- src/fruits/cap/meson.build | 6 +- src/fruits/cmc/cmc.c | 781 +++++++++++++++++++++++++++++++++++++++++++++ src/fruits/cmc/meson.build | 7 + src/fruits/cmv/cmv2.c | 88 +++-- src/fruits/cmv/meson.build | 30 +- src/fruits/cmv/tui.c | 99 ++++-- src/fruits/cmv/tui.h | 14 +- 9 files changed, 971 insertions(+), 122 deletions(-) create mode 100644 src/fruits/cmc/cmc.c create mode 100644 src/fruits/cmc/meson.build (limited to 'src/fruits') 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 -#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 #include #include +#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 +#include +#include +#include +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#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 +#include #if CMV_USE_SOCKET #include #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 #include #include +#include #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 +#include #include #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); -- cgit v1.2.3-101-g0448