From bbf3314165182e402ff25acccddc004a87f81ef0 Mon Sep 17 00:00:00 2001 From: Andrew Opalach Date: Mon, 12 Feb 2024 21:09:10 -0500 Subject: Initial cmc implementation, sink fixes Signed-off-by: Andrew Opalach --- src/fruits/cmc/tui/pager.c | 88 +++++++++ src/fruits/cmc/tui/pager.h | 33 ++++ src/fruits/cmc/tui/pane_search.c | 330 ++++++++++++++++++++++++++++++++++ src/fruits/cmc/tui/pane_search.h | 29 +++ src/fruits/cmc/tui/resources.c | 85 +++++++++ src/fruits/cmc/tui/resources.h | 31 ++++ src/fruits/cmc/tui/ui.c | 133 ++++++++++++++ src/fruits/cmc/tui/ui.h | 38 ++++ src/fruits/cmc/tui/view.h | 24 +++ src/fruits/cmc/tui/view_twitter.c | 370 ++++++++++++++++++++++++++++++++++++++ src/fruits/cmc/tui/view_twitter.h | 35 ++++ 11 files changed, 1196 insertions(+) create mode 100644 src/fruits/cmc/tui/pager.c create mode 100644 src/fruits/cmc/tui/pager.h create mode 100644 src/fruits/cmc/tui/pane_search.c create mode 100644 src/fruits/cmc/tui/pane_search.h create mode 100644 src/fruits/cmc/tui/resources.c create mode 100644 src/fruits/cmc/tui/resources.h create mode 100644 src/fruits/cmc/tui/ui.c create mode 100644 src/fruits/cmc/tui/ui.h create mode 100644 src/fruits/cmc/tui/view.h create mode 100644 src/fruits/cmc/tui/view_twitter.c create mode 100644 src/fruits/cmc/tui/view_twitter.h (limited to 'src/fruits/cmc/tui') 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 +#include + +#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 +#include + +#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 + +#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 +#include +#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 + +#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 + +#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 +#include + +#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 +#include + +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); -- cgit v1.2.3-101-g0448