summaryrefslogtreecommitdiff
path: root/src/fruits
diff options
context:
space:
mode:
authorAndrew Opalach <andrew@akon.city> 2023-11-06 11:54:08 -0500
committerAndrew Opalach <andrew@akon.city> 2023-11-06 11:54:08 -0500
commita48a68cfb04b2020737c0adfb0e7667451a22b5c (patch)
treea92121897f84298c3798a4f2ba56de45cd100eab /src/fruits
downloadcamu-a48a68cfb04b2020737c0adfb0e7667451a22b5c.tar.gz
camu-a48a68cfb04b2020737c0adfb0e7667451a22b5c.tar.bz2
camu-a48a68cfb04b2020737c0adfb0e7667451a22b5c.zip
Add camu
- Subprojects temporarily omitted Signed-off-by: Andrew Opalach <andrew@akon.city>
Diffstat (limited to 'src/fruits')
-rw-r--r--src/fruits/cap/cap.c219
-rw-r--r--src/fruits/cap/cap.h42
-rw-r--r--src/fruits/cap/meson.build4
-rw-r--r--src/fruits/cmv/cmv2.c331
-rw-r--r--src/fruits/cmv/meson.build26
-rw-r--r--src/fruits/cmv/tui.c118
-rw-r--r--src/fruits/cmv/tui.h21
7 files changed, 761 insertions, 0 deletions
diff --git a/src/fruits/cap/cap.c b/src/fruits/cap/cap.c
new file mode 100644
index 0000000..2f60cb4
--- /dev/null
+++ b/src/fruits/cap/cap.c
@@ -0,0 +1,219 @@
+#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
+
+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);
+}
+
+static struct cap_list *list_from_name(struct cap_runner *cap, str *name)
+{
+ struct cap_list *list;
+ al_array_foreach_ptr(cap->lists, i, list) {
+ if (al_str_eq(&list->name, name)) {
+ return list;
+ }
+ }
+ return NULL;
+}
+
+static struct cch_entry *entry_for_unique_id(str *unique_id)
+{
+ struct cch_entry *entry = NULL;
+#ifdef AKIYO_HAS_CURL
+ bool is_search = al_str_at(unique_id, 0) == ';';
+ 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);
+ 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) {
+ str query;
+ al_str_from(&query, "tweet:");
+ al_str_cat(&query, unique_id);
+ if (!sho_search_more_results(&s, al_str_c("twitter"), &query)) {
+ }
+ al_str_free(&query);
+ } else {
+ str query;
+ if (is_search) {
+ //al_str_from(&query, "\"");
+ al_str_from(&query, "");
+ al_str_cat(&query, al_str_substr(unique_id, 1, unique_id->len));
+ //al_str_cat(&query, al_str_c("\""));
+ } else {
+ al_str_from(&query, "link:");
+ al_str_cat(&query, unique_id);
+ }
+ if (!sho_search_more_results(&s, al_str_c("youtube"), &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 (entry) break;
+ }
+ }
+ sho_search_free(&s);
+#else
+ if (!is_search) entry = cch_handler_http_create(unique_id);
+#endif
+ entry->handler->maybe_spawn_worker(entry->handler, 0);
+ } else // {
+#endif
+ entry = cch_handler_file_create(unique_id);
+ // }
+ return entry;
+}
+
+static bool buffer_entry(struct cap_runner *cap, struct cap_list *list, struct cap_list_entry *entry)
+{
+ if (entry->buffer_requested) return true;
+ struct cch_entry *centry = entry_for_unique_id(&entry->unique_id);
+ if (!centry) return false;
+ if (!cap->callback(cap->userdata, CAP_BUFFER, &list->name, &entry->unique_id, centry)) {
+ entry->errored = true;
+ return false;
+ }
+ entry->buffer_requested = true;
+ return true;
+}
+
+static void maybe_cleanup_entries(struct cap_runner *cap, struct cap_list *list)
+{
+ struct cap_list_entry *entry;
+ al_array_foreach_ptr(list->entries, i, entry) {
+ if (entry->buffer_requested && abs((s32)i - list->current) >= 3) {
+ cap->callback(cap->userdata, CAP_UNLOAD, &list->name, &entry->unique_id, NULL);
+ entry->buffer_requested = false;
+ }
+ }
+}
+
+static bool list_skip_internal(struct cap_runner *cap, struct cap_list *list, s32 n);
+
+static void list_pump_internal(struct cap_runner *cap, struct cap_list *list)
+{
+ s32 size = (s32)list->entries.size;
+ if (size <= list->current) return;
+ if (list->set != list->current) {
+ struct cap_list_entry *entry = &al_array_at(list->entries, list->current);
+ if (!buffer_entry(cap, list, entry)) {
+ list_skip_internal(cap, list, 1);
+ return;
+ }
+ list->set = list->current;
+ if (list->swap && entry->buffer_requested) {
+ cap->callback(cap->userdata, CAP_SWAP, &list->name, &entry->unique_id, NULL);
+ } else {
+ cap->callback(cap->userdata, CAP_SET, &list->name, &entry->unique_id, NULL);
+ }
+ }
+ if (list->current + 1 < size) {
+ buffer_entry(cap, list, &al_array_at(list->entries, list->current + 1));
+ }
+ if (list->current - 1 >= 0) {
+ buffer_entry(cap, list, &al_array_at(list->entries, list->current - 1));
+ }
+ maybe_cleanup_entries(cap, list);
+}
+
+bool list_skip_internal(struct cap_runner *cap, struct cap_list *list, s32 n)
+{
+ s32 size = (s32)list->entries.size;
+ if (list->current + n < 0 || list->current + n >= size) {
+ return false;
+ }
+ list->current += n;
+ list_pump_internal(cap, list);
+ return true;
+}
+
+void cap_make_list(struct cap_runner *cap, str *name)
+{
+ struct cap_list list;
+ al_str_clone(&list.name, name);
+ list.current = 0;
+ list.set = -1;
+ list.idle = false;
+ list.swap = false;
+ al_array_init(list.entries);
+ al_array_push(cap->lists, list);
+}
+
+void cap_list_add(struct cap_runner *cap, str *name, str *unique_id)
+{
+ struct cap_list *list = list_from_name(cap, name);
+ if (!list) return;
+ struct cap_list_entry entry;
+ al_str_clone(&entry.unique_id, unique_id);
+ entry.buffer_requested = false;
+ entry.errored = false;
+ al_array_push(list->entries, entry);
+ if (list->idle && list_skip_internal(cap, list, 1)) {
+ list->idle = false;
+ return;
+ }
+ list_pump_internal(cap, list);
+}
+
+bool cap_list_skip(struct cap_runner *cap, str *name, s32 n)
+{
+ struct cap_list *list = list_from_name(cap, name);
+ if (!list) return false;
+ return list_skip_internal(cap, list, n);
+}
+
+void cap_list_pump(struct cap_runner *cap, str *name)
+{
+ struct cap_list *list = list_from_name(cap, name);
+ if (list) list_pump_internal(cap, list);
+}
+
+bool cap_list_set_completed(struct cap_runner *cap, str *name)
+{
+ struct cap_list *list = list_from_name(cap, name);
+ if (!list) return false;
+ list->swap = true;
+ list->idle = !list_skip_internal(cap, list, 1);
+ list->swap = false;
+ return !list->idle;
+}
+
+static void list_close_internal(struct cap_list *list)
+{
+ struct cap_list_entry *entry;
+ al_array_foreach_ptr(list->entries, i, entry) {
+ al_str_free(&entry->unique_id);
+ }
+ al_array_free(list->entries);
+ al_str_free(&list->name);
+}
+
+void cap_close(struct cap_runner *cap)
+{
+ struct cap_list *list;
+ al_array_foreach_ptr(cap->lists, i, list) {
+ list_close_internal(list);
+ }
+ al_array_free(cap->lists);
+}
diff --git a/src/fruits/cap/cap.h b/src/fruits/cap/cap.h
new file mode 100644
index 0000000..d522a82
--- /dev/null
+++ b/src/fruits/cap/cap.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#define CAP_USE_PYTHON 1
+
+#include <al/array.h>
+#include <al/str.h>
+
+struct cap_list_entry {
+ str unique_id;
+ bool buffer_requested;
+ bool errored;
+};
+
+struct cap_list {
+ str name;
+ s32 current;
+ s32 set;
+ bool idle;
+ bool swap;
+ array(struct cap_list_entry) entries;
+};
+
+enum {
+ CAP_BUFFER = 0,
+ CAP_SET,
+ CAP_SWAP,
+ CAP_UNLOAD
+};
+
+struct cap_runner {
+ array(struct cap_list) lists;
+ bool (*callback)(void *, u8, str *, str *, void *);
+ void *userdata;
+};
+
+void cap_init(struct cap_runner *cap, bool (*callback)(void *, u8, str *, str *, void *), void *userdata);
+void cap_make_list(struct cap_runner *cap, str *name);
+void cap_list_add(struct cap_runner *cap, str *name, str *unique_id);
+bool cap_list_skip(struct cap_runner *cap, str *name, s32 n);
+void cap_list_pump(struct cap_runner *cap, str *name);
+bool cap_list_set_completed(struct cap_runner *cap, str *name);
+void cap_close(struct cap_runner *cap);
diff --git a/src/fruits/cap/meson.build b/src/fruits/cap/meson.build
new file mode 100644
index 0000000..b5930ac
--- /dev/null
+++ b/src/fruits/cap/meson.build
@@ -0,0 +1,4 @@
+cap_src = ['cap.c']
+cap_deps = [common_deps, shoki]
+cap = declare_dependency(sources: cap_src,
+ dependencies: cap_deps)
diff --git a/src/fruits/cmv/cmv2.c b/src/fruits/cmv/cmv2.c
new file mode 100644
index 0000000..a7b688c
--- /dev/null
+++ b/src/fruits/cmv/cmv2.c
@@ -0,0 +1,331 @@
+#define CMV_USE_TUI 1
+
+#include <al/log.h>
+#include <aki/line_processor.h>
+
+#include "../../codec/libav/common.h"
+
+#include "../../mixer/audio_miniaudio.h"
+
+#include "../../screen/screen.h"
+#include "../../render/renderer_libplacebo.h"
+//#include "../../render/renderer_tiger.h"
+
+#include "../../libsink/sink.h"
+
+#include "../../shoki/src/search.h"
+
+#include "../cap/cap.h"
+
+#if CMV_USE_TUI
+#include "tui.h"
+#endif
+
+struct cmv {
+ s32 quit;
+ struct camu_screen scr;
+ struct camu_renderer *renderer;
+ struct camu_mixer mixer;
+ struct aki_event_loop loop;
+ struct camu_sink sink;
+ struct aki_socket socket;
+ struct aki_line_processor pro;
+ struct cap_runner cap;
+#if CMV_USE_TUI
+ struct cmv_tui tui;
+ struct aki_timer timer;
+#endif
+};
+
+static struct cmv c;
+static str *default_list = al_str_c("default");
+
+static u8 sink_callback(void *userdata, u8 op, u8 type, void *opaque)
+{
+ struct cmv *c = (struct cmv *)userdata;
+ switch (op) {
+ case CAMU_SINK_ADD_BUFFER:
+ switch (type) {
+ case CAMU_SINK_AUDIO: {
+ struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque;
+ camu_mixer_add_buffer(&c->mixer, buf);
+ al_log_debug("cmv", "Audio buffer added.");
+ break;
+ }
+ case CAMU_SINK_VIDEO: {
+ struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque;
+ camu_screen_add_buffer(&c->scr, buf);
+ al_log_debug("cmv", "Video buffer added.");
+ break;
+ }
+ }
+ break;
+ case CAMU_SINK_REMOVE_BUFFER:
+ switch (type) {
+ case CAMU_SINK_AUDIO: {
+ struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque;
+ camu_mixer_remove_buffer(&c->mixer, buf);
+ al_log_debug("cmv", "Audio buffer removed.");
+ break;
+ }
+ case CAMU_SINK_VIDEO: {
+ struct camu_video_buffer *buf = (struct camu_video_buffer *)opaque;
+ camu_screen_remove_buffer(&c->scr, buf);
+ al_log_debug("cmv", "Video buffer removed.");
+ break;
+ }
+ }
+ break;
+ case CAMU_SINK_SWAP_BUFFER:
+ switch (type) {
+ case CAMU_SINK_AUDIO: {
+ struct camu_audio_buffer *buf = (struct camu_audio_buffer *)opaque;
+ camu_mixer_remove_buffer(&c->mixer, buf);
+ if (cap_list_set_completed(&c->cap, default_list)) {
+ al_log_debug("cmv", "Audio buffers swapped (gapless).");
+ return CAMU_SINK_BUFFERS_SWAPPED;
+ } else {
+ al_log_debug("cmv", "Audio buffer removed.");
+ }
+ break;
+ }
+ }
+ break;
+ case CAMU_SINK_START:
+ switch (type) {
+ case CAMU_SINK_AUDIO: {
+ camu_mixer_resume(&c->mixer);
+ al_log_debug("cmv", "Audio started.");
+ break;
+ }
+ case CAMU_SINK_VIDEO: {
+ camu_screen_set_state(&c->scr, CAMU_SCREEN_PLAYING);
+ camu_screen_wake(&c->scr);
+ al_log_debug("cmv", "Video started.");
+ break;
+ }
+ }
+ break;
+ case CAMU_SINK_STOP:
+ switch (type) {
+ case CAMU_SINK_AUDIO: {
+ camu_mixer_pause(&c->mixer);
+ al_log_debug("cmv", "Audio stopped.");
+ break;
+ }
+ case CAMU_SINK_VIDEO: {
+ camu_screen_set_state(&c->scr, CAMU_SCREEN_PAUSED);
+ al_log_debug("cmv", "Video stopped.");
+ break;
+ }
+ }
+ break;
+ case CAMU_SINK_EXIT:
+ aki_event_loop_break(&c->loop);
+ camu_sink_close(&c->sink);
+ break;
+ }
+ return CAMU_SINK_OK;
+}
+
+static void screen_callback(void *userdata, u8 op, f64 float0)
+{
+ struct cmv *c = (struct cmv *)userdata;
+ switch (op) {
+ case CAMU_SCREEN_CLOSE:
+ c->quit = 1;
+ break;
+ case CAMU_SCREEN_NEXT:
+ cap_list_skip(&c->cap, default_list, 1);
+ break;
+ case CAMU_SCREEN_PREVIOUS:
+ cap_list_skip(&c->cap, default_list, -1);
+ break;
+ case CAMU_SCREEN_TOGGLE_PAUSE:
+ camu_sink_toggle_pause(&c->sink);
+ break;
+ case CAMU_SCREEN_SEEK:
+ camu_sink_seek(&c->sink, float0);
+ break;
+ default:
+ break;
+ }
+}
+
+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;
+ (void)name;
+ switch (op) {
+ case CAP_BUFFER:
+ return camu_sink_local_buffer(&c->sink, unique_id, (struct cch_entry *)opaque);
+ case CAP_SWAP:
+ camu_sink_local_swap(&c->sink, unique_id);
+ break;
+ case CAP_SET:
+ camu_sink_local_set(&c->sink, unique_id);
+ break;
+ case CAP_UNLOAD:
+ camu_sink_local_unload(&c->sink, unique_id);
+ break;
+ }
+ return true;
+}
+
+#ifndef _WIN32
+static u8 line_callback(void *userdata, str *line)
+{
+ struct cmv *c = (struct cmv *)userdata;
+ if (al_str_eq(line, al_str_c(";NEXT"))) {
+ cap_list_skip(&c->cap, default_list, 1);
+ } else if (al_str_eq(line, al_str_c(";PREV"))) {
+ cap_list_skip(&c->cap, default_list, -1);
+ } else {
+ cap_list_add(&c->cap, default_list, line);
+ }
+ return AKI_LINE_PROCESSOR_CONTINUE;
+}
+#endif
+
+#if CMV_USE_TUI
+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->clock, &entry->runner);
+ } else {
+ tui_draw(&c->tui, NULL, NULL);
+ }
+ aki_timer_again(timer);
+ camu_sink_return_current(&c->sink);
+}
+
+static s32 log_callback(void *userdata, char *s)
+{
+ struct cmv *c = (struct cmv *)userdata;
+ tui_push_log_msg(&c->tui, s);
+ return 0;
+}
+
+#ifdef HAVE_FFMPEG
+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);
+}
+#endif
+#endif
+
+s32 main(s32 argc, char *argv[])
+{
+ aki_common_init();
+
+#if CMV_USE_TUI
+ if (!tui_init(&c.tui)) {
+ aki_common_close();
+ return EXIT_FAILURE;
+ }
+ al_set_print(&c, log_callback);
+#ifdef HAVE_FFMPEG
+ camu_lav_set_log_callback(lav_log_callback);
+#endif
+#endif
+
+ 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;
+ }
+ c.renderer = camu_renderer_lp_create();
+ //c.renderer = camu_renderer_tiger_create();
+ camu_screen_create_renderer(&c.scr, c.renderer);
+
+#if CAP_USE_PYTHON
+ bool py_init = sho_python_init();
+#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);
+
+ c.sink.callback = sink_callback;
+ c.sink.userdata = &c;
+ camu_sink_init(&c.sink, &c.loop, &c.mixer, c.renderer);
+
+ 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]));
+ }
+
+#ifndef _WIN32
+ c.socket.type = AKI_SOCKET_UNIX;
+ aki_socket_init(&c.socket);
+ aki_socket_set_blocking(&c.socket, false);
+ aki_socket_set_no_delay(&c.socket, 1);
+ c.pro.callback = line_callback;
+ c.pro.userdata = &c;
+ aki_line_processor_init(&c.pro, al_str_c("\n"));
+ aki_line_processor_open_socket(&c.pro, &c.socket);
+ if (aki_socket_listen(&c.socket, al_str_c("/tmp/cmv_sock"), 0)) {
+ aki_line_processor_run(&c.pro, &c.loop);
+ }
+#endif
+
+#if CMV_USE_TUI
+ aki_timer_init(&c.timer, &c.loop, timer_callback, &c);
+ aki_timer_set_repeat(&c.timer, 0.15);
+ aki_timer_again(&c.timer);
+#endif
+
+ struct aki_thread thread0;
+ aki_thread_create(&thread0, event_loop_thread, &c);
+
+ c.quit = 0;
+
+ while (!c.quit) {
+ if (camu_screen_tick(&c.scr)) {
+ c.renderer->render(c.renderer, &c.scr);
+ }
+ }
+
+ camu_screen_set_state(&c.scr, CAMU_SCREEN_CLOSED);
+
+ camu_sink_stop(&c.sink);
+
+ aki_thread_join(&thread0);
+
+ camu_mixer_close(&c.mixer);
+ camu_screen_close(&c.scr);
+ c.renderer->free(&c.renderer);
+
+ cap_close(&c.cap);
+
+#if CAP_USE_PYTHON
+ if (py_init) sho_python_close();
+#endif
+
+#if CMV_USE_TUI
+ tui_close(&c.tui);
+#endif
+
+ aki_common_close();
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/fruits/cmv/meson.build b/src/fruits/cmv/meson.build
new file mode 100644
index 0000000..b604104
--- /dev/null
+++ b/src/fruits/cmv/meson.build
@@ -0,0 +1,26 @@
+cmv2_src = ['cmv2.c', 'tui.c']
+#cmv2_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'
+]
+
+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])
diff --git a/src/fruits/cmv/tui.c b/src/fruits/cmv/tui.c
new file mode 100644
index 0000000..675d5f4
--- /dev/null
+++ b/src/fruits/cmv/tui.c
@@ -0,0 +1,118 @@
+#include <aki/file.h>
+#include <al/log.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+#include <termios.h>
+
+#include "tui.h"
+
+static struct cmv_tui *tui_global;
+
+static void update_term_size(struct cmv_tui *tui)
+{
+ struct winsize w;
+ ioctl(tui->fd, TIOCGWINSZ, &w);
+ tui->width = w.ws_col;
+ tui->height = w.ws_row;
+}
+
+static void handle_winch(s32 sig)
+{
+ (void)sig;
+ struct cmv_tui *tui = tui_global;
+ aki_mutex_lock(&tui->mutex);
+ tui->update_size = true;
+ aki_mutex_unlock(&tui->mutex);
+}
+
+bool tui_init(struct cmv_tui *tui)
+{
+ tui->update_size = true;
+ tui->width = 0;
+ tui->height = 0;
+ //tui->prev_lines = 0;
+
+ tui->fd = STDIN_FILENO;
+ tui->out = fdopen(tui->fd, "w");
+ if (!tui->out) return false;
+ setbuf(tui->out, NULL);
+
+ tui_global = tui;
+ aki_mutex_init(&tui->mutex);
+ signal(SIGWINCH, handle_winch);
+
+ al_array_init(tui->log_buffer);
+ tui->last_pts = 0.0;
+
+ 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);
+}
+
+static char buffer[128];
+
+static void draw_now_playing(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner)
+{
+ 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);
+ if (pts > 0.0 && duration != 0.0) {
+ if (pts > duration) pts = duration;
+ f64 percent = pts / duration;
+ parts = tui->width * percent;
+ } else if (duration == 0.0) {
+ parts = tui->width;
+ }
+ al_memset(buffer, '-', parts);
+ buffer[parts] = '\0';
+ fprintf(tui->out, "%s", buffer);
+}
+
+void tui_draw(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner)
+{
+ aki_mutex_lock(&tui->mutex);
+
+ if (tui->update_size) {
+ update_term_size(tui);
+ tui->update_size = false;
+ }
+
+ fprintf(tui->out, "\r\033[K");
+ //for (u32 i = 1; i < tui->prev_lines; i++) {
+ // 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);
+ fprintf(tui->out, "%s\n", msg);
+ }
+
+ //tui->prev_lines = 0;
+
+ if (runner) {
+ draw_now_playing(tui, clock, runner);
+ }
+
+ aki_mutex_unlock(&tui->mutex);
+}
+
+void tui_close(struct cmv_tui *tui)
+{
+ char *msg;
+ al_array_foreach(tui->log_buffer, i, msg) {
+ al_free(msg);
+ }
+ al_array_free(tui->log_buffer);
+ aki_mutex_destroy(&tui->mutex);
+ fprintf(tui->out, "\n");
+ fflush(tui->out);
+ fclose(tui->out);
+}
diff --git a/src/fruits/cmv/tui.h b/src/fruits/cmv/tui.h
new file mode 100644
index 0000000..566a5e5
--- /dev/null
+++ b/src/fruits/cmv/tui.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "../../buffer/clock.h"
+#include "../../bimu/local.h"
+
+struct cmv_tui {
+ s32 fd;
+ FILE *out;
+ bool update_size;
+ u32 width;
+ u32 height;
+ //u32 prev_lines;
+ array(char *) log_buffer;
+ f64 last_pts;
+ struct aki_mutex mutex;
+};
+
+bool tui_init(struct cmv_tui *tui);
+void tui_push_log_msg(struct cmv_tui *tui, char *msg);
+void tui_draw(struct cmv_tui *tui, struct camu_clock *clock, struct bmu_local *runner);
+void tui_close(struct cmv_tui *tui);