diff options
Diffstat (limited to 'src/fruits')
| -rw-r--r-- | src/fruits/cap/cap.c | 219 | ||||
| -rw-r--r-- | src/fruits/cap/cap.h | 42 | ||||
| -rw-r--r-- | src/fruits/cap/meson.build | 4 | ||||
| -rw-r--r-- | src/fruits/cmv/cmv2.c | 331 | ||||
| -rw-r--r-- | src/fruits/cmv/meson.build | 26 | ||||
| -rw-r--r-- | src/fruits/cmv/tui.c | 118 | ||||
| -rw-r--r-- | src/fruits/cmv/tui.h | 21 |
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); |