From 42e8c4d28b6fe9cc5e041f02e0c85b390d49a218 Mon Sep 17 00:00:00 2001 From: Andrew Opalach Date: Sat, 19 Oct 2024 12:05:15 -0400 Subject: Attempt to make usable and packageable Signed-off-by: Andrew Opalach --- taro/attic/sway_background_covered.diff | 313 ----- taro/attic/sway_test_config | 9 - taro/config.h | 87 ++ taro/daemon.c | 493 ++++++++ taro/db.c | 209 ++++ taro/db.h | 41 + taro/file_ext.c | 56 + taro/file_ext.h | 7 + taro/meson.build | 27 +- taro/scripts/run_wallpaper.sh | 6 - taro/src/config.h | 69 -- taro/src/daemon.c | 446 ------- taro/src/db.c | 212 ---- taro/src/db.h | 41 - taro/src/file_ext.c | 56 - taro/src/file_ext.h | 7 - taro/src/ui.c | 1203 ------------------- taro/subprojects/libakiyo.wrap | 4 - taro/subprojects/libalabaster.wrap | 4 - taro/subprojects/notcurses.wrap | 12 - .../notcurses/duration_and_ffmpeg_fix.diff | 166 --- taro/subprojects/stela.wrap | 4 - taro/ui.c | 1226 ++++++++++++++++++++ 23 files changed, 2121 insertions(+), 2577 deletions(-) delete mode 100644 taro/attic/sway_background_covered.diff delete mode 100644 taro/attic/sway_test_config create mode 100644 taro/config.h create mode 100644 taro/daemon.c create mode 100644 taro/db.c create mode 100644 taro/db.h create mode 100644 taro/file_ext.c create mode 100644 taro/file_ext.h delete mode 100755 taro/scripts/run_wallpaper.sh delete mode 100644 taro/src/config.h delete mode 100644 taro/src/daemon.c delete mode 100644 taro/src/db.c delete mode 100644 taro/src/db.h delete mode 100644 taro/src/file_ext.c delete mode 100644 taro/src/file_ext.h delete mode 100644 taro/src/ui.c delete mode 100644 taro/subprojects/libakiyo.wrap delete mode 100644 taro/subprojects/libalabaster.wrap delete mode 100644 taro/subprojects/notcurses.wrap delete mode 100644 taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff delete mode 100644 taro/subprojects/stela.wrap create mode 100644 taro/ui.c (limited to 'taro') diff --git a/taro/attic/sway_background_covered.diff b/taro/attic/sway_background_covered.diff deleted file mode 100644 index 0dc9401..0000000 --- a/taro/attic/sway_background_covered.diff +++ /dev/null @@ -1,313 +0,0 @@ -diff --git a/include/ipc.h b/include/ipc.h -index ff011750..e417b421 100644 ---- a/include/ipc.h -+++ b/include/ipc.h -@@ -29,9 +29,10 @@ enum ipc_command_type { - IPC_EVENT_MODE = ((1<<31) | 2), - IPC_EVENT_WINDOW = ((1<<31) | 3), - IPC_EVENT_BARCONFIG_UPDATE = ((1<<31) | 4), -- IPC_EVENT_BINDING = ((1<<31) | 5), -- IPC_EVENT_SHUTDOWN = ((1<<31) | 6), -- IPC_EVENT_TICK = ((1<<31) | 7), -+ IPC_EVENT_BACKGROUND_COVERED = ((1<<31) | 5), -+ IPC_EVENT_BINDING = ((1<<31) | 6), -+ IPC_EVENT_SHUTDOWN = ((1<<31) | 7), -+ IPC_EVENT_TICK = ((1<<31) | 8), - - // sway-specific event types - IPC_EVENT_BAR_STATE_UPDATE = ((1<<31) | 20), -diff --git a/include/sway/ipc-server.h b/include/sway/ipc-server.h -index d4c00942..41e00679 100644 ---- a/include/sway/ipc-server.h -+++ b/include/sway/ipc-server.h -@@ -17,6 +17,7 @@ void ipc_event_workspace(struct sway_workspace *old, - void ipc_event_window(struct sway_container *window, const char *change); - void ipc_event_barconfig_update(struct bar_config *bar); - void ipc_event_bar_state_update(struct bar_config *bar); -+void ipc_event_background_covered(struct sway_output *output, bool covered); - void ipc_event_mode(const char *mode, bool pango); - void ipc_event_shutdown(const char *reason); - void ipc_event_binding(struct sway_binding *binding); -diff --git a/include/sway/output.h b/include/sway/output.h -index 4584ea45..83e3cd7f 100644 ---- a/include/sway/output.h -+++ b/include/sway/output.h -@@ -53,6 +53,8 @@ struct sway_output { - bool enabled; - list_t *workspaces; - -+ bool background_covered; -+ - struct sway_output_state current; - - struct wl_listener layout_destroy; -diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h -index 58bde20c..1fcd5017 100644 ---- a/include/sway/tree/workspace.h -+++ b/include/sway/tree/workspace.h -@@ -40,6 +40,9 @@ struct sway_workspace { - enum sway_container_layout layout; - enum sway_container_layout prev_split_layout; - -+ bool background_covered; -+ bool pending_background_check; -+ - struct side_gaps current_gaps; - int gaps_inner; - struct side_gaps gaps_outer; -@@ -47,6 +50,7 @@ struct sway_workspace { - struct sway_output *output; // NULL if no outputs are connected - list_t *floating; // struct sway_container - list_t *tiling; // struct sway_container -+ list_t *sorted; // struct sway_container - list_t *output_priority; - bool urgent; - -@@ -89,6 +93,8 @@ bool workspace_is_visible(struct sway_workspace *ws); - - bool workspace_is_empty(struct sway_workspace *ws); - -+void workspace_check_background_covered(struct sway_workspace *ws); -+ - void workspace_output_raise_priority(struct sway_workspace *workspace, - struct sway_output *old_output, struct sway_output *new_output); - -diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c -index 8f12832a..9869c934 100644 ---- a/sway/desktop/transaction.c -+++ b/sway/desktop/transaction.c -@@ -702,30 +702,60 @@ static void transaction_apply(struct sway_transaction *transaction) { - "(%.1f frames if 60Hz)", transaction, ms, ms / (1000.0f / 60)); - } - -+ struct sway_transaction_instruction *instruction; -+ struct sway_node *node; -+ struct sway_workspace *ws = NULL; -+ - // Apply the instruction state to the node's current state - for (int i = 0; i < transaction->instructions->length; ++i) { -- struct sway_transaction_instruction *instruction = -- transaction->instructions->items[i]; -- struct sway_node *node = instruction->node; -+ instruction = transaction->instructions->items[i]; -+ node = instruction->node; - - switch (node->type) { - case N_ROOT: -+ ws = NULL; - break; - case N_OUTPUT: - apply_output_state(node->sway_output, &instruction->output_state); -+ ws = node->sway_output->current.active_workspace; - break; - case N_WORKSPACE: - apply_workspace_state(node->sway_workspace, - &instruction->workspace_state); -+ ws = node->sway_workspace; - break; - case N_CONTAINER: - apply_container_state(node->sway_container, - &instruction->container_state); -+ ws = node->sway_container->current.workspace; - break; - } - -+ if (ws) ws->pending_background_check = true; -+ - node->instruction = NULL; - } -+ -+ for (int i = 0; i < transaction->instructions->length; ++i) { -+ instruction = transaction->instructions->items[i]; -+ node = instruction->node; -+ -+ switch (node->type) { -+ case N_ROOT: -+ continue; -+ case N_OUTPUT: -+ ws = node->sway_output->current.active_workspace; -+ break; -+ case N_WORKSPACE: -+ ws = node->sway_workspace; -+ break; -+ case N_CONTAINER: -+ ws = node->sway_container->current.workspace; -+ break; -+ } -+ -+ if (ws) workspace_check_background_covered(ws); -+ } - } - - static void transaction_commit_pending(void); -diff --git a/sway/ipc-json.c b/sway/ipc-json.c -index 571338a4..f9bdcfae 100644 ---- a/sway/ipc-json.c -+++ b/sway/ipc-json.c -@@ -16,6 +16,7 @@ - #include "sway/tree/view.h" - #include "sway/tree/workspace.h" - #include "sway/output.h" -+#include "sway/layers.h" - #include "sway/input/input-manager.h" - #include "sway/input/cursor.h" - #include "sway/input/seat.h" -@@ -360,6 +361,9 @@ static void ipc_json_describe_enabled_output(struct sway_output *output, - json_object_object_add(object, "current_workspace", - json_object_new_string(ws->name)); - -+ json_object_object_add(object, "covered", -+ json_object_new_boolean(ws->background_covered)); -+ - json_object *modes_array = json_object_new_array(); - struct wlr_output_mode *mode; - wl_list_for_each(mode, &wlr_output->modes, link) { -diff --git a/sway/ipc-server.c b/sway/ipc-server.c -index 7f353c0e..f3093505 100644 ---- a/sway/ipc-server.c -+++ b/sway/ipc-server.c -@@ -362,6 +362,20 @@ void ipc_event_bar_state_update(struct bar_config *bar) { - json_object_put(json); - } - -+void ipc_event_background_covered(struct sway_output *output, bool covered) { -+ if (!ipc_has_event_listeners(IPC_EVENT_BACKGROUND_COVERED)) { -+ return; -+ } -+ sway_log(SWAY_DEBUG, "Sending background_covered event"); -+ -+ json_object *json = json_object_new_object(); -+ json_object_object_add(json, "output", json_object_new_string(output->wlr_output->name)); -+ json_object_object_add(json, "covered", json_object_new_boolean(covered)); -+ const char *json_string = json_object_to_json_string(json); -+ ipc_send_event(json_string, IPC_EVENT_BACKGROUND_COVERED); -+ json_object_put(json); -+} -+ - void ipc_event_mode(const char *mode, bool pango) { - if (!ipc_has_event_listeners(IPC_EVENT_MODE)) { - return; -@@ -744,6 +758,8 @@ void ipc_client_handle_command(struct ipc_client *client, uint32_t payload_lengt - client->subscribed_events |= event_mask(IPC_EVENT_BARCONFIG_UPDATE); - } else if (strcmp(event_type, "bar_state_update") == 0) { - client->subscribed_events |= event_mask(IPC_EVENT_BAR_STATE_UPDATE); -+ } else if (strcmp(event_type, "background_covered") == 0) { -+ client->subscribed_events |= event_mask(IPC_EVENT_BACKGROUND_COVERED); - } else if (strcmp(event_type, "mode") == 0) { - client->subscribed_events |= event_mask(IPC_EVENT_MODE); - } else if (strcmp(event_type, "shutdown") == 0) { -diff --git a/sway/tree/output.c b/sway/tree/output.c -index 44b941ca..9004c3cb 100644 ---- a/sway/tree/output.c -+++ b/sway/tree/output.c -@@ -142,6 +142,7 @@ struct sway_output *output_create(struct wlr_output *wlr_output) { - - output->workspaces = create_list(); - output->current.workspaces = create_list(); -+ output->background_covered = false; - - return output; - } -diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c -index f8709a4c..36ea5fc0 100644 ---- a/sway/tree/workspace.c -+++ b/sway/tree/workspace.c -@@ -88,8 +88,12 @@ struct sway_workspace *workspace_create(struct sway_output *output, - ws->layout = output_get_default_layout(output); - ws->floating = create_list(); - ws->tiling = create_list(); -+ ws->sorted = create_list(); - ws->output_priority = create_list(); - -+ ws->background_covered = false; -+ ws->pending_background_check = true; -+ - ws->gaps_outer = config->gaps_outer; - ws->gaps_inner = config->gaps_inner; - if (name) { -@@ -153,6 +157,7 @@ void workspace_destroy(struct sway_workspace *workspace) { - list_free_items_and_destroy(workspace->output_priority); - list_free(workspace->floating); - list_free(workspace->tiling); -+ list_free(workspace->sorted); - list_free(workspace->current.floating); - list_free(workspace->current.tiling); - free(workspace); -@@ -616,6 +621,77 @@ bool workspace_is_empty(struct sway_workspace *ws) { - return true; - } - -+static bool x_covered_at_y(list_t *containers, double y, double x, double width) -+{ -+ double end = x + width; -+ double extent = x; -+ for (int i = 0; i < containers->length; ++i) { -+ struct sway_container *n = containers->items[i]; -+ if (!(n->current.y <= y && n->current.y + n->current.height >= y)) { -+ continue; -+ } -+ if (n->current.x > extent) return false; -+ x = n->current.x + n->current.width; -+ if (x >= end) return true; -+ if (x > extent) extent = x; -+ } -+ return false; -+} -+ -+static int sort_containers_x_cmp(const void *_a, const void *_b) { -+ struct sway_container *a = *(void **)_a; -+ struct sway_container *b = *(void **)_b; -+ return a->current.x - b->current.x; -+} -+ -+#define TERMINAL_APP_ID "kitty" -+ -+static void add_con_and_or_children_to_list(list_t *result, list_t *list) -+{ -+ struct sway_container *con; -+ for (int i = 0; i < list->length; ++i) { -+ con = list->items[i]; -+ if (con->current.children) { -+ add_con_and_or_children_to_list(result, con->current.children); -+ if (con->current.layout != L_NONE) return; -+ } -+ if (!con->view || !con->view->wlr_xdg_toplevel) continue; // Hack. -+ const char *app_id = view_get_app_id(con->view); -+ if (app_id == NULL || strcmp(app_id, TERMINAL_APP_ID) != 0) { -+ list_add(result, con); -+ } -+ } -+} -+ -+void workspace_check_background_covered(struct sway_workspace *ws) { -+ if (!ws->output || !ws->pending_background_check) return; -+ ws->sorted->length = 0; -+ add_con_and_or_children_to_list(ws->sorted, ws->tiling); -+ add_con_and_or_children_to_list(ws->sorted, ws->floating); -+ list_stable_sort(ws->sorted, sort_containers_x_cmp); -+ ws->background_covered = ws->sorted->length > 0; -+ struct sway_container *con; -+ for (int i = 0; i < ws->sorted->length; ++i) { -+ con = ws->sorted->items[i]; -+ double y = con->current.y - 1.0; -+ if (y < ws->y) y = ws->y; -+ double span = con->current.y + con->current.height + 1.0; -+ if (span > ws->y + ws->height) span = ws->y + ws->height; -+ bool top = x_covered_at_y(ws->sorted, y, ws->x, ws->width); -+ if (!top || !x_covered_at_y(ws->sorted, span, ws->x, ws->width)) { -+ ws->background_covered = false; -+ break; -+ } -+ } -+ if (ws == ws->output->current.active_workspace) { -+ if (ws->background_covered != ws->output->background_covered) { -+ ws->output->background_covered = ws->background_covered; -+ ipc_event_background_covered(ws->output, ws->output->background_covered); -+ } -+ ws->pending_background_check = false; -+ } -+} -+ - static int find_output(const void *id1, const void *id2) { - return strcmp(id1, id2); - } diff --git a/taro/attic/sway_test_config b/taro/attic/sway_test_config deleted file mode 100644 index 1223428..0000000 --- a/taro/attic/sway_test_config +++ /dev/null @@ -1,9 +0,0 @@ -workspace "1" output "WL-1" -workspace "2" output "WL-1" -bindsym Mod4+Return exec kitty -bindsym Mod4+Shift+q kill -bindsym Mod2+1 workspace 1 -bindsym Mod2+2 workspace 2 -bindsym Mod4+v split v -floating_modifier Mod4 -bindsym Mod4+space floating toggle diff --git a/taro/config.h b/taro/config.h new file mode 100644 index 0000000..1a1f10d --- /dev/null +++ b/taro/config.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include "file_ext.h" + +#define CONFIG_PATH "/.config/taro" + +struct config { + str config_dir; + str wallpaper_dir; +}; + +AL_UNUSED_FUNCTION_PUSH + +static void close_config(struct config *conf) +{ + al_str_free(&conf->config_dir); + al_str_free(&conf->wallpaper_dir); +} + +static bool parse_config_json(struct config *conf, char *envhome, json_t *root) +{ + bool valid = true; + + json_t *wallpaper_dir = json_object_get(root, "wallpaper_directory"); + if (!wallpaper_dir || !json_is_string(wallpaper_dir) || al_strlen(json_string_value(wallpaper_dir)) == 0) { + al_log_error("taro", "'wallpaper_directory' not set or invalid"); + valid = false; + goto out; + } + + const char *dir_str = json_string_value(wallpaper_dir); + if (dir_str[0] == '~') { + al_str_from(&conf->wallpaper_dir, envhome); + dir_str++; + al_str_cat(&conf->wallpaper_dir, al_str_cr(dir_str)); + } else { + al_str_from(&conf->wallpaper_dir, dir_str); + } + +out: + json_decref(root); + + return valid; +} + +static bool open_config(struct config *conf, bool create) +{ + al_memset(conf, 0, sizeof(struct config)); + + char *envhome = getenv("HOME"); + if (!envhome) goto err; + + al_str_from(&conf->config_dir, envhome); + al_str_cat(&conf->config_dir, al_str_c(CONFIG_PATH)); + if (!aki_dir_exists(&conf->config_dir)) { + if (!create || !aki_dir_create(&conf->config_dir)) { + goto err; + } + } + + str config_path; + al_str_clone(&config_path, &conf->config_dir); + al_str_cat(&config_path, al_str_c("/config.json")); + if (!aki_file_exists(&config_path)) { + if (aki_file_create(&config_path)) { + json_t *blank = json_object(); + json_object_set_new(blank, "wallpaper_directory", json_string_nocheck("")); + aki_dump_json_and_decref(&config_path, blank); + } + } + + json_t *root = aki_file_open_as_json(&config_path); + al_str_free(&config_path); + if (!root) goto err; + + if (!parse_config_json(conf, envhome, root)) goto err; + + return true; +err: + close_config(conf); + return false; +} + +AL_UNUSED_FUNCTION_POP diff --git a/taro/daemon.c b/taro/daemon.c new file mode 100644 index 0000000..d632e3d --- /dev/null +++ b/taro/daemon.c @@ -0,0 +1,493 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "file_ext.h" +#include "config.h" + +struct monitor { + struct stl_monitor *os; + struct aki_fs_event fs_event; + bool self_event_skip; + str config_dir; + str selection_path; + pid_t pid; + bool paused; + bool covered; + struct daemon *dmon; +}; + +#define SWAY_IPC_HEADER_LENGTH 14 +#define SWAY_IPC_SUBSCRIBE 2 + +struct daemon { + struct aki_event_loop loop; + struct aki_signal quit_signal; + struct aki_poll context_poll; + array(struct monitor *) monitors; + struct aki_socket ipc_socket; + bool connected; + struct aki_poll ipc_poll; + struct { + u8 *buf; + ssize_t index; + ssize_t size; + } out; + struct { + u8 *buf; + ssize_t index; + ssize_t size; + ssize_t alloc; + } ipc; + struct config conf; +}; + +static s32 parse_and_set_props(str *selection, str *wallpaper_path, bool *is_package) +{ + s32 index = al_str_find(selection, '|'); + s32 rindex = al_str_rfind(selection, '|'); + if (index == -1 || rindex == -1 || index == rindex) { + return -1; + } + + // index < 2 or id->len == 0 is expected behavior + // when a monitor has no wallpaper set. + if (index < 2) return 1; + str *id = al_str_substr(selection, 0, index - 2); + str *type = al_str_substr(selection, index - 2, index); + if (id->len == 0) return 1; + + al_str_cat(wallpaper_path, id); + + if (al_str_eq(type, al_str_c("_p"))) { + *is_package = true; + } else if (al_str_eq(type, al_str_c("_d"))) { + *is_package = false; + } else { + return -1; + } + + return 0; +} + +static bool send_signal_to_pid(pid_t pid, s32 signal) +{ + if (kill(pid, signal) != 0) { + al_log_error("tarod", "kill(%d) failed (%s)", pid, aki_strerror(errno)); + return false; + } + return true; +} + +static bool signal_monitor(struct monitor *monitor, s32 signal) +{ + if (monitor->pid == -1) return false; + if (!send_signal_to_pid(monitor->pid, signal)) { + monitor->pid = -1; + return false; + } + return true; +} + +static pid_t exec_or_exit(char *args[]) +{ + pid_t pid = fork(); + if (pid == -1) { + al_log_error("tarod", "fatal: failed to fork process, exiting..."); + exit(EXIT_FAILURE); + } else if (pid == 0) { + if (execv(args[0], args) == -1) { + al_log_error("tarod", "fatal: failed to run '%s', exiting...", args[0]); + // This should return the pid of init if the parent process has already exited. + pid_t ppid = getppid(); + if (ppid != 1) send_signal_to_pid(ppid, SIGINT); + exit(EXIT_FAILURE); + } + } + return pid; +} + +static void maybe_kill_wallpaper(struct monitor *monitor) +{ + if (monitor->pid == -1) return; + + if (monitor->paused) { + signal_monitor(monitor, SIGCONT); + monitor->paused = false; + } + + signal_monitor(monitor, SIGTERM); + monitor->pid = -1; +} + +static bool run_wallpaper(struct daemon *dmon, struct monitor *monitor) +{ + maybe_kill_wallpaper(monitor); + + str selection; + monitor->self_event_skip = true; + if (!aki_file_open_and_read_wholly(&monitor->selection_path, &selection, true)) { + monitor->self_event_skip = false; + return false; + } + + if (selection.len == 0) { + al_log_error("tarod", "config for %s is empty", monitor->os->name); + al_str_free(&selection); + return false; + } + + str wallpaper_path; + al_str_clone(&wallpaper_path, &dmon->conf.wallpaper_dir); + al_str_cat(&wallpaper_path, al_str_c("/")); + bool is_package; + s32 ret = parse_and_set_props(&selection, &wallpaper_path, &is_package); + al_str_free(&selection); + if (ret != 0) { + if (ret == -1) { + al_log_error("tarod", "failed to set properties for wallpaper on %s", monitor->os->name); + } + al_str_free(&wallpaper_path); + return false; + } + + if (is_package) { + al_str_cat(&wallpaper_path, al_str_c("/scene.pkg")); + } + + al_log_info("tarod", "setting wallpaper %.*s on %s", AL_STR_PRINTF(&wallpaper_path), monitor->os->name); + + char width[8], height[8]; + al_snprintf(width, sizeof(width), "%d", monitor->os->width / monitor->os->scale); + al_snprintf(height, sizeof(height), "%d", monitor->os->height / monitor->os->scale); + + char *c_str0 = al_str_to_c_str(&wallpaper_path); + al_str_free(&wallpaper_path); + if (is_package) { + monitor->pid = exec_or_exit((char *[]){ + "mauri", "-p", c_str0, "-m", monitor->os->name, "-w", width, "-h", height, NULL}); + } else { + monitor->pid = exec_or_exit((char *[]){ + "mauri", "-d", c_str0, "-m", monitor->os->name, "-w", width, "-h", height, NULL}); + } + al_free(c_str0); + + if (monitor->covered && signal_monitor(monitor, SIGSTOP)) { + monitor->paused = true; + } + + return true; +} + +static void selection_fs_event_callback(void *userdata, struct inotify_event *event) +{ + struct monitor *monitor = (struct monitor *)userdata; + (void)event; + if (monitor->self_event_skip) { + monitor->self_event_skip = false; + return; + } + struct daemon *dmon = monitor->dmon; + run_wallpaper(dmon, monitor); +} + +static void output_discovered_callback(void *userdata, struct stl_monitor *os_monitor) +{ + struct daemon *dmon = (struct daemon *)userdata; + + struct monitor *monitor; + al_array_foreach(dmon->monitors, i, monitor) { + if (strcmp(monitor->os->name, os_monitor->name) == 0) { + if (monitor->pid == -1) { + run_wallpaper(dmon, monitor); + } + return; + } + } + + str monitor_dir; + al_str_clone(&monitor_dir, &dmon->conf.config_dir); + al_str_cat(&monitor_dir, al_str_c("/")); + al_str_cat(&monitor_dir, al_str_cr(os_monitor->name)); + + if (!aki_dir_exists(&monitor_dir)) { + if (!aki_dir_create(&monitor_dir)) return; + } + + monitor = al_alloc_object(struct monitor); + monitor->os = os_monitor; + monitor->dmon = dmon; + al_array_push(dmon->monitors, monitor); + + al_str_clone(&monitor->config_dir, &monitor_dir); + al_str_free(&monitor_dir); + + al_str_clone(&monitor->selection_path, &monitor->config_dir); + al_str_cat(&monitor->selection_path, al_str_c("/selection")); + + if (!aki_file_exists(&monitor->selection_path)) { + aki_file_create(&monitor->selection_path); + aki_file_open_and_replace(&monitor->selection_path, al_str_c("||"), false); + } + + monitor->pid = -1; + monitor->paused = false; + monitor->covered = false; + + run_wallpaper(dmon, monitor); + + monitor->self_event_skip = false; + aki_fs_event_init(&monitor->fs_event, &monitor->selection_path, AKI_CLOSE_WRITE, + selection_fs_event_callback, monitor); + aki_fs_event_start(&monitor->fs_event, &monitor->dmon->loop); +} + +static void close_monitor(struct monitor *monitor) +{ + al_str_free(&monitor->config_dir); + al_str_free(&monitor->selection_path); + aki_fs_event_stop(&monitor->fs_event); + aki_fs_event_free(&monitor->fs_event); +} + +static void ipc_handle_subscribe_success(json_t *root) +{ + if (json_boolean_value(root)) { + al_log_info("tarod", "successfully subscribed to sway background covered events"); + } else { + al_log_info("tarod", "failed to subscribe to sway background covered events"); + } +} + +static void ipc_handle_response(struct daemon *dmon, u8 *buf, u32 size) +{ + al_log_debug("tarod", "IN: %.*s", size, (char *)buf); + + json_error_t error; + json_t *root = json_loadb((char *)buf, size, 0, &error); + if (!root) { + al_log_error("tarod", "failed to parse sway-ipc json payload (%s)", error.text); + goto done; + } + + json_t *success = json_object_get(root, "success"); + if (success) { + ipc_handle_subscribe_success(success); + goto done; + } + + json_t *output = json_object_get(root, "output"); + if (output) { + bool covered = json_boolean_value(json_object_get(root, "covered")); + struct monitor *monitor; + al_array_foreach(dmon->monitors, i, monitor) { + if (strcmp(monitor->os->name, json_string_value(output)) != 0) continue; + monitor->covered = covered; + if (monitor->pid == -1) goto done; + if (covered && !monitor->paused && signal_monitor(monitor, SIGSTOP)) { + monitor->paused = true; + } else if (!covered && monitor->paused && signal_monitor(monitor, SIGCONT)) { + monitor->paused = false; + } + break; + } + } + +done: + if (root) json_decref(root); +} + +static void ensure_ipc_buf_size(struct daemon *dmon, ssize_t size) +{ + if (dmon->ipc.alloc < size) { + dmon->ipc.buf = al_realloc(dmon->ipc.buf, size); + dmon->ipc.alloc = size; + } +} + +static void ipc_poll_callback(void *userdata, s32 revents) +{ + struct daemon *dmon = (struct daemon *)userdata; + + if (revents & AKI_POLL_WRITE) { + if (!dmon->connected) { + al_log_info("tarod", "connected to sway ipc socket"); + dmon->connected = true; + } + if (!dmon->out.buf) return; + dmon->out.index += aki_socket_write(&dmon->ipc_socket, dmon->out.buf + dmon->out.index, + dmon->out.size - dmon->out.index); + if (dmon->out.index == dmon->out.size) { + al_free(dmon->out.buf); + dmon->out.buf = NULL; + aki_poll_stop(&dmon->ipc_poll); + aki_poll_set(&dmon->ipc_poll, aki_socket_get_fd(&dmon->ipc_socket), AKI_POLL_READ); + aki_poll_start(&dmon->ipc_poll, &dmon->loop); + } + } + + if (revents & AKI_POLL_READ) { + if (dmon->ipc.index < SWAY_IPC_HEADER_LENGTH) { + dmon->ipc.index += aki_socket_read(&dmon->ipc_socket, dmon->ipc.buf + dmon->ipc.index, + SWAY_IPC_HEADER_LENGTH - dmon->ipc.index); + } + if (dmon->ipc.index == SWAY_IPC_HEADER_LENGTH && !dmon->ipc.size) { + dmon->ipc.size = *((s32 *)&dmon->ipc.buf[6]) + SWAY_IPC_HEADER_LENGTH; + ensure_ipc_buf_size(dmon, dmon->ipc.size); + } + if (dmon->ipc.size) { + dmon->ipc.index += aki_socket_read(&dmon->ipc_socket, dmon->ipc.buf + dmon->ipc.index, + dmon->ipc.size - dmon->ipc.index); + if (dmon->ipc.index == dmon->ipc.size) { + ipc_handle_response(dmon, dmon->ipc.buf + SWAY_IPC_HEADER_LENGTH, + dmon->ipc.size - SWAY_IPC_HEADER_LENGTH); + dmon->ipc.size = 0; + dmon->ipc.index = 0; + } + } + } +} + +static void context_poll_callback(void *userdata, s32 revents) +{ + (void)userdata; + al_assert(revents & AKI_POLL_READ); + stl_global_process_events(); +} + +static void quit_signal_callback(void *userdata) +{ + struct daemon *dmon = (struct daemon *)userdata; + aki_event_loop_break_one(&dmon->loop); +} + +static struct daemon dmon = { 0 }; + +// https://stackoverflow.com/a/59071370 +static void sigchld_handler(s32 signum) +{ + (void)signum; + s32 saved_errno = errno; + while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) {} + errno = saved_errno; +} + +static void sigint_handler(s32 signum) +{ + (void)signum; + aki_signal_send(&dmon.quit_signal); +} + +// TODO: +// - daemon quit +// - systemd? and related testing + +s32 main(void) +{ + if (!aki_common_init() || !open_config(&dmon.conf, true)) return EXIT_FAILURE; + + signal(SIGINT, sigint_handler); + signal(SIGCHLD, sigchld_handler); + + s32 ret = EXIT_FAILURE; + + aki_event_loop_init(&dmon.loop); + + aki_signal_init(&dmon.quit_signal, quit_signal_callback, &dmon); + aki_signal_start(&dmon.quit_signal, &dmon.loop); + + al_array_init(dmon.monitors); + stl_set_output_discovered_callback(output_discovered_callback, &dmon); + if (!stl_global_init(true)) goto out; + s32 poll_fd = stl_global_get_poll_fd(); + if (poll_fd != -1) { + aki_poll_init(&dmon.context_poll, context_poll_callback, &dmon); + aki_poll_set(&dmon.context_poll, poll_fd, AKI_POLL_READ); + aki_poll_start(&dmon.context_poll, &dmon.loop); + } + + str monitors; + al_str_from(&monitors, ""); + struct monitor *monitor; + al_array_foreach(dmon.monitors, i, monitor) { + al_str_cat(&monitors, al_str_cr(monitor->os->name)); + al_str_cat(&monitors, al_str_c("\n")); + } + + str monitors_path; + al_str_clone(&monitors_path, &dmon.conf.config_dir); + al_str_cat(&monitors_path, al_str_c("/monitors")); + bool monitors_exists = aki_file_exists(&monitors_path) || aki_file_create(&monitors_path); + if (monitors_exists) { + aki_file_open_and_replace(&monitors_path, &monitors, false); + } + al_str_free(&monitors_path); + al_str_free(&monitors); + if (!monitors_exists) goto out; + + dmon.ipc.buf = NULL; + dmon.ipc.size = 0; + dmon.ipc.index = 0; + dmon.ipc.alloc = 0; + ensure_ipc_buf_size(&dmon, SWAY_IPC_HEADER_LENGTH); + + size_t bufsize = SWAY_IPC_HEADER_LENGTH + 64; + dmon.out.buf = al_malloc(bufsize); + al_memcpy(dmon.out.buf, "i3-ipc", al_strlen("i3-ipc")); + s32 type = SWAY_IPC_SUBSCRIBE; + al_memcpy(&dmon.out.buf[10], &type, sizeof(s32)); + + json_t *payload = json_array(); + json_array_append_new(payload, json_string_nocheck("background_covered")); + s32 len = json_dumpb(payload, (char *)&dmon.out.buf[SWAY_IPC_HEADER_LENGTH], + bufsize - SWAY_IPC_HEADER_LENGTH, 0); + json_decref(payload); + al_memcpy(&dmon.out.buf[6], &len, sizeof(s32)); + + dmon.out.size = SWAY_IPC_HEADER_LENGTH + len; + dmon.out.index = 0; + + dmon.ipc_socket.type = AKI_SOCKET_UNIX; + aki_socket_init(&dmon.ipc_socket); + aki_socket_set_blocking(&dmon.ipc_socket, false); + aki_poll_init(&dmon.ipc_poll, ipc_poll_callback, &dmon); + aki_poll_set(&dmon.ipc_poll, aki_socket_get_fd(&dmon.ipc_socket), AKI_POLL_WRITE); + dmon.connected = false; + char *swaysock = getenv("SWAYSOCK"); + if (swaysock && aki_socket_connect(&dmon.ipc_socket, al_str_cr(swaysock), 0)) { + aki_poll_start(&dmon.ipc_poll, &dmon.loop); + } else { + al_log_warn("tarod", "couldn't connect to sway ipc socket"); + } + + aki_event_loop_run(&dmon.loop); + + al_array_foreach(dmon.monitors, i, monitor) { + close_monitor(monitor); + } + al_array_free(dmon.monitors); + + al_free(dmon.ipc.buf); + aki_socket_close(&dmon.ipc_socket); + + aki_event_loop_destroy(&dmon.loop); + + ret = EXIT_SUCCESS; + +out: + close_config(&dmon.conf); + + stl_global_close(); + aki_common_close(); + + return ret; +} diff --git a/taro/db.c b/taro/db.c new file mode 100644 index 0000000..5202df8 --- /dev/null +++ b/taro/db.c @@ -0,0 +1,209 @@ +#include + +#include "db.h" +#include "file_ext.h" + +#define REAL_OR_INT(v) \ + json_typeof(v) == JSON_REAL ? json_real_value(v) : json_integer_value(v) + +static bool load_entry(struct entry *e) +{ + struct aki_dir dir; + if (!aki_dir_open(&dir, &e->path)) return false; + + struct aki_dir_entry entry; + while (aki_dir_read(&dir, &entry)) { + if (entry.entry->d_type != DT_REG) { + continue; + } + + s32 index = al_str_rfind(al_str_cr(entry.entry->d_name), '.'); + if (index == -1) { + continue; + } + + if (al_str_eq(al_str_w(entry.entry->d_name, 0, index), al_str_c("project"))) { + str package_path; + al_str_clone(&package_path, &e->path); + al_str_cat(&package_path, al_str_c("/scene.pkg")); + e->has_package = aki_file_exists(&package_path); + al_str_free(&package_path); + + str project_path; + al_str_clone(&project_path, &e->path); + al_str_cat(&project_path, al_str_c("/")); + al_str_cat(&project_path, al_str_cr(entry.entry->d_name)); + + json_t *root = aki_file_open_as_json(&project_path); + al_str_free(&project_path); + if (!root) continue; + e->loaded = true; + + json_t *title = json_object_get(root, "title"); + if (title) { + al_wstr_from_cstr(&e->title, (char *)json_string_value(title)); + } + + json_t *preview = json_object_get(root, "preview"); + if (preview) { + al_str_clone(&e->preview_path, &e->path); + al_str_cat(&e->preview_path, al_str_c("/")); + al_str_cat(&e->preview_path, al_str_cr(json_string_value(preview))); + e->has_preview = aki_file_exists(&e->preview_path); + } + + json_t *general = json_object_get(root, "general"); + if (!general) { + continue; + } + + al_array_init(e->props); + json_t *properties = json_object_get(general, "properties"); + if (properties) { + void *iter = json_object_iter(properties); + json_t *iter_obj; + json_t *text, *type; + while (iter) { + iter_obj = json_object_iter_value(iter); + text = json_object_get(iter_obj, "text"); + if (!text || strcmp(json_string_value(text), "ui_browse_properties_scheme_color") == 0) { + iter = json_object_iter_next(properties, iter); + continue; + } + + struct prop p; + p.type = PROP_TYPE_UNKNOWN; + al_wstr_from_cstr(&p.title, (char *)json_string_value(text)); + type = json_object_get(iter_obj, "type"); + if (!type) { + // Silently ignore for now. Is "Notes" a special case? see 1918273588. + //log_warn("improperly handled prop (%.*s)", AL_STR_PRINTF(&e->id)); + iter = json_object_iter_next(properties, iter); + continue; + } + + str *type_str = al_str_cr(json_string_value(type)); + if (al_str_eq(type_str, al_str_c("bool"))) { + json_t *value = json_object_get(iter_obj, "value"); + if (!value) { + al_log_warn("db", "improperly handled bool (%.*s)", AL_STR_PRINTF(&e->id)); + iter = json_object_iter_next(properties, iter); + continue; + } + p.value = al_malloc(sizeof(bool)); + *((bool *)p.value) = json_boolean_value(value); + p.type = PROP_TYPE_BOOL; + } else if (al_str_eq(type_str, al_str_c("slider"))) { + json_t *value = json_object_get(iter_obj, "value"); + json_t *min = json_object_get(iter_obj, "min"); + json_t *max = json_object_get(iter_obj, "max"); + if (!value || !min || !max) { + al_log_warn("db", "improperly handled slider (%.*s)", AL_STR_PRINTF(&e->id)); + iter = json_object_iter_next(properties, iter); + continue; + } + json_t *step = json_object_get(iter_obj, "step"); + p.value = al_malloc(sizeof(struct slider_value)); + struct slider_value *slider = (struct slider_value *)p.value; + slider->value = REAL_OR_INT(value); + slider->min = REAL_OR_INT(min); + slider->max = REAL_OR_INT(max); + if (step) { + slider->step = REAL_OR_INT(step); + } else { + // Assume the default step is 1.0. + slider->step = 1.0; + } + p.type = PROP_TYPE_SLIDER; + } else { + //al_log_warn("db", "unhandled type. (%.*s)", AL_STR_PRINTF(type_str)); + } + + al_array_push(e->props, p); + + iter = json_object_iter_next(properties, iter); + } + } + } + } + + aki_dir_close(&dir); + + return e->loaded; +} + +bool db_ensure_entry_loaded(struct db *db, struct entry *e) +{ + if (!e->loaded && !load_entry(e)) { + // Remove entry if it cannot be loaded. + struct entry *re; + al_array_foreach_ptr(db->entries, i, re) { + if (re == e) { + al_array_remove_at(db->entries, i); + break; + } + } + } + return e->loaded; +} + +static s32 wallpaper_id_compare(const void *a, const void *b) +{ + // Non-integer ids will be at the end. + s64 aid = al_str_to_long(&((struct entry *)a)->id, 10); + s64 bid = al_str_to_long(&((struct entry *)b)->id, 10); + if (aid > bid) return 1; + else if (aid < bid) return -1; + return 0; +} + +bool db_load_entries(struct db *db, str *path) +{ + struct aki_dir dir; + if (!aki_dir_open(&dir, path)) return false; + + al_array_init(db->entries); + struct aki_dir_entry entry; + while (aki_dir_read(&dir, &entry)) { + if (!(entry.entry->d_type == DT_DIR || entry.entry->d_type == DT_UNKNOWN)) { + continue; + } + if (strcmp(entry.entry->d_name, ".") == 0 || + strcmp(entry.entry->d_name, "..") == 0) { + continue; + } + struct entry e = { 0 }; + e.loaded = false; + al_str_clone(&e.id, al_str_cr(entry.entry->d_name)); + al_str_clone(&e.path, path); + al_str_cat(&e.path, al_str_c("/")); + al_str_cat(&e.path, &e.id); + if (!aki_dir_exists(&e.path)) { + al_str_free(&e.id); + al_str_free(&e.path); + continue; + } + al_array_push(db->entries, e); + } + + al_array_sort(db->entries, struct entry, wallpaper_id_compare); + + return true; +} + +void db_unload(struct db *db) +{ + struct entry *e; + al_array_foreach_ptr(db->entries, i, e) { + al_str_free(&e->path); + al_str_free(&e->preview_path); + al_wstr_free(&e->title); + struct prop *p; + al_array_foreach_ptr(e->props, j, p) { + al_wstr_free(&p->title); + if (p->type != PROP_TYPE_UNKNOWN) { + al_free(p->value); + } + } + } +} diff --git a/taro/db.h b/taro/db.h new file mode 100644 index 0000000..78f4c77 --- /dev/null +++ b/taro/db.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +enum { + PROP_TYPE_BOOL, + PROP_TYPE_SLIDER, + PROP_TYPE_UNKNOWN +}; + +struct slider_value { + f64 min, max; + f64 step; + f64 value; +}; + +struct prop { + u8 type; + wstr title; + void *value; +}; + +struct entry { + bool loaded; + str id; + str path; + bool has_package; + str preview_path; + bool has_preview; + wstr title; + array(struct prop) props; +}; + +struct db { + array(struct entry) entries; +}; + +bool db_load_entries(struct db *db, str *path); +bool db_ensure_entry_loaded(struct db *db, struct entry *e); +void db_unload(struct db *db); diff --git a/taro/file_ext.c b/taro/file_ext.c new file mode 100644 index 0000000..3091fa4 --- /dev/null +++ b/taro/file_ext.c @@ -0,0 +1,56 @@ +#include + +#include "file_ext.h" + +bool aki_file_open_and_read_wholly(str *path, str *out, bool lock) +{ + struct aki_file file; + if (!aki_file_open(&file, path, lock ? AKI_FILE_LOCK : AKI_FILE_READONLY)) { + return false; + } + s32 ret = aki_file_read_as_str(&file, out); + aki_file_close(&file); + return ret != -1; +} + +bool aki_file_open_and_replace(str *path, str *buf, bool lock) +{ + struct aki_file file; + if (!aki_file_open(&file, path, lock ? AKI_FILE_LOCK : 0)) { + return false; + } + s32 ret = aki_file_replace(&file, buf); + aki_file_close(&file); + return ret != -1; +} + +json_t *aki_file_open_as_json(str *path) +{ + str s; + if (!aki_file_open_and_read_wholly(path, &s, false)) { + return NULL; + } + json_error_t error; + json_t *root = json_loadb(s.data, s.len, 0, &error); + if (!root) { + al_log_error("file_ext", "json_loadb(%.*s, %u) failed (%s)", AL_STR_PRINTF(path), s.len, error.text); + } + al_str_free(&s); + return root; +} + +bool aki_dump_json_and_decref(str *path, json_t *root) +{ + str s; + size_t size = json_dumpb(root, NULL, 0, JSON_INDENT(4)); + al_str_sized(&s, size); + s.len = json_dumpb(root, s.data, size, JSON_INDENT(4)); + if (s.len == 0) { + json_decref(root); + return false; + } + bool ret = aki_file_open_and_replace(path, &s, false); + al_str_free(&s); + json_decref(root); + return ret; +} diff --git a/taro/file_ext.h b/taro/file_ext.h new file mode 100644 index 0000000..9398eb3 --- /dev/null +++ b/taro/file_ext.h @@ -0,0 +1,7 @@ +#include +#include + +bool aki_file_open_and_read_wholly(str *path, str *out, bool lock); +bool aki_file_open_and_replace(str *path, str *buf, bool lock); +json_t *aki_file_open_as_json(str *path); +bool aki_dump_json_and_decref(str *path, json_t *root); diff --git a/taro/meson.build b/taro/meson.build index 0291648..935678e 100644 --- a/taro/meson.build +++ b/taro/meson.build @@ -1,10 +1,3 @@ -project('taro', 'c', version: '0.01', - default_options: ['warning_level=2', 'c_std=c99']) - -cmake = import('cmake') - -is_debug = get_option('buildtype').startswith('debug') - notcurses = dependency('notcurses', required: false, allow_fallback: false) notcurses_core = dependency('notcurses-core', required: false, allow_fallback: false) if not notcurses.found() or not notcurses_core.found() @@ -19,27 +12,11 @@ if not notcurses.found() or not notcurses_core.found() notcurses_core = notcurses_proj.dependency('notcurses-core') endif -akiyo = subproject('libakiyo', - default_options: [ - 'curl=disabled', - 'json=enabled', - 'event-loop=enabled' - ]).get_variable('akiyo') - -stela = subproject('stela', - default_options: [ - 'api=null', - 'window=wayland', - 'poll=fd', - 'event-buffer=false', - 'pause=false' - ]).get_variable('stela') - taro_deps = [akiyo, notcurses, notcurses_core] -taro_src = ['src/ui.c', 'src/db.c', 'src/file_ext.c'] +taro_src = ['ui.c', 'db.c', 'file_ext.c'] tarod_deps = [akiyo, stela] -tarod_src = ['src/daemon.c', 'src/file_ext.c'] +tarod_src = ['daemon.c', 'file_ext.c'] executable('taro', taro_src, dependencies: taro_deps, install: true) executable('tarod', tarod_src, dependencies: tarod_deps, install: true) diff --git a/taro/scripts/run_wallpaper.sh b/taro/scripts/run_wallpaper.sh deleted file mode 100755 index a274889..0000000 --- a/taro/scripts/run_wallpaper.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /usr/bin/env sh -if [ "$1" = "true" ]; then - exec mauri -p $2/scene.pkg -m $3 -w $4 -h $5 -else - exec mauri -d $2 -m $3 -w $4 -h $5 -fi diff --git a/taro/src/config.h b/taro/src/config.h deleted file mode 100644 index ddf3430..0000000 --- a/taro/src/config.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include - -#include "file_ext.h" - -#define CONFIG_PATH "/.config/taro" - -struct config { - str config_dir; - str wallpaper_dir; -}; - -AL_UNUSED_FUNCTION_PUSH - -static void close_config(struct config *conf) -{ - al_str_free(&conf->config_dir); - al_str_free(&conf->wallpaper_dir); -} - -static bool open_config(struct config *conf) -{ - conf->config_dir = al_str_zero(); - conf->wallpaper_dir = al_str_zero(); - - char *envhome = getenv("HOME"); - if (!envhome) return false; - al_str_from(&conf->config_dir, envhome); - al_str_cat(&conf->config_dir, al_str_c(CONFIG_PATH)); - if (!aki_dir_exists(&conf->config_dir)) { - if (!aki_dir_create(&conf->config_dir)) { - close_config(conf); - return false; - } - } - - str config_path; - al_str_clone(&config_path, &conf->config_dir); - al_str_cat(&config_path, al_str_c("/config.json")); - json_t *root = aki_file_open_as_json(&config_path); - al_str_free(&config_path); - if (!root) { - close_config(conf); - return false; - } - - json_t *wallpaper_dir = json_object_get(root, "wallpaper_directory"); - if (!wallpaper_dir) { - json_decref(root); - close_config(conf); - return false; - } - - const char *dir_str = json_string_value(wallpaper_dir); - if (dir_str[0] == '~') { - al_str_from(&conf->wallpaper_dir, envhome); - dir_str++; - al_str_cat(&conf->wallpaper_dir, al_str_cr(dir_str)); - } else { - al_str_from(&conf->wallpaper_dir, dir_str); - } - - json_decref(root); - - return true; -} - -AL_UNUSED_FUNCTION_POP diff --git a/taro/src/daemon.c b/taro/src/daemon.c deleted file mode 100644 index 45eb599..0000000 --- a/taro/src/daemon.c +++ /dev/null @@ -1,446 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "file_ext.h" -#include "config.h" - -struct monitor { - struct stl_monitor *os; - struct aki_fs_event fs_event; - bool self_event_skip; - str config_dir; - str selection_path; - pid_t pid; - bool paused; - bool covered; - struct daemon *dmon; -}; - -#define IPC_HEADER_LENGTH 14 -#define IPC_MESSAGE_BUF_SIZE 64 - -struct daemon { - struct aki_event_loop loop; - struct aki_poll context_poll; - array(struct monitor *) monitors; - struct aki_socket ipc_socket; - bool connected; - struct aki_poll ipc_poll; - struct { - u8 *buf; - ssize_t index; - ssize_t size; - } out; - struct { - u8 *buf; - ssize_t index; - ssize_t size; - ssize_t alloc; - } ipc; - struct config conf; -}; - -static s32 parse_and_set_props(str *selection, str *wallpaper_path, bool *is_package) -{ - s32 index = al_str_find(selection, '|'); - s32 rindex = al_str_rfind(selection, '|'); - if (index == -1 || rindex == -1 || index == rindex) { - return -1; - } - - // index < 2 or id->len == 0 is expected behavior - // when a monitor has no wallpaper set. - if (index < 2) return 1; - str *id = al_str_substr(selection, 0, index - 2); - str *type = al_str_substr(selection, index - 2, index); - if (id->len == 0) return 1; - - al_str_cat(wallpaper_path, id); - - if (al_str_eq(type, al_str_c("_p"))) { - *is_package = true; - } else if (al_str_eq(type, al_str_c("_d"))) { - *is_package = false; - } else { - return -1; - } - - return 0; -} - -static bool send_signal_to_pid(pid_t pid, s32 signal) -{ - if (kill(pid, signal) != 0) { - al_log_error("tarod", "kill(%d) failed (%s)", pid, aki_strerror(errno)); - return false; - } - return true; -} - -static pid_t exec_or_exit(char *args[]) -{ - pid_t pid = fork(); - if (pid == -1) { - al_log_error("tarod", "fatal: failed to fork process, exiting..."); - exit(EXIT_FAILURE); - } else if (pid == 0) { - if (execv(args[0], args) == -1) { - al_log_error("tarod", "fatal: failed to run '%s', exiting...", args[0]); - // This should return the pid of init if the parent process has already exited. - pid_t ppid = getppid(); - if (ppid != 1) send_signal_to_pid(ppid, SIGINT); - } - } - return pid; -} - -static void maybe_kill_wallpaper(struct monitor *monitor) -{ - if (monitor->pid == -1) return; - - if (monitor->paused) { - if (!send_signal_to_pid(monitor->pid, SIGCONT)) { - send_signal_to_pid(monitor->pid, SIGKILL); - } - monitor->paused = false; - } - - send_signal_to_pid(monitor->pid, SIGTERM); - monitor->pid = -1; -} - -static bool run_wallpaper(struct daemon *dmon, struct monitor *monitor) -{ - maybe_kill_wallpaper(monitor); - - str selection; - monitor->self_event_skip = true; - if (!aki_file_open_and_read_wholly(&monitor->selection_path, &selection, true)) { - monitor->self_event_skip = false; - return false; - } - - if (selection.len == 0) { - al_log_error("tarod", "config for %s is empty", monitor->os->name); - al_str_free(&selection); - return false; - } - - str wallpaper_path; - al_str_clone(&wallpaper_path, &dmon->conf.wallpaper_dir); - al_str_cat(&wallpaper_path, al_str_c("/")); - bool is_package; - s32 ret = parse_and_set_props(&selection, &wallpaper_path, &is_package); - al_str_free(&selection); - if (ret != 0) { - if (ret == -1) { - al_log_error("tarod", "failed to set properties for wallpaper on %s", monitor->os->name); - } - al_str_free(&wallpaper_path); - return false; - } - - al_log_info("tarod", "setting wallpaper %.*s on %s", AL_STR_PRINTF(&wallpaper_path), monitor->os->name); - - char width[8], height[8]; - al_sprintf(width, "%d", monitor->os->width / monitor->os->scale); - al_sprintf(height, "%d", monitor->os->height / monitor->os->scale); - - char *c_str0 = al_str_to_c_str(&wallpaper_path); - al_str_free(&wallpaper_path); - monitor->pid = exec_or_exit((char *[]){ "./run_wallpaper.sh", - AL_BOOLSTR(is_package), c_str0, monitor->os->name, width, height, NULL }); - al_free(c_str0); - - if (monitor->covered && send_signal_to_pid(monitor->pid, SIGSTOP)) { - monitor->paused = true; - } - - return true; -} - -static void selection_fs_event_callback(void *userdata, struct inotify_event *event) -{ - struct monitor *monitor = (struct monitor *)userdata; - (void)event; - if (monitor->self_event_skip) { - monitor->self_event_skip = false; - return; - } - struct daemon *dmon = monitor->dmon; - run_wallpaper(dmon, monitor); -} - -static void output_discovered_callback(void *userdata, struct stl_monitor *os_monitor) -{ - struct daemon *dmon = (struct daemon *)userdata; - - struct monitor *monitor; - al_array_foreach(dmon->monitors, i, monitor) { - if (strcmp(monitor->os->name, os_monitor->name) == 0) { - if (monitor->pid == -1) { - run_wallpaper(dmon, monitor); - } - return; - } - } - - str config_dir; - al_str_clone(&config_dir, &dmon->conf.config_dir); - al_str_cat(&config_dir, al_str_c("/")); - al_str_cat(&config_dir, al_str_cr(os_monitor->name)); - - if (!aki_dir_exists(&config_dir)) { - if (!aki_dir_create(&config_dir)) return; - } - - monitor = al_alloc_object(struct monitor); - monitor->os = os_monitor; - monitor->dmon = dmon; - al_array_push(dmon->monitors, monitor); - - al_str_clone(&monitor->config_dir, &config_dir); - al_str_free(&config_dir); - - al_str_clone(&monitor->selection_path, &monitor->config_dir); - al_str_cat(&monitor->selection_path, al_str_c("/selection")); - - if (!aki_file_exists(&monitor->selection_path)) { - aki_file_create(&monitor->selection_path); - aki_file_open_and_replace(&monitor->selection_path, al_str_c("||"), false); - } - - monitor->pid = -1; - monitor->paused = false; - monitor->covered = false; - - run_wallpaper(dmon, monitor); - - monitor->self_event_skip = false; - aki_fs_event_init(&monitor->fs_event, &monitor->selection_path, AKI_CLOSE_WRITE, - selection_fs_event_callback, monitor); - aki_fs_event_start(&monitor->fs_event, &monitor->dmon->loop); -} - -static void close_monitor(struct monitor *monitor) -{ - al_str_free(&monitor->config_dir); - al_str_free(&monitor->selection_path); - aki_fs_event_stop(&monitor->fs_event); - aki_fs_event_free(&monitor->fs_event); -} - -static void ipc_handle_subscribe_success(json_t *root) -{ - if (json_boolean_value(root)) { - al_log_info("tarod", "successfully subscribed to sway background covered events"); - } else { - al_log_info("tarod", "failed to subscribe to sway background covered events"); - } -} - -static void ipc_handle_response(struct daemon *dmon, u8 *buf, u32 size) -{ - al_log_debug("tarod", "IN: %.*s", size, (char *)buf); - - json_error_t error; - json_t *root = json_loadb((char *)buf, size, 0, &error); - if (!root) { - al_log_error("tarod", "failed to parse sway-ipc json payload (%s)", error.text); - goto done; - } - - json_t *success = json_object_get(root, "success"); - if (success) { - ipc_handle_subscribe_success(success); - goto done; - } - - json_t *output = json_object_get(root, "output"); - if (output) { - bool covered = json_boolean_value(json_object_get(root, "covered")); - struct monitor *monitor; - al_array_foreach(dmon->monitors, i, monitor) { - if (strcmp(monitor->os->name, json_string_value(output)) != 0) continue; - monitor->covered = covered; - if (monitor->pid == -1) goto done; - if (covered && !monitor->paused && send_signal_to_pid(monitor->pid, SIGSTOP)) { - monitor->paused = true; - } else if (!covered && monitor->paused && send_signal_to_pid(monitor->pid, SIGCONT)) { - monitor->paused = false; - } - break; - } - } - -done: - if (root) json_decref(root); -} - -static void ensure_ipc_buf_size(struct daemon *dmon, ssize_t size) -{ - if (dmon->ipc.alloc < size) { - dmon->ipc.buf = realloc(dmon->ipc.buf, size); - dmon->ipc.alloc = size; - } -} - -static void ipc_poll_callback(void *userdata, s32 revents) -{ - struct daemon *dmon = (struct daemon *)userdata; - - if (revents & AKI_POLL_WRITE) { - if (!dmon->connected) { - al_log_info("tarod", "connected to sway ipc socket"); - dmon->connected = true; - } - if (!dmon->out.buf) return; - dmon->out.index += aki_socket_write(&dmon->ipc_socket, dmon->out.buf + dmon->out.index, - dmon->out.size - dmon->out.index); - if (dmon->out.index == dmon->out.size) { - al_free(dmon->out.buf); - dmon->out.buf = NULL; - aki_poll_stop(&dmon->ipc_poll); - aki_poll_set(&dmon->ipc_poll, aki_socket_get_fd(&dmon->ipc_socket), AKI_POLL_READ); - aki_poll_start(&dmon->ipc_poll, &dmon->loop); - } - } - - if (revents & AKI_POLL_READ) { - if (dmon->ipc.index < IPC_HEADER_LENGTH) { - dmon->ipc.index += aki_socket_read(&dmon->ipc_socket, dmon->ipc.buf + dmon->ipc.index, - IPC_HEADER_LENGTH - dmon->ipc.index); - } - if (dmon->ipc.index == IPC_HEADER_LENGTH && !dmon->ipc.size) { - dmon->ipc.size = *((s32 *)&dmon->ipc.buf[6]) + IPC_HEADER_LENGTH; - ensure_ipc_buf_size(dmon, dmon->ipc.size); - } - if (dmon->ipc.size) { - dmon->ipc.index += aki_socket_read(&dmon->ipc_socket, dmon->ipc.buf + dmon->ipc.index, - dmon->ipc.size - dmon->ipc.index); - if (dmon->ipc.index == dmon->ipc.size) { - ipc_handle_response(dmon, dmon->ipc.buf + IPC_HEADER_LENGTH, - dmon->ipc.size - IPC_HEADER_LENGTH); - dmon->ipc.size = 0; - dmon->ipc.index = 0; - } - } - } -} - -static void context_poll_callback(void *userdata, s32 revents) -{ - (void)userdata; - al_assert(revents & AKI_POLL_READ); - stl_global_process_events(); -} - - -// https://stackoverflow.com/a/59071370 -static void handle_sigchld(s32 sig) -{ - (void)sig; - s32 saved_errno = errno; - while (waitpid((pid_t)(-1), 0, WNOHANG) > 0) {} - errno = saved_errno; -} - -#define SWAY_IPC_SUBSCRIBE 2 - -static struct daemon dmon = { 0 }; - -s32 main(void) -{ - if (!aki_common_init()) return EXIT_FAILURE; - - signal(SIGCHLD, handle_sigchld); - - s32 ret = EXIT_FAILURE; - - if (!open_config(&dmon.conf)) goto out; - - aki_event_loop_init(&dmon.loop); - - al_array_init(dmon.monitors); - - stl_set_output_discovered_callback(output_discovered_callback, &dmon); - if (!stl_global_init()) return EXIT_FAILURE; - aki_poll_init(&dmon.context_poll, context_poll_callback, &dmon); - aki_poll_set(&dmon.context_poll, stl_global_get_poll_fd(), AKI_POLL_READ); - aki_poll_start(&dmon.context_poll, &dmon.loop); - - str monitors; - al_str_from(&monitors, ""); - struct monitor *monitor; - al_array_foreach(dmon.monitors, i, monitor) { - al_str_cat(&monitors, al_str_cr(monitor->os->name)); - al_str_cat(&monitors, al_str_c("\n")); - } - - str monitors_path; - al_str_clone(&monitors_path, &dmon.conf.config_dir); - al_str_cat(&monitors_path, al_str_c("/monitors")); - if (!aki_file_exists(&monitors_path)) { - aki_file_create(&monitors_path); - } - aki_file_open_and_replace(&monitors_path, &monitors, false); - - dmon.ipc.buf = NULL; - dmon.ipc.size = 0; - dmon.ipc.index = 0; - dmon.ipc.alloc = 0; - ensure_ipc_buf_size(&dmon, IPC_HEADER_LENGTH); - - json_t *payload = json_array(); - json_array_append(payload, json_string("background_covered")); - dmon.out.buf = al_malloc(IPC_MESSAGE_BUF_SIZE); - al_memcpy(dmon.out.buf, "i3-ipc", strlen("i3-ipc")); - s32 type = SWAY_IPC_SUBSCRIBE; - al_memcpy(&dmon.out.buf[10], &type, sizeof(s32)); - s32 len = json_dumpb(payload, (char *)&dmon.out.buf[14], IPC_MESSAGE_BUF_SIZE - IPC_HEADER_LENGTH, 0); - al_memcpy(&dmon.out.buf[6], &len, sizeof(s32)); - dmon.out.size = IPC_HEADER_LENGTH + len; - dmon.out.index = 0; - json_decref(payload); - - dmon.ipc_socket.type = AKI_SOCKET_UNIX; - aki_socket_init(&dmon.ipc_socket); - aki_socket_set_blocking(&dmon.ipc_socket, false); - aki_poll_init(&dmon.ipc_poll, ipc_poll_callback, &dmon); - aki_poll_set(&dmon.ipc_poll, aki_socket_get_fd(&dmon.ipc_socket), AKI_POLL_WRITE); - dmon.connected = false; - char *swaysock = getenv("SWAYSOCK"); - if (swaysock && aki_socket_connect(&dmon.ipc_socket, al_str_cr(swaysock), 0)) { - aki_poll_start(&dmon.ipc_poll, &dmon.loop); - } else { - al_log_error("tarod", "failed to connect to sway ipc socket"); - } - - aki_event_loop_run(&dmon.loop); - - al_array_foreach(dmon.monitors, i, monitor) { - close_monitor(monitor); - } - al_array_free(dmon.monitors); - al_free(dmon.ipc.buf); - - aki_event_loop_destroy(&dmon.loop); - - close_config(&dmon.conf); - - ret = EXIT_SUCCESS; - -out: - aki_common_close(); - - return ret; -} diff --git a/taro/src/db.c b/taro/src/db.c deleted file mode 100644 index 9d2ffa2..0000000 --- a/taro/src/db.c +++ /dev/null @@ -1,212 +0,0 @@ -#include - -#include "db.h" -#include "file_ext.h" - -#define REAL_OR_INT(v) \ - json_typeof(v) == JSON_REAL ? json_real_value(v) : json_integer_value(v) - -static bool load_entry(struct entry *e) -{ - struct aki_dir dir; - if (!aki_dir_open(&dir, &e->path)) return false; - - struct aki_dir_entry entry; - while (aki_dir_read(&dir, &entry)) { - if (entry.entry->d_type != DT_REG) { - continue; - } - - s32 index = al_str_rfind(al_str_cr(entry.entry->d_name), '.'); - if (index == -1) { - continue; - } - - if (al_str_eq(al_str_w(entry.entry->d_name, 0, index), al_str_c("project"))) { - str package_path; - al_str_clone(&package_path, &e->path); - al_str_cat(&package_path, al_str_c("/scene.pkg")); - e->has_package = aki_file_exists(&package_path); - al_str_free(&package_path); - - str project_path; - al_str_clone(&project_path, &e->path); - al_str_cat(&project_path, al_str_c("/")); - al_str_cat(&project_path, al_str_cr(entry.entry->d_name)); - - json_t *root = aki_file_open_as_json(&project_path); - al_str_free(&project_path); - if (!root) continue; - e->loaded = true; - - json_t *title = json_object_get(root, "title"); - if (title) { - al_wstr_from_cstr(&e->title, (char *)json_string_value(title)); - } - - json_t *preview = json_object_get(root, "preview"); - if (preview) { - al_str_clone(&e->preview_path, &e->path); - al_str_cat(&e->preview_path, al_str_c("/")); - al_str_cat(&e->preview_path, al_str_cr(json_string_value(preview))); - e->has_preview = aki_file_exists(&e->preview_path); - } - - json_t *general = json_object_get(root, "general"); - if (!general) { - continue; - } - - al_array_init(e->props); - json_t *properties = json_object_get(general, "properties"); - if (properties) { - void *iter = json_object_iter(properties); - json_t *iter_obj; - json_t *text, *type; - while (iter) { - iter_obj = json_object_iter_value(iter); - text = json_object_get(iter_obj, "text"); - if (!text || strcmp(json_string_value(text), "ui_browse_properties_scheme_color") == 0) { - iter = json_object_iter_next(properties, iter); - continue; - } - - struct prop p; - p.type = PROP_TYPE_UNKNOWN; - al_wstr_from_cstr(&p.title, (char *)json_string_value(text)); - type = json_object_get(iter_obj, "type"); - if (!type) { - // Silently ignore for now. Is "Notes" a special case? see 1918273588. - //log_warn("improperly handled prop (%.*s)", AL_STR_PRINTF(&e->id)); - iter = json_object_iter_next(properties, iter); - continue; - } - - str *type_str = al_str_cr(json_string_value(type)); - if (al_str_eq(type_str, al_str_c("bool"))) { - json_t *value = json_object_get(iter_obj, "value"); - if (!value) { - al_log_warn("db", "improperly handled bool (%.*s)", AL_STR_PRINTF(&e->id)); - iter = json_object_iter_next(properties, iter); - continue; - } - p.value = al_malloc(sizeof(bool)); - *((bool *)p.value) = json_boolean_value(value); - p.type = PROP_TYPE_BOOL; - } else if (al_str_eq(type_str, al_str_c("slider"))) { - json_t *value = json_object_get(iter_obj, "value"); - json_t *min = json_object_get(iter_obj, "min"); - json_t *max = json_object_get(iter_obj, "max"); - if (!value || !min || !max) { - al_log_warn("db", "improperly handled slider (%.*s)", AL_STR_PRINTF(&e->id)); - iter = json_object_iter_next(properties, iter); - continue; - } - json_t *step = json_object_get(iter_obj, "step"); - p.value = al_malloc(sizeof(struct slider_value)); - struct slider_value *slider = (struct slider_value *)p.value; - slider->value = REAL_OR_INT(value); - slider->min = REAL_OR_INT(min); - slider->max = REAL_OR_INT(max); - if (step) { - slider->step = REAL_OR_INT(step); - } else { - // Assume the default step is 1.0. - slider->step = 1.0; - } - p.type = PROP_TYPE_SLIDER; - } else { - //al_log_warn("db", "unhandled type. (%.*s)", AL_STR_PRINTF(type_str)); - } - - al_array_push(e->props, p); - - iter = json_object_iter_next(properties, iter); - } - } - } - } - - aki_dir_close(&dir); - - return e->loaded; -} - -bool db_ensure_entry_loaded(struct db *db, struct entry *e) -{ - if (!e->loaded && !load_entry(e)) { - // Remove entry if it cannot be loaded. - struct entry *re; - al_array_foreach_ptr(db->entries, i, re) { - if (re == e) { - al_array_remove_at(db->entries, i); - break; - } - } - } - return e->loaded; -} - -static s32 id_compare(void *a, void *b) -{ - s64 aid = al_str_to_long(&((struct entry *)a)->id, 10); - s64 bid = al_str_to_long(&((struct entry *)b)->id, 10); - if (aid > bid) { - return 1; - } else if (aid < bid) { - return -1; - } else { - return 0; - } -} - -bool db_load_entries(struct db *db, str *path) -{ - struct aki_dir dir; - if (!aki_dir_open(&dir, path)) return false; - - al_array_init(db->entries); - struct aki_dir_entry entry; - while (aki_dir_read(&dir, &entry)) { - if (!(entry.entry->d_type == DT_DIR || entry.entry->d_type == DT_UNKNOWN)) { - continue; - } - if (strcmp(entry.entry->d_name, ".") == 0 || - strcmp(entry.entry->d_name, "..") == 0) { - continue; - } - struct entry e = { 0 }; - e.loaded = false; - al_str_clone(&e.id, al_str_cr(entry.entry->d_name)); - al_str_clone(&e.path, path); - al_str_cat(&e.path, al_str_c("/")); - al_str_cat(&e.path, &e.id); - if (!aki_dir_exists(&e.path)) { - al_str_free(&e.id); - al_str_free(&e.path); - continue; - } - al_array_push(db->entries, e); - } - - al_array_sort(db->entries, struct entry, id_compare); - - return true; -} - -void db_unload(struct db *db) -{ - struct entry *e; - al_array_foreach_ptr(db->entries, i, e) { - al_str_free(&e->path); - al_str_free(&e->preview_path); - al_wstr_free(&e->title); - struct prop *p; - al_array_foreach_ptr(e->props, j, p) { - al_wstr_free(&p->title); - if (p->type != PROP_TYPE_UNKNOWN) { - al_free(p->value); - } - } - } -} diff --git a/taro/src/db.h b/taro/src/db.h deleted file mode 100644 index 78f4c77..0000000 --- a/taro/src/db.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include - -enum { - PROP_TYPE_BOOL, - PROP_TYPE_SLIDER, - PROP_TYPE_UNKNOWN -}; - -struct slider_value { - f64 min, max; - f64 step; - f64 value; -}; - -struct prop { - u8 type; - wstr title; - void *value; -}; - -struct entry { - bool loaded; - str id; - str path; - bool has_package; - str preview_path; - bool has_preview; - wstr title; - array(struct prop) props; -}; - -struct db { - array(struct entry) entries; -}; - -bool db_load_entries(struct db *db, str *path); -bool db_ensure_entry_loaded(struct db *db, struct entry *e); -void db_unload(struct db *db); diff --git a/taro/src/file_ext.c b/taro/src/file_ext.c deleted file mode 100644 index 48dffce..0000000 --- a/taro/src/file_ext.c +++ /dev/null @@ -1,56 +0,0 @@ -#include - -#include "file_ext.h" - -bool aki_file_open_and_read_wholly(str *path, str *out, bool lock) -{ - struct aki_file file; - if (!aki_file_open(&file, path, lock ? AKI_FILE_LOCK : AKI_FILE_READONLY)) { - return false; - } - s32 ret = aki_file_read_as_str(&file, out); - aki_file_close(&file); - return ret != -1; -} - -bool aki_file_open_and_replace(str *path, str *buf, bool lock) -{ - struct aki_file file; - if (!aki_file_open(&file, path, lock ? AKI_FILE_LOCK : 0)) { - return false; - } - s32 ret = aki_file_replace(&file, buf); - aki_file_close(&file); - return ret != -1; -} - -json_t *aki_file_open_as_json(str *path) -{ - str s; - if (!aki_file_open_and_read_wholly(path, &s, false)) { - return NULL; - } - json_error_t error; - json_t *root = json_loadb(s.data, s.len, 0, &error); - if (!root) { - al_log_error("file_ext", "json_loadb(%.*s, %u) failed (%s)", AL_STR_PRINTF(path), s.len, error.text); - } - al_str_free(&s); - return root; -} - -bool aki_file_dump_and_decref_json(str *path, json_t *root) -{ - str s; - size_t size = json_dumpb(root, NULL, 0, JSON_INDENT(4)); - al_str_sized(&s, size); - s.len = json_dumpb(root, s.data, size, JSON_INDENT(4)); - if (s.len == 0) { - json_decref(root); - return false; - } - bool ret = aki_file_open_and_replace(path, &s, false); - al_str_free(&s); - json_decref(root); - return ret; -} diff --git a/taro/src/file_ext.h b/taro/src/file_ext.h deleted file mode 100644 index ddaceba..0000000 --- a/taro/src/file_ext.h +++ /dev/null @@ -1,7 +0,0 @@ -#include -#include - -bool aki_file_open_and_read_wholly(str *path, str *out, bool lock); -bool aki_file_open_and_replace(str *path, str *buf, bool lock); -json_t *aki_file_open_as_json(str *path); -bool aki_file_dump_and_decref_json(str *path, json_t *root); diff --git a/taro/src/ui.c b/taro/src/ui.c deleted file mode 100644 index b2c700e..0000000 --- a/taro/src/ui.c +++ /dev/null @@ -1,1203 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "db.h" -#include "file_ext.h" -#include "config.h" - -#define PIXEL_BLIT 1 - -#define TIMER_UPDATE_BASE 1.f - -#define TILE_WIDTH 18 -#define TILE_HEIGHT 10 -#define FLASH_DURATION 0.3f - -#define TEXT_WIDTH (TILE_WIDTH - 4) -#define TEXT_UPDATE_RATE 0.3f; - -#define INFO_RES_WIDTH 33 -#define INFO_RES_HEIGHT 2 - -#define INFO_X_OFFSET 2 -#define INFO_Y_OFFSET 1 - -#define INFO_WIDTH 30 -// INFO_HEIGHT = term_rows + INFO_RES_HEIGHT - -#define INFO_TILE_WIDTH 24 -#define INFO_TILE_HEIGHT 12 - -#define INFO_TEXT_WIDTH (TEXT_WIDTH + 4) - -#define PROP_TEXT_WIDTH 16 - -#define MAX_MONITOR_NAME_LEN 255 - -struct scrolling_text { - wstr *s; - s32 width; - s32 index, max; - s32 x, y; -}; - -struct image { - u32 hash; - bool gif; - bool errored; - struct ncvisual *v; -}; - -struct image_pool { - u32 max; - u32 chunk_size; - array(struct image) images; -}; - -struct tile { - struct ncplane *p, *ip; - struct entry *e; - bool blitted; - struct scrolling_text text; -}; - -struct monitors { - array(char *) monitors; - s32 selected_monitor; - s32 set_monitor; - f32 flash; - str selection_path; -}; - -struct grid { - struct ncplane *p; - s32 tiles_x, tiles_y; - s32 min_index; - s32 max_index; - s32 selected_index; - s32 set_index; - f32 flash; - s32 tiles_per_page; - array(struct tile) tiles; -}; - -struct info_prop { - struct prop *p; - struct scrolling_text text; -}; - -struct info { - struct ncplane *ip; - struct entry *e; - bool blitted; - struct scrolling_text text; - u32 max_props; - s32 selected_prop; - array(struct info_prop) props; -}; - -enum { - MENU_GRID = 0, - MENU_INFO, - MENU_MONITORS -}; - -struct ui { - struct notcurses *nc; - struct aki_event_loop loop; - struct aki_poll input_poll; - u64 delay; - struct aki_timer timer; - bool layed_out; - f32 text_update; - u32 term_rows, term_cols; - u8 selected_menu; - struct ncplane *p, *mp, *gp, *ip; - struct monitors m; - struct grid g; - struct info n; - struct image_pool pool; - struct db db; - struct config conf; -}; - -// --------- -// Client -// --------- - -static s32 get_entry_from_config(struct ui *u) -{ - s32 ret = -1; - str config; - if (aki_file_open_and_read_wholly(&u->m.selection_path, &config, false)) { - s32 index = al_str_find(&config, '|'); - if (index > 2) { - str *id = al_str_substr(&config, 0, index - 2); - struct entry *e; - al_array_foreach_ptr(u->db.entries, i, e) { - if (al_str_eq(&e->id, id)) { - ret = i; - break; - } - } - } - al_str_free(&config); - } - return ret; -} - -static void select_monitor(struct ui *u) -{ - if (u->m.set_monitor == -1) return; - u->m.set_monitor = u->m.selected_monitor; - al_str_free(&u->m.selection_path); - al_str_clone(&u->m.selection_path, &u->conf.config_dir); - al_str_cat(&u->m.selection_path, al_str_c("/")); - char *monitor = al_array_at(u->m.monitors, u->m.set_monitor); - al_str_cat(&u->m.selection_path, al_str_cr(monitor)); - al_str_cat(&u->m.selection_path, al_str_c("/selection")); - u->g.set_index = get_entry_from_config(u); - if (u->g.set_index >= 0) { - u->g.selected_index = u->g.set_index; - } -} - -static void write_selected_entry_to_config(struct ui *u) -{ - if (u->m.set_monitor == -1) return; - str config; - if (u->g.set_index == -1) { - al_str_from(&config, "||"); - } else { - al_str_clone(&config, &u->n.e->id); - if (u->n.e->has_package) { - al_str_cat(&config, al_str_c("_p")); - } else { - al_str_cat(&config, al_str_c("_d")); - } - al_str_cat(&config, al_str_c("|false,100|")); - struct prop *p; - al_array_foreach_ptr(u->n.e->props, i, p) { - switch (p->type) { - case PROP_TYPE_BOOL: - al_str_cat(&config, *((bool *)p->value) ? al_str_c("true") : al_str_c("false")); - break; - } - al_str_cat(&config, al_str_c(",")); - } - if (al_str_at(&config, config.len - 1) == ',') { - config.len--; - } - } - aki_file_open_and_replace(&u->m.selection_path, &config, true); -} - -// --------- -// Interface -// --------- - -static void set_scrolling_text(struct scrolling_text *text, wstr *s, s32 text_width, s32 width, s32 x, s32 y) -{ - text->index = 0; - text->width = width; - text->s = s; - if (text_width > text->width) { - text->max = (text_width - text->width); - // If the character at text->index is wider than 1 cell, - // that is the amount the text will scroll by. So, account - // for that here. - for (s32 i = 0; i < text->max; i++) { - s32 wcl = al_wchar_width(al_wstr_at(s, i)); - if (wcl > 1) { - text->max -= wcl - 1; - } - } - text->max++; - } else { - text->max = -1; - } - text->x = x; - text->y = y; -} - -static void draw_scrolling_text(struct ncplane *p, struct scrolling_text *t, bool update) -{ - ncplane_cursor_move_yx(p, t->y, t->x); - if (t->max != -1) { - s32 i = 0; - s32 width = 0; - while (width < t->width) { - wchar_t wc = al_wstr_at(t->s, t->index + i); - width += al_wchar_width(wc); - if (width > t->width) { - break; - } - ncplane_putwc(p, wc); - i++; - } - if (update) { - t->index = (t->index + 1) % t->max; - } - } else { - ncplane_putwstr(p, &al_wstr_at(t->s, 0)); - } -} - -static void set_info_entry(struct ui *u, struct info *n) -{ - n->selected_prop = 0; - n->props.size = 0; - - if (u->g.selected_index == -1 || u->g.selected_index >= (s32)u->db.entries.size) { - n->e = NULL; - return; - } - - n->e = &al_array_at(u->db.entries, u->g.selected_index); - - n->blitted = false; - - s32 width = al_wstr_width(&u->n.e->title); - s32 tx = 2 + (width < INFO_TEXT_WIDTH ? (INFO_TEXT_WIDTH - width) / 2 : 0); - set_scrolling_text(&n->text, &u->n.e->title, width, INFO_TEXT_WIDTH, tx, INFO_TILE_HEIGHT + 1); - - struct prop *p; - al_array_foreach_ptr(u->n.e->props, i, p) { - struct info_prop ip = { 0 }; - ip.p = p; - width = al_wstr_width(&p->title); - s32 ty = (INFO_TILE_HEIGHT + 3) + ((i % n->max_props) * 2); - set_scrolling_text(&ip.text, &p->title, width, PROP_TEXT_WIDTH, 0, ty); - al_array_push(n->props, ip); - } -} - -static bool init_info(struct info *n) -{ - al_array_init(n->props); - return true; -} - -static bool init_image_pool(struct image_pool *pool) -{ - pool->chunk_size = 0; - pool->max = 0; - al_array_init(pool->images); - return true; -} - -static bool init_grid(struct grid *g) -{ - al_array_init(g->tiles); - return true; -} - -static s32 monitor_cmp(void *va, void *vb) -{ - return strncmp(*(char **)va, *(char **)vb, MAX_MONITOR_NAME_LEN); -} - -static bool init_monitors(struct ui *u, struct monitors *m) -{ - al_array_init(m->monitors); - str monitors_path; - al_str_clone(&monitors_path, &u->conf.config_dir); - al_str_cat(&monitors_path, al_str_c("/monitors")); - str monitors; - bool ret = aki_file_open_and_read_wholly(&monitors_path, &monitors, false); - al_str_free(&monitors_path); - if (!ret) { - al_log_error("taro", "failed to open monitor list, is the daemon configured?"); - return false; - } - str monitor_dir; - str monitor = al_str_zero(); - while (al_str_get_line(&monitors, '\n', &monitor)) { - al_str_clone(&monitor_dir, &u->conf.config_dir); - al_str_cat(&monitor_dir, al_str_c("/")); - al_str_cat(&monitor_dir, &monitor); - if (aki_dir_exists(&monitor_dir)) { - char *c_str = al_str_to_c_str(&monitor); - al_array_push(m->monitors, c_str); - } - al_str_free(&monitor_dir); - } - if (!u->m.monitors.size) { - u->m.set_monitor = -1; - u->m.selected_monitor = -1; - } - al_array_sort(m->monitors, char *, monitor_cmp); - u->g.set_index = -1; - if (m->selected_monitor >= 0) { - select_monitor(u); - } - if (u->db.entries.size > 0) { - u->g.selected_index = (u->g.set_index >= 0) ? u->g.set_index : 0; - } - return true; -} - -static bool init_ui(struct ui *u) -{ - if (!init_monitors(u, &u->m)) return false; - if (!init_grid(&u->g)) return false; - if (!init_image_pool(&u->pool)) return false; - if (!init_info(&u->n)) return false; - if (!(u->nc = notcurses_init(NULL, stdin))) { - al_log_error("taro", "failed to initialize notcurses"); - return false; - } - return true; -} - -struct image *get_image_from_pool(struct image_pool *pool, str *path) -{ - u32 hash = al_str_hash(path); - struct image *rimg; - al_array_foreach_ptr(pool->images, i, rimg) { - if (rimg->hash == hash) { - return rimg; - } - } - - bool gif = false; - s32 index = al_str_rfind(path, '.'); - if (index != -1) { - if (al_str_eq(al_str_w(path->data, index, 4), al_str_c(".gif"))) { - gif = true; - } - } - - if (pool->images.size >= pool->max) { - // al_array_remove_range()... - for (u32 i = 0; i < pool->chunk_size; i++) { - struct image img; - al_array_pop_at(pool->images, 0, img); - ncvisual_destroy(img.v); - } - } - - char *c_str = al_str_to_c_str(path); - struct image img = { - .hash = hash, - .gif = gif, - .errored = false, - .v = ncvisual_from_file(c_str) - }; - al_free(c_str); - if (!img.v) return NULL; - al_array_push(pool->images, img); - - return &al_array_last(pool->images); -} - -static bool layout_ui(struct ui *u); -static void draw_monitors(struct ui *u, struct monitors *m); -static void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text); -static void draw_info(struct ui *u, struct info *n, bool update_text); - -static s32 resize_cb(struct ncplane *p) -{ - struct ui *u = ncplane_userptr(p); - if (!u) return 0; - notcurses_stddim_yx(u->nc, &u->term_rows, &u->term_cols); - if (!layout_ui(u)) { - u->layed_out = false; - return 0; - } - u->layed_out = true; - struct tile *t; - al_array_foreach_ptr(u->g.tiles, i, t) { - t->blitted = false; - ncplane_erase(t->ip); - } - u->n.blitted = false; - ncplane_erase(u->n.ip); - // To not break images we need to erase them - // then render them in 2 steps. - notcurses_refresh(u->nc, NULL, NULL); - notcurses_render(u->nc); - draw_monitors(u, &u->m); - draw_grid_tiles(u, &u->g, false); - draw_info(u, &u->n, false); - notcurses_render(u->nc); - return 0; -} - -static void maybe_destroy_info(struct info *n) -{ - if (n->ip) { - ncplane_destroy(n->ip); - n->ip = NULL; - } -} - -static bool layout_info(struct ui *u, struct info *n, struct ncplane *parent, s32 width, s32 height) -{ - (void)width; - - maybe_destroy_info(n); - - n->max_props = (height - INFO_TILE_HEIGHT - 3) / 2; - if (!n->max_props) return false; - - struct ncplane_options nopts = { 0 }; - - nopts.cols = INFO_TILE_WIDTH; - nopts.rows = INFO_TILE_HEIGHT; - nopts.x = 0; - nopts.y = 1; - - n->ip = ncplane_create(parent, &nopts); - if (!n->ip) { - maybe_destroy_info(n); - return false; - } - - set_info_entry(u, &u->n); - - return true; -} - -static void maybe_destroy_grid(struct grid *g) -{ - if (!g->p) return; - struct tile *t; - al_array_foreach_ptr(g->tiles, i, t) { - ncplane_destroy(t->ip); - t->ip = NULL; - ncplane_destroy(t->p); - t->p = NULL; - } - g->tiles.size = 0; - ncplane_destroy(g->p); - g->p = NULL; -} - -static void set_grid_tiles(struct ui *u, struct grid *g) -{ - u32 index = (g->selected_index - (g->selected_index % g->tiles_per_page)); - - g->min_index = index; - g->max_index = index; - - for (s32 y = 0; y < g->tiles_y; y++) { - for (s32 x = 0; x < g->tiles_x; x++) { - struct tile *t = &al_array_at(g->tiles, y * g->tiles_x + x); - ncplane_erase(t->ip); - t->blitted = false; - ncplane_erase(t->p); - - if (g->max_index >= (s32)u->db.entries.size) { - t->e = NULL; - t->text.s = NULL; - ncplane_erase(t->ip); - continue; - } - - t->e = &al_array_at(u->db.entries, g->max_index); - if (!db_ensure_entry_loaded(&u->db, t->e)) { - x--; - continue; - } - - s32 width = al_wstr_width(&t->e->title); - s32 tx = 2 + ((width < TEXT_WIDTH) ? (TEXT_WIDTH - width) / 2 : 0); - set_scrolling_text(&t->text, &t->e->title, width, TEXT_WIDTH, tx, TILE_HEIGHT - 1); - - g->max_index++; - }} - - g->max_index--; -} - -static bool layout_grid(struct ui *u, struct grid *g, struct ncplane *parent, s32 width, s32 height) -{ - maybe_destroy_grid(g); - - s32 inner_width = width; - s32 inner_height = height; - - g->tiles_x = (s32)(width / TILE_WIDTH); - g->tiles_y = (s32)(height / TILE_HEIGHT); - - if (!g->tiles_x || !g->tiles_y) { - // Window is too small. - return false; - } - - g->tiles_per_page = g->tiles_x * g->tiles_y; - - // Allow 4 pages of tiles in the pool, but never shrink. - if (u->pool.chunk_size < (u32)g->tiles_per_page) { - u->pool.chunk_size = g->tiles_per_page; - u->pool.max = u->pool.chunk_size * 4; - al_array_reserve(u->pool.images, u->pool.max); - } - - while (inner_width && inner_width % g->tiles_x != 0) { - inner_width--; - } - - while (inner_height && inner_height % g->tiles_y != 0) { - inner_height--; - } - - struct ncplane_options nopts = { 0 }; - - nopts.cols = inner_width; - nopts.rows = inner_height; - nopts.x = (width - (g->tiles_x * TILE_WIDTH)) / 2; - nopts.y = (height - (g->tiles_y * TILE_HEIGHT)) / 2; - - g->p = ncplane_create(parent, &nopts); - if (!g->p) return false; - - for (s32 y = 0; y < g->tiles_y; y++) { - for (s32 x = 0; x < g->tiles_x; x++) { - struct tile t = { 0 }; - - nopts.cols = TILE_WIDTH; - nopts.rows = TILE_HEIGHT; - nopts.x = x * TILE_WIDTH; - nopts.y = y * TILE_HEIGHT; - - t.p = ncplane_create(g->p, &nopts); - if (!t.p) { - maybe_destroy_grid(g); - return false; - } - - nopts.cols = TILE_WIDTH - 2; - nopts.rows = TILE_HEIGHT - 2; - nopts.x = 1; - nopts.y = 1; - - t.ip = ncplane_create(t.p, &nopts); - if (!t.ip) { - maybe_destroy_grid(g); - return false; - } - - al_array_push(g->tiles, t); - }} - - set_grid_tiles(u, g); - - return true; -} - -static void maybe_destroy_ui(struct ui *u) -{ - if (u->ip) { - ncplane_destroy(u->ip); - u->ip = NULL; - } - if (u->gp) { - ncplane_destroy(u->gp); - u->gp = NULL; - } - if (u->mp) { - ncplane_destroy(u->mp); - u->mp = NULL; - } - if (u->p) { - ncplane_destroy(u->p); - u->p = NULL; - } -} - -bool layout_ui(struct ui *u) -{ - bool even = u->term_cols % 2 == 0; - - u32 rwidth = (even) ? INFO_RES_WIDTH + 1 : INFO_RES_WIDTH; - u32 rheight = (even) ? INFO_RES_HEIGHT + 1 : INFO_RES_HEIGHT; - - if (u->term_cols < TILE_WIDTH + rwidth || u->term_rows < TILE_HEIGHT + rheight) { - return false; - } - - maybe_destroy_ui(u); - - struct ncplane_options nopts = { 0 }; - - nopts.rows = u->term_rows; - nopts.cols = u->term_cols; - - u->p = ncplane_create(notcurses_stdplane(u->nc), &nopts); - if (!u->p) { - return false; - } - - s32 cols = u->term_cols - rwidth; - s32 rows = u->term_rows - rheight; - - if (cols < 0 || rows < 0) { - maybe_destroy_ui(u); - return false; - } - - nopts.x = 1; - nopts.y = 1; - nopts.cols = cols - 1; - nopts.rows = 1; - - u->mp = ncplane_create(u->p, &nopts); - if (!u->mp) { - maybe_destroy_ui(u); - return false; - } - - nopts.x = 1; - nopts.y = 2; - nopts.cols = cols - 1; - nopts.rows = rows - 2; - - u->gp = ncplane_create(u->p, &nopts); - if (!u->gp) { - maybe_destroy_ui(u); - return false; - } - - if (!layout_grid(u, &u->g, u->gp, nopts.cols, nopts.rows)) { - maybe_destroy_ui(u); - return false; - } - - nopts.x = nopts.cols + ((even) ? INFO_X_OFFSET + 1 : INFO_X_OFFSET); - nopts.y = INFO_Y_OFFSET; - nopts.cols = INFO_WIDTH; - nopts.rows = u->term_rows - rheight; - - u->ip = ncplane_create(u->p, &nopts); - if (!u->ip) { - maybe_destroy_ui(u); - return false; - } - - if (!layout_info(u, &u->n, u->ip, nopts.cols, nopts.rows)) { - maybe_destroy_ui(u); - return false; - } - - return true; -} - -void draw_monitors(struct ui *u, struct monitors *m) -{ - if (!u->layed_out) return; - - s32 width = 0; - char *monitor; - al_array_foreach(m->monitors, i, monitor) { - width += strlen(monitor) + 1; - } - - ncplane_set_styles(u->mp, NCSTYLE_BOLD); - s32 w = ncplane_dim_x(u->mp); - s32 x = ((w - 1) - width) / 2; - ncplane_cursor_move_yx(u->mp, 0, x); - al_array_foreach(m->monitors, i, monitor) { - if (m->flash <= 0.f && u->selected_menu == MENU_MONITORS && i == (u32)m->selected_monitor) { - ncplane_set_bg_rgb8(u->mp, 211, 184, 152); - ncplane_set_fg_rgb8(u->mp, 13, 22, 45); - } else if (i == (u32)m->set_monitor) { - ncplane_set_bg_rgb8(u->mp, 242, 233, 215); - ncplane_set_fg_rgb8(u->mp, 13, 22, 45); - m->flash -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); - } else { - ncplane_set_fg_rgb8(u->mp, 0xff, 0xff, 0xff); - ncplane_set_bg_palindex(u->mp, 0); - } - ncplane_cursor_move_rel(u->mp, 0, 1); - ncplane_putstr(u->mp, monitor); - } -} - -void draw_info(struct ui *u, struct info *n, bool update_text) -{ - if (!u->layed_out || !n->e) return; - - ncplane_erase(u->ip); - - struct ncvisual_options vopts = { 0 }; - vopts.scaling = NCSCALE_SCALE; - vopts.blitter = PIXEL_BLIT ? NCBLIT_PIXEL : NCBLIT_2x1; - struct image *img = get_image_from_pool(&u->pool, &n->e->preview_path); - if (img && !n->blitted && !img->errored) { - ncplane_erase(n->ip); - img->errored = img->gif && ncvisual_decode_loop(img->v) < 0; - u->delay = ncvisual_get_duration(img->v); - if (!img->errored) { - vopts.n = n->ip; - ncvisual_blit(u->nc, img->v, &vopts); - n->blitted = !img->gif; - } - } - - ncplane_set_fg_rgb8(u->ip, 0xff, 0xff, 0xff); - draw_scrolling_text(u->ip, &n->text, update_text); - - u32 selected_index = n->selected_prop - (n->selected_prop % n->max_props); - struct info_prop *ip = NULL; - u32 i, index; - for (i = 0; i < n->max_props; i++) { - index = i + selected_index; - if (index >= n->props.size) { - break; - } - ip = &al_array_at(n->props, index); - if (u->selected_menu == MENU_INFO && index == (u32)n->selected_prop) { - ncplane_set_fg_rgb8(u->ip, 211, 184, 152); - } else { - ncplane_set_fg_rgb8(u->ip, 0xff, 0xff, 0xff); - } - draw_scrolling_text(u->ip, &ip->text, update_text); - switch (ip->p->type) { - case PROP_TYPE_BOOL: - ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, - *((bool *)ip->p->value) ? "[x]" : "[ ]"); - break; - case PROP_TYPE_SLIDER: { - char c[16]; - sprintf(c, "%.2f", ((struct slider_value *)ip->p->value)->value); - ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, c); - break; - } - } - } - if (ip && i == n->max_props && i != n->props.size) { - ncplane_putstr_yx(u->ip, ip->text.y + 1, (INFO_TILE_WIDTH - 1) / 2, "V"); - } -} - -static void set_highlighted_style(struct tile *t, s32 br, s32 bg, s32 bb, s32 fr, s32 fg, s32 fb) -{ - ncplane_set_fg_rgb8(t->p, br, bg, bb); - ncplane_set_bg_alpha(t->p, NCALPHA_TRANSPARENT); - u64 c = 0; - ncchannels_set_bg_rgb8(&c, br, bg, bb); - ncchannels_set_fg_rgb8(&c, br, bg, bb); - c = ncchannels_reverse(c); - ncplane_gradient(t->p, 1, 0, TILE_HEIGHT - 1, TILE_WIDTH, "", 0, c, c, c, c); - for (s32 i = 0; i < TILE_WIDTH; i++) { - ncplane_putwc_yx(t->p, 0, i, L'▂'); - } - ncplane_set_bg_rgb8(t->p, br, bg, bb); - ncplane_set_fg_rgb8(t->p, fr, fg, fb); - ncplane_set_bg_alpha(t->p, NCALPHA_OPAQUE); - ncplane_set_styles(t->p, NCSTYLE_BOLD); -} - -void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text) -{ - if (!u->layed_out) return; - - struct ncvisual_options vopts = { 0 }; - vopts.scaling = NCSCALE_SCALE; - vopts.blitter = PIXEL_BLIT ? NCBLIT_PIXEL : NCBLIT_2x1; - - s32 index = (g->selected_index - (g->selected_index % g->tiles_per_page)); - - struct tile *t; - al_array_foreach_ptr(g->tiles, i, t) { - if (!t->e) continue; - if (t->e->has_preview) { - struct image *img = get_image_from_pool(&u->pool, &t->e->preview_path); - if (img && !t->blitted && !img->errored) { - vopts.n = t->ip; - ncvisual_blit(u->nc, img->v, &vopts); - t->blitted = true; - } - } - if (g->flash <= 0.f && u->selected_menu == MENU_GRID && index == g->selected_index) { - set_highlighted_style(t, 211, 184, 152, 13, 22, 45); - } else if (g->set_index >= 0 && index == g->set_index) { - set_highlighted_style(t, 242, 233, 215, 13, 22, 45); - g->flash -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); - } else { - ncplane_erase(t->p); - ncplane_set_fg_rgb8(t->p, 0xff, 0xff, 0xff); - ncplane_set_bg_palindex(t->p, 0); - ncplane_set_styles(t->p, 0); - } - if (t->text.s && t->text.s->len) { - draw_scrolling_text(t->p, &t->text, update_text); - } - index++; - } -} - -static bool handle_input_monitors(struct ui *u, struct monitors *m, struct ncinput *input) -{ - bool should_redraw_monitors = false; - bool should_redraw_grid = false; - bool should_redraw_info = false; - switch (input->id) { - case NCKEY_RIGHT: - case 'l': - if ((u32)(m->selected_monitor + 1) >= m->monitors.size) { - return false; - } - m->selected_monitor++; - should_redraw_monitors = true; - break; - case NCKEY_LEFT: - case 'h': - if (m->selected_monitor - 1 < 0) { - return false; - } - m->selected_monitor--; - should_redraw_monitors = true; - break; - case NCKEY_RETURN: - select_monitor(u); - m->flash = FLASH_DURATION; - should_redraw_monitors = true; - should_redraw_grid = true; - should_redraw_info = true; - break; - } - if (should_redraw_monitors) { - draw_monitors(u, m); - } - if (should_redraw_grid) { - set_grid_tiles(u, &u->g); - draw_grid_tiles(u, &u->g, false); - } - if (should_redraw_info) { - set_info_entry(u, &u->n); - draw_info(u, &u->n, false); - } - return should_redraw_monitors || should_redraw_grid || should_redraw_info; -} - -static bool handle_input_info(struct ui *u, struct info *n, struct ncinput *input) -{ - bool should_redraw_info = false; - switch (input->id) { - case NCKEY_DOWN: - case 'j': - if ((u32)(n->selected_prop + 1) >= n->props.size) { - return false; - } - n->selected_prop++; - should_redraw_info = true; - break; - case NCKEY_UP: - case 'k': - if (n->selected_prop - 1 < 0) { - return false; - } - n->selected_prop--; - should_redraw_info = true; - break; - case NCKEY_RETURN: - case NCKEY_SPACE: { - if (!n->props.size) return false; - struct prop *p = &al_array_at(n->e->props, n->selected_prop); - if (p->type == PROP_TYPE_BOOL) { - *((bool *)p->value) = !*((bool *)p->value); - write_selected_entry_to_config(u); - should_redraw_info = true; - } - break; - } - case NCKEY_LEFT: - case 'h': - case NCKEY_RIGHT: - case 'l': - if (!n->props.size) return false; - struct prop *p = &al_array_at(n->e->props, n->selected_prop); - if (p->type == PROP_TYPE_SLIDER) { - struct slider_value *value = (struct slider_value *)p->value; - if (input->id == NCKEY_RIGHT) { - if (value->value == value->max) { - break; - } - if (value->value + value->step <= value->max) { - value->value += value->step; - } else { - value->value = value->max; - } - } else if (input->id == NCKEY_LEFT) { - if (value->value == value->min) { - break; - } - if (value->value - value->step >= value->min) { - value->value -= value->step; - } else { - value->value = value->min; - } - } - write_selected_entry_to_config(u); - should_redraw_info = true; - } - break; - } - if (should_redraw_info) draw_info(u, n, false); - return should_redraw_info; -} - -static bool grid_next_page(struct ui *u, struct grid *g, s32 offset) -{ - s32 max = (s32)u->db.entries.size - 1; - if (g->selected_index == max) { - return false; - } - g->selected_index += g->tiles_per_page - offset; - if (g->selected_index > max) { - g->selected_index = max; - } - return true; -} - -static bool grid_previous_page(struct grid *g, s32 offset) -{ - if (g->selected_index == 0) { - return false; - } - g->selected_index -= g->tiles_per_page - offset; - if (g->selected_index < 0) { - g->selected_index = 0; - } - return true; -} - -static bool handle_input_grid(struct ui *u, struct grid *g, struct ncinput *input) -{ - bool should_update_grid = false; - bool should_redraw_grid = false; - bool should_redraw_info = false; - switch (input->id) { - case NCKEY_RIGHT: - case 'l': - if (g->selected_index + 1 >= (s32)u->db.entries.size) { - return false; - } - if ((g->selected_index + 1) % g->tiles_x == 0) { - should_update_grid = grid_next_page(u, &u->g, g->tiles_x - 1); - } else { - g->selected_index++; - } - should_redraw_info = true; - should_redraw_grid = true; - break; - case NCKEY_LEFT: - case 'h': - if (g->selected_index - 1 < 0) { - return false; - } - if (g->selected_index % g->tiles_x == 0) { - should_update_grid = grid_previous_page(&u->g, g->tiles_x - 1); - } else { - g->selected_index--; - } - should_redraw_info = true; - should_redraw_grid = true; - break; - case NCKEY_DOWN: - case 'j': - if (g->selected_index + g->tiles_x > g->max_index) { - return false; - } - g->selected_index += g->tiles_x; - should_redraw_info = true; - should_redraw_grid = true; - break; - case NCKEY_UP: - case 'k': - if (g->selected_index - g->tiles_x < g->min_index) { - return false; - } - g->selected_index -= g->tiles_x; - should_redraw_info = true; - should_redraw_grid = true; - break; - case 'b': - should_update_grid = grid_previous_page(&u->g, 0); - should_redraw_info = true; - should_redraw_grid = true; - break; - case 'f': - should_update_grid = grid_next_page(u, &u->g, 0); - should_redraw_info = true; - should_redraw_grid = true; - break; - case 'r': - g->set_index = -1; - write_selected_entry_to_config(u); - should_redraw_grid = true; - should_redraw_info = true; - break; - case NCKEY_RETURN: - case NCKEY_SPACE: { - if (g->selected_index >= 0) { - g->set_index = g->selected_index; - g->flash = FLASH_DURATION; - } - write_selected_entry_to_config(u); - should_redraw_grid = true; - should_redraw_info = true; - break; - } - } - if (should_update_grid) { - set_grid_tiles(u, g); - } - if (should_redraw_grid) { - draw_grid_tiles(u, g, false); - } - if (should_redraw_info) { - set_info_entry(u, &u->n); - draw_info(u, &u->n, false); - } - return should_redraw_grid || should_redraw_info; -} - -static void input_poll_callback(void *userdata, s32 revents) -{ - struct ui *u = (struct ui *)userdata; - (void)revents; - struct ncinput input; - bool should_render = false; - u32 ret; - while (1) { - ret = notcurses_get_nblock(u->nc, &input); - if (ret == (u32)-1 || ret == 0) { - break; - } - if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) { - if (input.id == NCKEY_TAB) { - switch (u->selected_menu) { - case MENU_GRID: - if (u->n.props.size > 0) { - u->selected_menu = MENU_INFO; - } else { - u->selected_menu = MENU_MONITORS; - } - break; - case MENU_INFO: - if (u->m.selected_monitor >= 0) { - u->selected_menu = MENU_MONITORS; - } else { - u->selected_menu = MENU_GRID; - } - break; - case MENU_MONITORS: - u->selected_menu = MENU_GRID; - break; - } - draw_info(u, &u->n, false); - draw_monitors(u, &u->m); - draw_grid_tiles(u, &u->g, false); - should_render = true; - } else if (input.id == 'q') { - aki_event_loop_break(&u->loop); - } - switch (u->selected_menu) { - case MENU_GRID: - should_render |= handle_input_grid(u, &u->g, &input); - break; - case MENU_INFO: - should_render |= handle_input_info(u, &u->n, &input); - break; - case MENU_MONITORS: - should_render |= handle_input_monitors(u, &u->m, &input); - break; - } - } - } - if (should_render) notcurses_render(u->nc); -} - -static void timer_callback(void *userdata, struct aki_timer *timer) -{ - struct ui *u = (struct ui *)userdata; - (void)timer; - - if (!u->layed_out) { - notcurses_render(u->nc); - return; - } - - if (u->text_update <= 0.f) { - u->text_update = TEXT_UPDATE_RATE; - draw_grid_tiles(u, &u->g, true); - draw_info(u, &u->n, true); - } else { - u->text_update -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); - draw_grid_tiles(u, &u->g, false); - draw_info(u, &u->n, false); - } - - draw_monitors(u, &u->m); - - notcurses_render(u->nc); - - aki_timer_set_repeat(&u->timer, AKI_TS_FROM_USEC(u->delay)); - aki_timer_again(&u->timer); -} - -static struct ui u = { 0 }; - -s32 main(void) -{ - if (!aki_common_init()) return EXIT_FAILURE; - - s32 ret = EXIT_FAILURE; - - if (!open_config(&u.conf)) goto out; - - if (!db_load_entries(&u.db, &u.conf.wallpaper_dir)) { - close_config(&u.conf); - goto out; - } - - if (!init_ui(&u)) { - close_config(&u.conf); - goto out; - } - - notcurses_stddim_yx(u.nc, &u.term_rows, &u.term_cols); - - ncplane_set_resizecb(notcurses_stdplane(u.nc), resize_cb); - ncplane_set_userptr(notcurses_stdplane(u.nc), &u); - - if (!layout_ui(&u)) { - notcurses_stop(u.nc); - close_config(&u.conf); - al_log_error("taro", "failed to layout ui, your terminal window is probably too small"); - goto out; - } - u.layed_out = true; - draw_monitors(&u, &u.m); - - aki_event_loop_init(&u.loop); - - aki_poll_init(&u.input_poll, input_poll_callback, &u); - aki_poll_set(&u.input_poll, notcurses_inputready_fd(u.nc), AKI_POLL_READ); - aki_poll_start(&u.input_poll, &u.loop); - - aki_timer_init(&u.timer, &u.loop, timer_callback, &u); - u.delay = 1000000; // 1fps. - timer_callback(&u, NULL); - - aki_event_loop_run(&u.loop); - - notcurses_stop(u.nc); - - close_config(&u.conf); - - ret = EXIT_SUCCESS; - -out: - aki_common_close(); - - return ret; -} diff --git a/taro/subprojects/libakiyo.wrap b/taro/subprojects/libakiyo.wrap deleted file mode 100644 index 2cd2caa..0000000 --- a/taro/subprojects/libakiyo.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -url = https://git.akon.city/libakiyo -revision = master -depth = 1 diff --git a/taro/subprojects/libalabaster.wrap b/taro/subprojects/libalabaster.wrap deleted file mode 100644 index c0358cf..0000000 --- a/taro/subprojects/libalabaster.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -url = https://git.akon.city/libalabaster -revision = master -depth = 1 diff --git a/taro/subprojects/notcurses.wrap b/taro/subprojects/notcurses.wrap deleted file mode 100644 index 64f6731..0000000 --- a/taro/subprojects/notcurses.wrap +++ /dev/null @@ -1,12 +0,0 @@ -[wrap-git] -url = https://github.com/dankamongmen/notcurses.git -revision = 9377370b59e31f3f3adde6eaa89aa875dd84377f -depth = 1 - -patch_directory = notcurses -diff_files = notcurses/duration_and_ffmpeg_fix.diff - -method = cmake - -[provide] -dependency_names = notcurses, notcurses-core diff --git a/taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff b/taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff deleted file mode 100644 index 71680f7..0000000 --- a/taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff +++ /dev/null @@ -1,166 +0,0 @@ -diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h -index 9e09cb3..f4da60f 100644 ---- a/include/notcurses/notcurses.h -+++ b/include/notcurses/notcurses.h -@@ -3406,6 +3406,10 @@ API int ncvisual_geom(const struct notcurses* nc, const struct ncvisual* n, - const struct ncvisual_options* vopts, ncvgeom* geom) - __attribute__ ((nonnull (4))); - -+ -+API uint64_t ncvisual_get_duration(const struct ncvisual* n) -+ __attribute__ ((nonnull (1))); -+ - // Destroy an ncvisual. Rendered elements will not be disrupted, but the visual - // can be neither decoded nor rendered any further. - API void ncvisual_destroy(struct ncvisual* ncv); -diff --git a/src/lib/visual-details.h b/src/lib/visual-details.h -index e9347e3..6852b76 100644 ---- a/src/lib/visual-details.h -+++ b/src/lib/visual-details.h -@@ -25,6 +25,7 @@ typedef struct ncvisual { - unsigned pixx, pixy; // pixel geometry, *not* cell geometry - // lines are sometimes padded. this many true bytes per row in data. - unsigned rowstride; -+ uint64_t duration; - bool owndata; // we own data iff owndata == true - } ncvisual; - -diff --git a/src/lib/visual.c b/src/lib/visual.c -index 047b68e..25e8422 100644 ---- a/src/lib/visual.c -+++ b/src/lib/visual.c -@@ -458,6 +458,10 @@ int ncvisual_geom(const notcurses* nc, const ncvisual* n, - &disppxy, &disppxx, &outy, &outx, &placey, &placex); - } - -+uint64_t ncvisual_get_duration(const ncvisual* n){ -+ return n->duration; -+} -+ - void* rgb_loose_to_rgba(const void* data, int rows, int* rowstride, int cols, int alpha){ - if(*rowstride % 4){ // must be a multiple of 4 bytes - return NULL; -diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c -index 655f269..d2bf399 100644 ---- a/src/media/ffmpeg.c -+++ b/src/media/ffmpeg.c -@@ -33,6 +33,7 @@ typedef struct ncvisual_details { - struct SwsContext* swsctx; - struct SwsContext* rgbactx; - AVSubtitle subtitle; -+ AVRational timebase; - int stream_index; // match against this following av_read_frame() - int sub_stream_index; // subtitle stream index, can be < 0 if no subtitles - bool packet_outstanding; -@@ -224,9 +225,7 @@ struct ncplane* ffmpeg_subtitle(ncplane* parent, const ncvisual* ncv){ - - static int - averr2ncerr(int averr){ -- if(averr == AVERROR_EOF){ -- return 1; -- } -+ (void)averr; - // FIXME need to map averror codes to ncerrors - //fprintf(stderr, "AVERR: %d/%x %d/%x\n", averr, averr, -averr, -averr); - return -1; -@@ -326,6 +325,11 @@ ffmpeg_decode(ncvisual* n){ - /*if(averr != AVERROR_EOF){ - fprintf(stderr, "Error reading frame info (%s)\n", av_err2str(averr)); - }*/ -+ if(averr == AVERROR_EOF){ -+ // Start flush. -+ unref = false; -+ break; -+ } - return averr2ncerr(averr); - } - unref = true; -@@ -339,10 +343,10 @@ ffmpeg_decode(ncvisual* n){ - } - }while(n->details->packet->stream_index != n->details->stream_index); - n->details->packet_outstanding = true; -- int averr = avcodec_send_packet(n->details->codecctx, n->details->packet); -+ int averr = avcodec_send_packet(n->details->codecctx, unref ? n->details->packet : NULL); - if(averr < 0){ -- n->details->packet_outstanding = false; - av_packet_unref(n->details->packet); -+ n->details->packet_outstanding = false; - //fprintf(stderr, "Error processing AVPacket\n"); - return averr2ncerr(averr); - } -@@ -350,10 +354,16 @@ ffmpeg_decode(ncvisual* n){ - int averr = avcodec_receive_frame(n->details->codecctx, n->details->frame); - if(averr >= 0){ - have_frame = true; -+ }else if(averr == AVERROR_EOF){ -+ n->details->packet_outstanding = false; -+ // Where EOF gets returned during normal operation. -+ return 1; - }else if(averr < 0){ -- av_packet_unref(n->details->packet); -- have_frame = false; -+ if(unref){ -+ av_packet_unref(n->details->packet); -+ } - n->details->packet_outstanding = false; -+ have_frame = false; - if(averr != AVERROR(EAGAIN)){ - return averr2ncerr(averr); - } -@@ -365,6 +375,8 @@ ffmpeg_decode(ncvisual* n){ - n->rowstride = f->linesize[0]; - n->pixx = n->details->frame->width; - n->pixy = n->details->frame->height; -+ const uint64_t pktduration = ffmpeg_pkt_duration(f); -+ n->duration = av_rescale_q(pktduration, n->details->timebase, AV_TIME_BASE_Q); - //fprintf(stderr, "good decode! %d/%d %d %p\n", n->details->frame->height, n->details->frame->width, n->rowstride, f->data); - ncvisual_set_data(n, f->data[0], false); - force_rgba(n); -@@ -457,6 +469,7 @@ ffmpeg_from_file(const char* filename){ - goto err; - } - AVStream* st = ncv->details->fmtctx->streams[ncv->details->stream_index]; -+ ncv->details->timebase = st->time_base; - if((ncv->details->codecctx = avcodec_alloc_context3(ncv->details->codec)) == NULL){ - //fprintf(stderr, "Couldn't allocate decoder for %s\n", filename); - goto err; -@@ -501,14 +514,6 @@ ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale, - memcpy(&activevopts, vopts, sizeof(*vopts)); - int ncerr; - do{ -- // codecctx seems to be off by a factor of 2 regularly. instead, go with -- // the time_base from the avformatctx. except ts isn't properly reset for -- // all media when we loop =[. we seem to be accurate enough now with the -- // tbase/ppd. see https://github.com/dankamongmen/notcurses/issues/1352. -- double tbase = av_q2d(ncv->details->fmtctx->streams[ncv->details->stream_index]->time_base); -- if(isnan(tbase)){ -- tbase = 0; -- } - if(activevopts.n){ - ncplane_erase(activevopts.n); // new frame could be partially transparent - } -@@ -529,7 +534,7 @@ ffmpeg_stream(notcurses* nc, ncvisual* ncv, float timescale, - } - // display duration in units of time_base - const uint64_t pktduration = ffmpeg_pkt_duration(ncv->details->frame); -- uint64_t duration = pktduration * tbase * NANOSECS_IN_SEC; -+ uint64_t duration = av_rescale_q(pktduration, ncv->details->timebase, (AVRational){1, NANOSECS_IN_SEC}); - double schedns = nsbegin; - sum_duration += (duration * timescale); - schedns += sum_duration; -@@ -561,11 +566,13 @@ static int - ffmpeg_decode_loop(ncvisual* ncv){ - int r = ffmpeg_decode(ncv); - if(r == 1){ -- if(av_seek_frame(ncv->details->fmtctx, ncv->details->stream_index, 0, AVSEEK_FLAG_FRAME) < 0){ -+ avformat_flush(ncv->details->fmtctx); -+ if(avformat_seek_file(ncv->details->fmtctx, ncv->details->stream_index, 0, 0, 0, AVSEEK_FLAG_FRAME) < 0){ - // FIXME log error - return -1; - } -- if(ffmpeg_decode(ncv) < 0){ -+ avcodec_flush_buffers(ncv->details->codecctx); -+ if(ffmpeg_decode(ncv) != 0){ - return -1; - } - } diff --git a/taro/subprojects/stela.wrap b/taro/subprojects/stela.wrap deleted file mode 100644 index 7807a8c..0000000 --- a/taro/subprojects/stela.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -url = https://git.akon.city/stela -revision = master -depth = 1 diff --git a/taro/ui.c b/taro/ui.c new file mode 100644 index 0000000..5bcaf1d --- /dev/null +++ b/taro/ui.c @@ -0,0 +1,1226 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "db.h" +#include "file_ext.h" +#include "config.h" + +#define PIXEL_BLIT 1 + +#define TIMER_UPDATE_BASE 1.f + +#define TEXT_UPDATE_RATE 0.4f +#define FLASH_DURATION 0.4f + +#define UI_RESERVE_WIDTH 33 +#define UI_RESERVE_HEIGHT 2 + +#define GRID_TILE_WIDTH 18 // img = 16 +#define GRID_TILE_HEIGHT 10 // img = 8 + +#define GRID_TEXT_WIDTH (GRID_TILE_WIDTH - 4) + +#define INFO_X_OFFSET 2 +#define INFO_Y_OFFSET 1 + +#define INFO_WIDTH 30 +// INFO_HEIGHT = term_rows + UI_RESERVE_HEIGHT + +#define INFO_IMG_WIDTH 24 +#define INFO_IMG_HEIGHT 12 + +#define INFO_TEXT_WIDTH (GRID_TEXT_WIDTH + 6) + +#define PROP_TEXT_WIDTH 16 + +#define MAX_MONITOR_NAME 255 + +struct scrolling_text { + wstr *s; + s32 y; + s32 x; + s32 width; + u32 index; + s32 skip; +}; + +struct image { + u32 hash; + bool gif; + bool errored; + struct ncvisual *v; +}; + +struct image_pool { + u32 max; + u32 chunk_size; + array(struct image) images; +}; + +struct tile { + struct ncplane *p, *ip; + struct entry *e; + bool blitted; + struct scrolling_text text; +}; + +struct monitors { + array(char *) monitors; + s32 selected_monitor; + s32 set_monitor; + f32 flash; + str selection_path; +}; + +struct grid { + struct ncplane *p; + s32 tiles_x; + s32 tiles_y; + s32 min_index; + s32 max_index; + s32 selected_index; + s32 set_index; + f32 flash; + u32 tiles_per_page; + array(struct tile) tiles; +}; + +struct info_prop { + struct prop *p; + struct scrolling_text text; +}; + +struct info { + struct ncplane *ip; + struct entry *e; + bool blitted; + struct scrolling_text text; + u32 max_props; + s32 selected_prop; + array(struct info_prop) props; +}; + +enum { + MENU_GRID = 0, + MENU_INFO, + MENU_MONITORS +}; + +struct ui { + struct notcurses *nc; + struct aki_event_loop loop; + struct aki_poll input_poll; + u64 delay; + struct aki_timer timer; + bool layed_out; + f32 text_update; + u32 term_rows; + u32 term_cols; + u8 selected_menu; + struct ncplane *p, *mp, *gp, *ip; + struct monitors m; + struct grid g; + struct info n; + struct image_pool pool; + struct db db; + struct config conf; +}; + +// --------- +// Client +// --------- + +static s32 get_entry_from_config(struct ui *u) +{ + s32 ret = -1; + str config; + if (aki_file_open_and_read_wholly(&u->m.selection_path, &config, false)) { + s32 index = al_str_find(&config, '|'); + if (index > 2) { + str *id = al_str_substr(&config, 0, index - 2); + struct entry *e; + al_array_foreach_ptr(u->db.entries, i, e) { + if (al_str_eq(&e->id, id)) { + ret = i; + break; + } + } + } + al_str_free(&config); + } + return ret; +} + +static void select_monitor(struct ui *u) +{ + if (u->m.set_monitor == -1) return; + u->m.set_monitor = u->m.selected_monitor; + al_str_free(&u->m.selection_path); + al_str_clone(&u->m.selection_path, &u->conf.config_dir); + al_str_cat(&u->m.selection_path, al_str_c("/")); + char *monitor = al_array_at(u->m.monitors, u->m.set_monitor); + al_str_cat(&u->m.selection_path, al_str_cr(monitor)); + al_str_cat(&u->m.selection_path, al_str_c("/selection")); + u->g.set_index = get_entry_from_config(u); + if (u->g.set_index >= 0) { + u->g.selected_index = u->g.set_index; + } +} + +static void write_selected_entry_to_config(struct ui *u) +{ + if (u->m.set_monitor == -1) return; + str config; + if (u->g.set_index == -1) { + al_str_from(&config, "||"); + } else { + al_str_clone(&config, &u->n.e->id); + if (u->n.e->has_package) { + al_str_cat(&config, al_str_c("_p")); + } else { + al_str_cat(&config, al_str_c("_d")); + } + al_str_cat(&config, al_str_c("|false,100|")); + struct prop *p; + al_array_foreach_ptr(u->n.e->props, i, p) { + switch (p->type) { + case PROP_TYPE_BOOL: + al_str_cat(&config, *((bool *)p->value) ? al_str_c("true") : al_str_c("false")); + break; + } + al_str_cat(&config, al_str_c(",")); + } + if (al_str_at(&config, config.len - 1) == ',') { + config.len--; + } + } + aki_file_open_and_replace(&u->m.selection_path, &config, true); +} + +// --------- +// Interface +// --------- + +static bool wide_string_to_width(wstr *w, u32 index, s32 max_width, void (*func)(void *, wchar_t), void *userdata) +{ + u32 i = index; + s32 width = 0; + do { + wchar_t wc = al_wstr_at(w, i); + width += al_wchar_width(wc); + if (width >= max_width) { + break; + } + func(userdata, wc); + } while (++i < w->len); + return i == w->len; +} + +static void set_scrolling_text(struct scrolling_text *text, wstr *s, s32 y, s32 x, s32 width) +{ + text->s = s; + text->y = y; + text->x = x; + text->width = width; + text->index = 0; + text->skip = al_wchar_width(al_wstr_at(s, text->index)); +} + +static void draw_func(void *userdata, wchar_t wc) +{ + struct ncplane *p = (struct ncplane *)userdata; + ncplane_putwc(p, wc); +} + +static void draw_scrolling_text(struct ncplane *p, struct scrolling_text *text, bool update) +{ + ncplane_cursor_move_yx(p, text->y, text->x); + bool end = wide_string_to_width(text->s, text->index, text->width, draw_func, p); + if (update && !--text->skip) { + text->skip = al_wchar_width(al_wstr_at(text->s, text->index)); + text->index = end ? 0 : text->index + 1; + } +} + +static void set_info_entry(struct ui *u, struct info *n) +{ + n->e = &al_array_at(u->db.entries, u->g.selected_index); + n->blitted = false; + + s32 width = al_wstr_width(&u->n.e->title); + s32 tx = 2 + (width < INFO_TEXT_WIDTH ? (INFO_TEXT_WIDTH - width) / 2 : 0); + set_scrolling_text(&n->text, &u->n.e->title, INFO_IMG_HEIGHT + 1, tx, INFO_TEXT_WIDTH); + + n->selected_prop = 0; + n->props.size = 0; + struct prop *p; + struct info_prop *ip; + al_array_foreach_ptr(u->n.e->props, i, p) { + al_array_push(n->props, (struct info_prop){ 0 }); + ip = &al_array_last(n->props); + ip->p = p; + s32 ty = INFO_IMG_HEIGHT + 3 + ((i % n->max_props) * 2); + set_scrolling_text(&ip->text, &p->title, ty, 0, PROP_TEXT_WIDTH); + } +} + +static bool init_info(struct info *n) +{ + al_array_init(n->props); + return true; +} + +static bool init_image_pool(struct image_pool *pool) +{ + pool->chunk_size = 0; + pool->max = 0; + al_array_init(pool->images); + return true; +} + +static bool init_grid(struct grid *g) +{ + al_array_init(g->tiles); + return true; +} + +static s32 monitor_cmp(const void *va, const void *vb) +{ + return strncmp(*(char **)va, *(char **)vb, MAX_MONITOR_NAME); +} + +static bool init_monitors(struct ui *u, struct monitors *m) +{ + al_array_init(m->monitors); + str monitors_path; + al_str_clone(&monitors_path, &u->conf.config_dir); + al_str_cat(&monitors_path, al_str_c("/monitors")); + str monitors; + bool ret = aki_file_open_and_read_wholly(&monitors_path, &monitors, false); + al_str_free(&monitors_path); + if (!ret) { + al_log_error("taro", "failed to open monitor list, has `tarod` run?"); + return false; + } + str monitor_dir; + str monitor = al_str_zero(); + while (al_str_get_line(&monitors, '\n', &monitor)) { + al_str_clone(&monitor_dir, &u->conf.config_dir); + al_str_cat(&monitor_dir, al_str_c("/")); + al_str_cat(&monitor_dir, &monitor); + if (aki_dir_exists(&monitor_dir)) { + char *c_str = al_str_to_c_str(&monitor); + al_array_push(m->monitors, c_str); + } + al_str_free(&monitor_dir); + } + if (!u->m.monitors.size) { + u->m.set_monitor = -1; + u->m.selected_monitor = -1; + } + al_array_sort(m->monitors, char *, monitor_cmp); + u->g.set_index = -1; + if (m->selected_monitor >= 0) { + select_monitor(u); + } + if (u->db.entries.size > 0) { + u->g.selected_index = (u->g.set_index >= 0) ? u->g.set_index : 0; + } else { + u->g.selected_index = -1; + } + return true; +} + +static bool init_ui(struct ui *u) +{ + if (!init_monitors(u, &u->m)) return false; + if (!init_grid(&u->g)) return false; + if (!init_image_pool(&u->pool)) return false; + if (!init_info(&u->n)) return false; + if (!(u->nc = notcurses_init(NULL, stdin))) { + al_log_error("taro", "failed to initialize notcurses"); + return false; + } + return true; +} + +struct image *get_image_from_pool(struct image_pool *pool, str *path) +{ + u32 hash = al_str_hash(path); + struct image *rimg; + al_array_foreach_ptr(pool->images, i, rimg) { + if (rimg->hash == hash) { + return rimg; + } + } + + bool gif = false; + s32 index = al_str_rfind(path, '.'); + if (index != -1) { + if (al_str_eq(al_str_w(path->data, index, 4), al_str_c(".gif"))) { + gif = true; + } + } + + if (pool->images.size >= pool->max) { + for (u32 i = 0; i < pool->chunk_size; i++) { + ncvisual_destroy(al_array_at(pool->images, i).v); + } + al_array_remove_range(pool->images, 0, pool->chunk_size); + } + + char *c_str = al_str_to_c_str(path); + struct image img = { + .hash = hash, + .gif = gif, + .errored = false, + .v = ncvisual_from_file(c_str) + }; + al_free(c_str); + if (!img.v) return NULL; + al_array_push(pool->images, img); + + return &al_array_last(pool->images); +} + +static bool layout_ui(struct ui *u); +static void draw_monitors(struct ui *u, struct monitors *m); +static void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text); +static void draw_info(struct ui *u, struct info *n, bool update_text); + +static s32 resize_cb(struct ncplane *p) +{ + struct ui *u = ncplane_userptr(p); + if (!u) return 0; + notcurses_stddim_yx(u->nc, &u->term_rows, &u->term_cols); + if (!layout_ui(u)) { + u->layed_out = false; + return 0; + } + u->layed_out = true; + struct tile *t; + al_array_foreach_ptr(u->g.tiles, i, t) { + t->blitted = false; + ncplane_erase(t->ip); + } + u->n.blitted = false; + ncplane_erase(u->n.ip); + // To not break images we need to erase them + // then render them in 2 steps. + notcurses_refresh(u->nc, NULL, NULL); + notcurses_render(u->nc); + draw_monitors(u, &u->m); + draw_grid_tiles(u, &u->g, false); + draw_info(u, &u->n, false); + notcurses_render(u->nc); + return 0; +} + +static void maybe_destroy_info(struct info *n) +{ + if (n->ip) { + ncplane_destroy(n->ip); + n->ip = NULL; + } +} + +static bool layout_info(struct ui *u, struct info *n, struct ncplane *parent, s32 height) +{ + maybe_destroy_info(n); + + n->max_props = (height - INFO_IMG_HEIGHT - 3) / 2; + if (!n->max_props) return false; + + struct ncplane_options nopts = { 0 }; + + nopts.cols = INFO_IMG_WIDTH; + nopts.rows = INFO_IMG_HEIGHT; + nopts.x = 0; + nopts.y = 1; + + n->ip = ncplane_create(parent, &nopts); + if (!n->ip) { + maybe_destroy_info(n); + return false; + } + + if (u->g.selected_index >= 0) { + set_info_entry(u, &u->n); + } + + return true; +} + +static void maybe_destroy_grid(struct grid *g) +{ + if (!g->p) return; + struct tile *t; + al_array_foreach_ptr(g->tiles, i, t) { + ncplane_destroy(t->ip); + t->ip = NULL; + ncplane_destroy(t->p); + t->p = NULL; + } + g->tiles.size = 0; + ncplane_destroy(g->p); + g->p = NULL; +} + +static void set_grid_tiles(struct ui *u, struct grid *g) +{ + u32 index = (g->selected_index - (g->selected_index % g->tiles_per_page)); + + g->min_index = index; + g->max_index = index; + + for (s32 y = 0; y < g->tiles_y; y++) { + for (s32 x = 0; x < g->tiles_x; x++) { + struct tile *t = &al_array_at(g->tiles, y * g->tiles_x + x); + ncplane_erase(t->ip); + t->blitted = false; + ncplane_erase(t->p); + + while (g->max_index < (s32)u->db.entries.size) { + t->e = &al_array_at(u->db.entries, g->max_index); + if (db_ensure_entry_loaded(&u->db, t->e)) { + break; + } + } + + al_assert(g->max_index <= (s32)u->db.entries.size); + + if (g->max_index == (s32)u->db.entries.size) { + t->e = NULL; + t->text.s = NULL; + ncplane_erase(t->ip); + continue; + } + + s32 width = al_wstr_width(&t->e->title); + s32 tx = 2 + (width < GRID_TEXT_WIDTH ? (GRID_TEXT_WIDTH - width) / 2 : 0); + set_scrolling_text(&t->text, &t->e->title, GRID_TILE_HEIGHT - 1, tx, GRID_TEXT_WIDTH); + + g->max_index++; + }} + + g->max_index--; + + // This can happen in grid_next_page() and shuffle. Any other case where + // `selected_index` goes over `max_index` is undefined. Handling it this way + // saves us from a lot of pre-loaded. + if (g->selected_index > g->max_index) g->selected_index = g->max_index; +} + +static bool layout_grid(struct ui *u, struct grid *g, struct ncplane *parent, s32 width, s32 height) +{ + maybe_destroy_grid(g); + + s32 inner_width = width; + s32 inner_height = height; + + g->tiles_x = width / GRID_TILE_WIDTH; + g->tiles_y = height / GRID_TILE_HEIGHT; + + if (!g->tiles_x || !g->tiles_y) { + // Window is too small. + return false; + } + + g->tiles_per_page = g->tiles_x * g->tiles_y; + + // Reserve space for 4 pages, but never shrink. + if (u->pool.chunk_size < g->tiles_per_page) { + u->pool.chunk_size = g->tiles_per_page; + u->pool.max = u->pool.chunk_size * 4; + al_array_reserve(u->pool.images, u->pool.max); + } + + while (inner_width && inner_width % g->tiles_x != 0) { + inner_width--; + } + + while (inner_height && inner_height % g->tiles_y != 0) { + inner_height--; + } + + struct ncplane_options nopts = { 0 }; + + nopts.cols = inner_width; + nopts.rows = inner_height; + nopts.x = (width - (g->tiles_x * GRID_TILE_WIDTH)) / 2; + nopts.y = (height - (g->tiles_y * GRID_TILE_HEIGHT)) / 2; + + g->p = ncplane_create(parent, &nopts); + if (!g->p) return false; + + for (s32 y = 0; y < g->tiles_y; y++) { + for (s32 x = 0; x < g->tiles_x; x++) { + struct tile t = { 0 }; + + nopts.cols = GRID_TILE_WIDTH; + nopts.rows = GRID_TILE_HEIGHT; + nopts.x = x * GRID_TILE_WIDTH; + nopts.y = y * GRID_TILE_HEIGHT; + + t.p = ncplane_create(g->p, &nopts); + if (!t.p) { + maybe_destroy_grid(g); + return false; + } + + nopts.cols = GRID_TILE_WIDTH - 2; + nopts.rows = GRID_TILE_HEIGHT - 2; + nopts.x = 1; + nopts.y = 1; + + t.ip = ncplane_create(t.p, &nopts); + if (!t.ip) { + maybe_destroy_grid(g); + return false; + } + + al_array_push(g->tiles, t); + }} + + if (g->selected_index >= 0) { + set_grid_tiles(u, g); + } + + return true; +} + +static void maybe_destroy_ui(struct ui *u) +{ + if (u->ip) { + ncplane_destroy(u->ip); + u->ip = NULL; + } + + if (u->gp) { + ncplane_destroy(u->gp); + u->gp = NULL; + } + + if (u->mp) { + ncplane_destroy(u->mp); + u->mp = NULL; + } + + if (u->p) { + ncplane_destroy(u->p); + u->p = NULL; + } +} + +bool layout_ui(struct ui *u) +{ + bool even = u->term_cols % 2 == 0; + + u32 rwidth = (even) ? UI_RESERVE_WIDTH + 1 : UI_RESERVE_WIDTH; + u32 rheight = (even) ? UI_RESERVE_HEIGHT + 1 : UI_RESERVE_HEIGHT; + + if (u->term_cols < GRID_TILE_WIDTH + rwidth || u->term_rows < GRID_TILE_HEIGHT + rheight) { + return false; + } + + maybe_destroy_ui(u); + + struct ncplane_options nopts = { 0 }; + + nopts.rows = u->term_rows; + nopts.cols = u->term_cols; + + u->p = ncplane_create(notcurses_stdplane(u->nc), &nopts); + if (!u->p) { + return false; + } + + s32 cols = (s32)u->term_cols - rwidth; + s32 rows = (s32)u->term_rows - rheight; + + if (cols < 0 || rows < 0) { + maybe_destroy_ui(u); + return false; + } + + nopts.x = 1; + nopts.y = 1; + nopts.cols = cols - 1; + nopts.rows = 1; + + u->mp = ncplane_create(u->p, &nopts); + if (!u->mp) { + maybe_destroy_ui(u); + return false; + } + + nopts.x = 1; + nopts.y = 2; + nopts.cols = cols - 1; + nopts.rows = rows - 2; + + u->gp = ncplane_create(u->p, &nopts); + if (!u->gp) { + maybe_destroy_ui(u); + return false; + } + + if (!layout_grid(u, &u->g, u->gp, nopts.cols, nopts.rows)) { + maybe_destroy_ui(u); + return false; + } + + nopts.x = nopts.cols + (even ? INFO_X_OFFSET + 1 : INFO_X_OFFSET); + nopts.y = INFO_Y_OFFSET; + nopts.cols = INFO_WIDTH; + nopts.rows = u->term_rows - rheight; + + u->ip = ncplane_create(u->p, &nopts); + if (!u->ip) { + maybe_destroy_ui(u); + return false; + } + + if (!layout_info(u, &u->n, u->ip, nopts.rows)) { + maybe_destroy_ui(u); + return false; + } + + return true; +} + +void draw_monitors(struct ui *u, struct monitors *m) +{ + if (!u->layed_out) return; + + s32 width = 0; + char *monitor; + al_array_foreach(m->monitors, i, monitor) { + width += strlen(monitor) + 1; + } + + ncplane_set_styles(u->mp, NCSTYLE_BOLD); + s32 w = ncplane_dim_x(u->mp); + s32 x = ((w - 1) - width) / 2; + ncplane_cursor_move_yx(u->mp, 0, x); + al_array_foreach(m->monitors, i, monitor) { + if (m->flash <= 0.f && u->selected_menu == MENU_MONITORS && i == (u32)m->selected_monitor) { + ncplane_set_bg_palindex(u->mp, 8); + ncplane_set_fg_palindex(u->mp, 0); + } else if (i == (u32)m->set_monitor) { + ncplane_set_bg_palindex(u->mp, 7); + ncplane_set_fg_palindex(u->mp, 0); + m->flash -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); + } else { + ncplane_set_fg_default(u->mp); + ncplane_set_bg_default(u->mp); + } + ncplane_cursor_move_rel(u->mp, 0, 1); + ncplane_putstr(u->mp, monitor); + } +} + +void draw_info(struct ui *u, struct info *n, bool update_text) +{ + if (!u->layed_out || !n->e) return; + + ncplane_erase(u->ip); + + struct ncvisual_options vopts = { 0 }; + vopts.scaling = NCSCALE_STRETCH; + vopts.blitter = PIXEL_BLIT ? NCBLIT_PIXEL : NCBLIT_2x1; + struct image *img = get_image_from_pool(&u->pool, &n->e->preview_path); + if (img && !n->blitted && !img->errored) { + ncplane_erase(n->ip); + img->errored = img->gif && ncvisual_decode_loop(img->v) < 0; + if (!img->errored) { + vopts.n = n->ip; + ncvisual_blit(u->nc, img->v, &vopts); + n->blitted = !img->gif; + u->delay = ncvisual_get_duration(img->v); + if (u->delay == 10000) { + // Default 100fps gifs to 10fps, seems like some weird wallpaper engine behavior. + u->delay = 100000; + } + } + } + + ncplane_set_fg_default(u->ip); + draw_scrolling_text(u->ip, &n->text, update_text); + + u32 selected_index = n->selected_prop - (n->selected_prop % n->max_props); + struct info_prop *ip = NULL; + u32 i, index; + for (i = 0; i < n->max_props; i++) { + index = i + selected_index; + if (index >= n->props.size) { + break; + } + ip = &al_array_at(n->props, index); + if (u->selected_menu == MENU_INFO && index == (u32)n->selected_prop) { + ncplane_set_fg_palindex(u->ip, 8); + } else { + ncplane_set_fg_default(u->ip); + } + draw_scrolling_text(u->ip, &ip->text, update_text); + switch (ip->p->type) { + case PROP_TYPE_BOOL: + ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, + *((bool *)ip->p->value) ? "[x]" : "[ ]"); + break; + case PROP_TYPE_SLIDER: { + char c[16]; + al_snprintf(c, sizeof(c), "%.2f", ((struct slider_value *)ip->p->value)->value); + ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, c); + break; + } + } + } + if (ip && i == n->max_props && i != n->props.size) { + ncplane_putstr_yx(u->ip, ip->text.y + 1, (INFO_IMG_WIDTH - 1) / 2, "V"); + } +} + +static void set_tile_highlight(struct tile *t, s32 bindex, s32 findex) +{ + ncplane_set_bg_alpha(t->p, NCALPHA_TRANSPARENT); + struct nccell ncl; + nccell_init(&ncl); + nccell_load(t->p, &ncl, " "); + nccell_set_bg_palindex(&ncl, bindex); + ncplane_polyfill_yx(t->p, 1, 0, &ncl); + ncplane_set_fg_palindex(t->p, bindex); + for (s32 i = 0; i < GRID_TILE_WIDTH; i++) { + ncplane_putwc_yx(t->p, 0, i, L'▃'); + } + ncplane_set_fg_palindex(t->p, findex); + ncplane_set_bg_palindex(t->p, bindex); + ncplane_set_bg_alpha(t->p, NCALPHA_OPAQUE); + ncplane_set_styles(t->p, NCSTYLE_BOLD); +} + +void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text) +{ + if (!u->layed_out) return; + + struct ncvisual_options vopts = { 0 }; + vopts.scaling = NCSCALE_STRETCH; + vopts.blitter = PIXEL_BLIT ? NCBLIT_PIXEL : NCBLIT_2x1; + + s32 index = (g->selected_index - (g->selected_index % g->tiles_per_page)); + struct tile *t; + al_array_foreach_ptr(g->tiles, i, t) { + if (t->e && t->e->has_preview) { + struct image *img = get_image_from_pool(&u->pool, &t->e->preview_path); + if (img && !t->blitted && !img->errored) { + vopts.n = t->ip; + ncvisual_blit(u->nc, img->v, &vopts); + t->blitted = true; + } + } + ncplane_erase(t->p); + if (index == g->selected_index && g->flash <= 0.f && u->selected_menu == MENU_GRID) { + set_tile_highlight(t, 8, 0); + } else if (index == g->set_index && g->set_index >= 0) { + set_tile_highlight(t, 7, 0); + g->flash -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); + } else { + ncplane_set_fg_default(t->p); + ncplane_set_bg_default(t->p); + ncplane_set_bg_alpha(t->p, NCALPHA_TRANSPARENT); + ncplane_set_styles(t->p, NCSTYLE_NONE); + } + if (t->text.s && t->text.s->len) { + draw_scrolling_text(t->p, &t->text, update_text); + } + index++; + } +} + +static bool handle_input_monitors(struct ui *u, struct monitors *m, struct ncinput *input) +{ + bool should_redraw_monitors = false; + bool should_redraw_grid = false; + bool should_redraw_info = false; + switch (input->id) { + case NCKEY_RIGHT: + case 'l': + if ((u32)(m->selected_monitor + 1) >= m->monitors.size) { + return false; + } + m->selected_monitor++; + should_redraw_monitors = true; + break; + case NCKEY_LEFT: + case 'h': + if (m->selected_monitor - 1 < 0) { + return false; + } + m->selected_monitor--; + should_redraw_monitors = true; + break; + case NCKEY_RETURN: + select_monitor(u); + m->flash = FLASH_DURATION; + should_redraw_monitors = true; + should_redraw_grid = true; + should_redraw_info = true; + break; + } + if (should_redraw_monitors) { + draw_monitors(u, m); + } + if (should_redraw_grid) { + set_grid_tiles(u, &u->g); + draw_grid_tiles(u, &u->g, false); + } + if (should_redraw_info) { + set_info_entry(u, &u->n); + draw_info(u, &u->n, false); + } + return should_redraw_monitors || should_redraw_grid || should_redraw_info; +} + +static bool handle_input_info(struct ui *u, struct info *n, struct ncinput *input) +{ + bool should_redraw_info = false; + switch (input->id) { + case NCKEY_DOWN: + case 'j': + if ((u32)(n->selected_prop + 1) >= n->props.size) { + return false; + } + n->selected_prop++; + should_redraw_info = true; + break; + case NCKEY_UP: + case 'k': + if (n->selected_prop - 1 < 0) { + return false; + } + n->selected_prop--; + should_redraw_info = true; + break; + case NCKEY_RETURN: + case NCKEY_SPACE: { + if (!n->props.size) return false; + struct prop *p = &al_array_at(n->e->props, n->selected_prop); + if (p->type == PROP_TYPE_BOOL) { + *((bool *)p->value) = !*((bool *)p->value); + write_selected_entry_to_config(u); + should_redraw_info = true; + } + break; + } + case NCKEY_LEFT: + case 'h': + case NCKEY_RIGHT: + case 'l': + if (!n->props.size) return false; + struct prop *p = &al_array_at(n->e->props, n->selected_prop); + if (p->type == PROP_TYPE_SLIDER) { + struct slider_value *value = (struct slider_value *)p->value; + if (input->id == NCKEY_RIGHT) { + if (value->value == value->max) { + break; + } + if (value->value + value->step <= value->max) { + value->value += value->step; + } else { + value->value = value->max; + } + } else if (input->id == NCKEY_LEFT) { + if (value->value == value->min) { + break; + } + if (value->value - value->step >= value->min) { + value->value -= value->step; + } else { + value->value = value->min; + } + } + write_selected_entry_to_config(u); + should_redraw_info = true; + } + break; + } + if (should_redraw_info) draw_info(u, n, false); + return should_redraw_info; +} + +static bool grid_next_page(struct ui *u, struct grid *g, s32 offset) +{ + if (u->db.entries.size == 0) return false; + s32 max = (s32)u->db.entries.size - 1; + if (g->selected_index == max) { + return false; + } + g->selected_index += g->tiles_per_page - offset; + if (g->selected_index > max) { + g->selected_index = max; + } + return true; +} + +static bool grid_previous_page(struct grid *g, s32 offset) +{ + if (g->selected_index <= 0) { + return false; + } + g->selected_index -= g->tiles_per_page - offset; + if (g->selected_index < 0) { + g->selected_index = 0; + } + return true; +} + +static bool handle_input_grid(struct ui *u, struct grid *g, struct ncinput *input) +{ + bool should_update_grid = false; + bool should_redraw_grid = false; + bool should_redraw_info = false; + switch (input->id) { + case NCKEY_RIGHT: + case 'l': + if (g->selected_index + 1 >= (s32)u->db.entries.size) { + return false; + } + if ((g->selected_index + 1) % g->tiles_x == 0) { + should_update_grid = grid_next_page(u, &u->g, g->tiles_x - 1); + } else { + g->selected_index++; + } + should_redraw_info = true; + should_redraw_grid = true; + break; + case NCKEY_LEFT: + case 'h': + if (g->selected_index - 1 < 0) { + return false; + } + if (g->selected_index % g->tiles_x == 0) { + should_update_grid = grid_previous_page(&u->g, g->tiles_x - 1); + } else { + g->selected_index--; + } + should_redraw_info = true; + should_redraw_grid = true; + break; + case NCKEY_DOWN: + case 'j': + if (g->selected_index + g->tiles_x > g->max_index) { + return false; + } + g->selected_index += g->tiles_x; + should_redraw_info = true; + should_redraw_grid = true; + break; + case NCKEY_UP: + case 'k': + if (g->selected_index - g->tiles_x < g->min_index) { + return false; + } + g->selected_index -= g->tiles_x; + should_redraw_info = true; + should_redraw_grid = true; + break; + case 'b': + if (grid_previous_page(&u->g, 0)) { + should_update_grid = true; + should_redraw_info = true; + should_redraw_grid = true; + } + break; + case 'f': + if (grid_next_page(u, &u->g, 0)) { + should_update_grid = true; + should_redraw_info = true; + should_redraw_grid = true; + } + break; + case 'r': + g->set_index = -1; + write_selected_entry_to_config(u); + should_redraw_grid = true; + should_redraw_info = true; + break; + case 's': { + s32 max = (s32)u->db.entries.size - 1; + g->selected_index = al_rand() % max; + should_update_grid = true; + should_redraw_info = true; + should_redraw_grid = true; + break; + } + case NCKEY_RETURN: + case NCKEY_SPACE: { + if (g->selected_index >= 0) { + g->set_index = g->selected_index; + g->flash = FLASH_DURATION; + } + write_selected_entry_to_config(u); + should_redraw_grid = true; + should_redraw_info = true; + break; + } + } + if (should_update_grid) { + set_grid_tiles(u, g); + } + if (should_redraw_grid) { + draw_grid_tiles(u, g, false); + } + if (should_redraw_info) { + set_info_entry(u, &u->n); + draw_info(u, &u->n, false); + } + return should_redraw_grid || should_redraw_info; +} + +static void input_poll_callback(void *userdata, s32 revents) +{ + struct ui *u = (struct ui *)userdata; + (void)revents; + struct ncinput input; + bool should_render = false; + u32 ret; + while (1) { + ret = notcurses_get_nblock(u->nc, &input); + if (ret == (u32)-1 || ret == 0) { + break; + } + if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) { + if (input.id == NCKEY_TAB) { + switch (u->selected_menu) { + case MENU_GRID: + if (u->n.props.size > 0) { + u->selected_menu = MENU_INFO; + } else { + u->selected_menu = MENU_MONITORS; + } + break; + case MENU_INFO: + if (u->m.selected_monitor >= 0) { + u->selected_menu = MENU_MONITORS; + } else { + u->selected_menu = MENU_GRID; + } + break; + case MENU_MONITORS: + u->selected_menu = MENU_GRID; + break; + } + draw_info(u, &u->n, false); + draw_monitors(u, &u->m); + draw_grid_tiles(u, &u->g, false); + should_render = true; + } else if (input.id == 'q') { + aki_event_loop_break(&u->loop); + } + switch (u->selected_menu) { + case MENU_GRID: + should_render |= handle_input_grid(u, &u->g, &input); + break; + case MENU_INFO: + should_render |= handle_input_info(u, &u->n, &input); + break; + case MENU_MONITORS: + should_render |= handle_input_monitors(u, &u->m, &input); + break; + } + } + } + if (should_render) notcurses_render(u->nc); +} + +static void timer_callback(void *userdata, struct aki_timer *timer) +{ + struct ui *u = (struct ui *)userdata; + (void)timer; + + if (!u->layed_out) { + notcurses_render(u->nc); + return; + } + + if (u->text_update <= 0.f) { + u->text_update = TEXT_UPDATE_RATE; + draw_grid_tiles(u, &u->g, true); + draw_info(u, &u->n, true); + } else { + u->text_update -= AL_MAX(TIMER_UPDATE_BASE * (u->delay / 1000000.f), 0.001f); + draw_grid_tiles(u, &u->g, false); + draw_info(u, &u->n, false); + } + + draw_monitors(u, &u->m); + + notcurses_render(u->nc); + + aki_timer_set_repeat(&u->timer, AKI_TS_FROM_USEC(u->delay)); + aki_timer_again(&u->timer); +} + +static struct ui u = { 0 }; + +s32 main(void) +{ + if (!aki_common_init()) return EXIT_FAILURE; + + s32 ret = EXIT_FAILURE; + + if (!open_config(&u.conf, false)) { + al_log_error("taro", "failed to open config, run `tarod` at least once to generate needed config"); + goto out; + } + + if (!db_load_entries(&u.db, &u.conf.wallpaper_dir)) { + goto out; + } + + if (!init_ui(&u)) { + goto out; + } + + notcurses_stddim_yx(u.nc, &u.term_rows, &u.term_cols); + + ncplane_set_resizecb(notcurses_stdplane(u.nc), resize_cb); + ncplane_set_userptr(notcurses_stdplane(u.nc), &u); + + if (!layout_ui(&u)) { + notcurses_stop(u.nc); + al_log_error("taro", "failed to layout ui, your terminal window is probably too small"); + goto out; + } + u.layed_out = true; + draw_monitors(&u, &u.m); + + aki_event_loop_init(&u.loop); + + aki_poll_init(&u.input_poll, input_poll_callback, &u); + aki_poll_set(&u.input_poll, notcurses_inputready_fd(u.nc), AKI_POLL_READ); + aki_poll_start(&u.input_poll, &u.loop); + + aki_timer_init(&u.timer, &u.loop, timer_callback, &u); + u.delay = 1000000; // 1fps. + timer_callback(&u, NULL); + + aki_event_loop_run(&u.loop); + + notcurses_stop(u.nc); + + ret = EXIT_SUCCESS; + +out: + close_config(&u.conf); + + aki_common_close(); + + return ret; +} -- cgit v1.2.3-101-g0448