diff options
Diffstat (limited to 'taro/src')
| -rw-r--r-- | taro/src/daemon.c | 723 | ||||
| -rw-r--r-- | taro/src/db.c | 212 | ||||
| -rw-r--r-- | taro/src/db.h | 41 | ||||
| -rw-r--r-- | taro/src/dirs.h | 10 | ||||
| -rw-r--r-- | taro/src/file_ext.c | 56 | ||||
| -rw-r--r-- | taro/src/file_ext.h | 7 | ||||
| -rw-r--r-- | taro/src/ui.c | 1183 |
7 files changed, 2232 insertions, 0 deletions
diff --git a/taro/src/daemon.c b/taro/src/daemon.c new file mode 100644 index 0000000..ec68df0 --- /dev/null +++ b/taro/src/daemon.c @@ -0,0 +1,723 @@ +#include <sys/wait.h> +#include <al/types.h> +#include <al/array.h> +#include <al/log.h> +#include <al/str.h> +#include <aki/common.h> +#include <aki/event_loop.h> +#include <aki/fs_event.h> +#include <aki/socket.h> +#include <stl/window.h> +#include <jansson.h> + +#include "dirs.h" +#include "file_ext.h" + +#define USE_QUEUE 0 +#define USE_MAURI "true" + +struct monitor { + struct stl_monitor *os; + struct aki_fs_event fs_event; + bool self_event_skip; + str config_dir; + str prefix_dir; + str wine_log_path; + str selection_path; + //str pid_path; + pid_t pid; + bool paused; + struct daemon *dmon; +}; + +#define IPC_HEADER_LENGTH 14 +#define IPC_MESSAGE_BUF_SIZE 64 + +struct daemon { + struct stl_window *context; + struct aki_poll context_poll; + array(struct monitor *) monitors; + array(struct monitor *) queued_monitors; + bool queue_running; + struct aki_event_loop loop; + struct aki_fs_event fs_event; + 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; + str wp_config_dir; +}; + +// If s is malformed, this will be false. +#define JSON_STR_TO_BOOL(s) (al_str_eq(s, al_str_c("true")) ? true : false + +static json_t *parse_and_set_globals(json_t *root, str *gsub) +{ + const char *key; + json_t *value; + json_t *general = NULL; + json_t *user = NULL; + + json_object_foreach(root, key, value) { + general = json_object_get(value, "general"); + if (general) { + user = value; + break; + } + } + + if (!general) { + json_decref(root); + return NULL; + } + + // When wallpaper engine runs, it corrects this value. We use (abuse) that to + // run the queue and set wallpapers in order. + json_object_set_new(user, "version", json_integer(0)); + + json_t *gprops = json_object(); + + s32 count = 0; + str tok = al_str_zero(); + while (al_str_get_line(gsub, ',', &tok)) { + switch (count++) { + case 0: + json_object_set_new(gprops, "cameraparallax", json_boolean(JSON_STR_TO_BOOL(&tok)))); + break; + case 1: { + s64 n = al_str_to_long(&tok, 10); + if (n < 0 || n > 200) n = 100; + json_object_set_new(gprops, "rate", json_integer(n)); + break; + } + default: + al_log_warn("tarod", "excess value \"%.*s\" in selection file at index %i", AL_STR_PRINTF(&tok), count); + break; + } + } + + json_object_set_new(general, "defaultproperties", gprops); + + return root; +} + +static json_t *parse_and_set_locals(json_t *root, str *lsub) +{ + json_t *general = json_object_get(root, "general"); + if (!general) { + json_decref(root); + return NULL; + } + + json_t *properties = json_object_get(general, "properties"); + if (!properties) { + // Probably don't want to completely fail here. + json_decref(root); + return NULL; + } + + void *iter = json_object_iter(properties); + json_t *iter_obj = json_object_iter_value(iter); + + str tok = al_str_zero(); + while (al_str_get_line(lsub, ',', &tok)) { + json_t *value = json_object_get(iter_obj, "value"); + switch (json_typeof(value)) { + case JSON_TRUE: + case JSON_FALSE: + json_object_set_new(iter_obj, "value", json_boolean(JSON_STR_TO_BOOL(&tok)))); + break; + default: + break; + } + iter = json_object_iter_next(properties, iter); + if (!iter) { + break; + } + iter_obj = json_object_iter_value(iter); + } + + return root; +} + +static s32 parse_and_set_props(struct monitor *monitor, str *selection_str, str *selection_id, bool *is_package) +{ + s32 index = al_str_find(selection_str, '|'); + s32 rindex = al_str_rfind(selection_str, '|'); + if (index < 2 || rindex == -1 || index == rindex) { + return -1; + } + + str *id = al_str_substr(selection_str, 0, index - 2); + str *type = al_str_substr(selection_str, index - 2, index); + if (id->len == 0) return 1; + + al_str_cat(selection_id, 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; + } + + json_t *root = aki_file_open_as_json(&monitor->dmon->wp_config_dir); + if (!root) return -1; + + str *gsub = al_str_substr(selection_str, index + 1, rindex); + if (gsub->len > 0) { + root = parse_and_set_globals(root, gsub); + if (!root || !aki_file_dump_and_decref_json(&monitor->dmon->wp_config_dir, root)) { + al_log_warn("tarod", "failed to parse and/or set global properties for %s", monitor->os->name); + } + } + + str project_path; + al_str_clone(&project_path, selection_id); + al_str_cat(&project_path, al_str_c("/project.json")); + + root = aki_file_open_as_json(&project_path); + if (!root) { + al_str_free(&project_path); + return -1; + } + + // If local props are empty, don't try to set them. + if ((u32)rindex < selection_str->len - 1) { + str *lsub = al_str_substr(selection_str, rindex + 1, selection_str->len - 1); + if (lsub->len > 0) { + root = parse_and_set_locals(root, lsub); + if (!root || !aki_file_dump_and_decref_json(&project_path, root)) { + al_log_warn("tarod", "failed to parse and/or set local properties for %s", monitor->os->name); + } + } + } + + al_str_free(&project_path); + + return 0; +} + +static bool send_signal_to_pid(pid_t pid, s32 signal) +{ + if (kill(pid, signal) != 0) { + al_log_error("tarod", "kill(%i) failed (%s)", pid, aki_strerror(errno)); + return false; + } + return true; +} + +// We assume that if exec fails, the entire operation of this daemon will no longer work properly. +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]); + pid_t ppid = getppid(); + // This should return the pid of init if the parent process has already exited. + if (ppid != 1) { + send_signal_to_pid(ppid, SIGINT); + } + } + } + return pid; +} + +/* +static void wait_for_exit(pid_t pid) +{ + s32 status = 0; + do { + waitpid(pid, &status, 0); + } while (!WIFEXITED(status)); +} +*/ + +#if USE_QUEUE +static void lock_fs_event_callback(uv_fs_event_t *handle, const char *filename, s32 events, s32 status); +static void close_callback(uv_handle_t *handle); + +static inline void manually_push_queue(struct daemon *dmon) +{ + if (uv_is_active((uv_handle_t *)&dmon->queue_evnt)) { + uv_fs_event_stop(&dmon->queue_evnt); + } + uv_close((uv_handle_t *)&dmon->queue_evnt, close_callback); +} +#endif + +static void maybe_kill_wallpaper(struct monitor *monitor) +{ + if (monitor->pid != -1) { + send_signal_to_pid(monitor->pid, SIGTERM); + monitor->pid = -1; + } + /* + char *c_str = al_str_to_c_str(&monitor->prefix_dir); + pid_t pid = exec_or_exit((char *[]){ "./kill_wallpaper.sh", USE_MAURI, c_str, NULL }); + al_free(c_str); + wait_for_exit(pid); + */ +} + +static bool run_wallpaper(struct monitor *monitor) +{ +#if USE_QUEUE + struct daemon *dmon = monitor->dmon; + dmon->queue_running = true; + uv_fs_event_init(uv_default_loop(), &dmon->queue_evnt); +#endif + + maybe_kill_wallpaper(monitor); + + monitor->paused = false; + + str selection_str; + monitor->self_event_skip = true; + if (!aki_file_open_and_read_wholly(&monitor->selection_path, &selection_str, true)) { +#if USE_QUEUE + manually_push_queue(dmon); +#endif + monitor->self_event_skip = false; + return false; + } + + if (!selection_str.len) { + al_log_error("tarod", "config for %s is empty", monitor->os->name); + al_str_free(&selection_str); +#if USE_QUEUE + manually_push_queue(dmon); +#endif + return false; + } + + str selection; + al_str_from(&selection, WALLPAPER_DIR); + al_str_cat(&selection, al_str_c("/")); + + bool is_package; + s32 ret = parse_and_set_props(monitor, &selection_str, &selection, &is_package); + al_str_free(&selection_str); + if (ret != 0) { + if (ret == -1) { + al_log_error("tarod", "failed to set properties for wallpaper on %s", monitor->os->name); + al_str_free(&selection); + } +#if USE_QUEUE + manually_push_queue(dmon); +#endif + return false; + } + +#if USE_QUEUE + c_str = al_str_to_c_str(&dmon->wp_config_dir); + uv_fs_event_start(&dmon->queue_evnt, lock_fs_event_callback, c_str, 0); + al_free(c_str); +#endif + + char width[16], height[16]; + al_sprintf(width, "%i", monitor->os->width / monitor->os->scale); + al_sprintf(height, "%i", monitor->os->height / monitor->os->scale); + + al_log_info("tarod", "setting wallpaper %.*s on %s", AL_STR_PRINTF(&selection), monitor->os->name); + + char *c_str0 = al_str_to_c_str(&monitor->prefix_dir); + char *c_str1 = al_str_to_c_str(&selection); + char *c_str2 = al_str_to_c_str(&monitor->wine_log_path); + char *args[] = { + "./run_wallpaper.sh", USE_MAURI, c_str0, monitor->os->name, WINE64_PATH, + AL_BOOLSTR(is_package), c_str1, width, height, c_str2, NULL + }; + monitor->pid = exec_or_exit(args); + al_free(c_str0); + al_free(c_str1); + al_free(c_str2); + + /* + char pid_str[16]; + al_sprintf(pid_str, "%i", monitor->pid); + // The daemon is the only writer or reader of this file. + if (!aki_file_open_and_replace(&monitor->pid_path, al_str_c(pid_str), false)) { + // If we can't track the pid of the child process don't let it keep running. + kill(monitor->pid, SIGKILL); + monitor->pid = -1; +#if USE_QUEUE + manually_push_queue(dmon); +#endif + return false; + } + */ + + al_str_free(&selection); + + return true; +} + +#if USE_QUEUE +void close_callback(uv_handle_t *handle) +{ + struct daemon *dmon = (struct dmon *)handle->data; + bool ran_wallpaper = false; + while (dmon->queued_monitors.size > 0) { + struct monitor *monitor; + al_array_pop_at(dmon->queued_monitors, 0, monitor); + if (run_wallpaper(monitor)) { + ran_wallpaper = true; + break; + } + } + if (!ran_wallpaper) { + dmon->queue_running = false; + } +} + +void lock_fs_event_callback(uv_fs_event_t *handle, const char *filename, s32 events, s32 status) +{ + (void)filename; + (void)status; + if (events & UV_CHANGE) { + // uv_fs_event_stop doesn't work like I would expect, not sure if it's a + // bug or not but using uv_close to run the queue works here. + uv_fs_event_stop(handle); + uv_close((uv_handle_t *)handle, close_callback); + } +} +#endif + +static void maybe_create_config_directory(struct monitor *monitor) +{ + bool config_dir_exists = aki_dir_exists(&monitor->config_dir); + if (!config_dir_exists) { + if (!aki_dir_create(&monitor->config_dir)) { + return; + } + } + + if (!aki_dir_exists(&monitor->prefix_dir)) { + char *c_str = al_str_to_c_str(&monitor->prefix_dir); + //pid_t pid = exec_or_exit((char *[]){ "make_prefix.sh", c_str, CONFIG_DIR, NULL }); + al_free(c_str); + //wait_for_exit(pid); + } + + if (config_dir_exists) { + return; + } + + aki_file_create(&monitor->selection_path); + aki_file_open_and_replace(&monitor->selection_path, al_str_c("||"), false); + + //aki_file_create(&monitor->pid_path); +} + +static void enqueue_monitor(struct daemon *dmon, struct monitor *monitor) +{ + if (!dmon->queue_running) { + run_wallpaper(monitor); + } else { + struct monitor *queued_monitor; + al_array_foreach(dmon->queued_monitors, i, queued_monitor) { + if (queued_monitor == monitor) return; + } + al_array_push(dmon->queued_monitors, monitor); + } +} + +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; + enqueue_monitor(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) { + enqueue_monitor(dmon, monitor); + return; + } + } + + monitor = al_alloc_object(struct monitor); + al_array_push(dmon->monitors, monitor); + + monitor->os = os_monitor; + monitor->dmon = dmon; + + al_str_from(&monitor->config_dir, CONFIG_DIR); + if (al_str_atr(&monitor->config_dir, -1) != '/') { + al_str_cat(&monitor->config_dir, al_str_c("/")); + } + al_str_cat(&monitor->config_dir, al_str_cr(monitor->os->name)); + + al_str_clone(&monitor->prefix_dir, &monitor->config_dir); + al_str_cat(&monitor->prefix_dir, al_str_c("/prefix")); + + al_str_clone(&monitor->wine_log_path, &monitor->config_dir); + al_str_cat(&monitor->wine_log_path, al_str_c("/wine.log")); + + al_str_clone(&monitor->selection_path, &monitor->config_dir); + al_str_cat(&monitor->selection_path, al_str_c("/selection")); + + //al_str_clone(&monitor->pid_path, &monitor->config_dir); + //al_str_cat(&monitor->pid_path, al_str_c("/pid")); + + monitor->pid = -1; + monitor->paused = false; + + maybe_create_config_directory(monitor); + + enqueue_monitor(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->prefix_dir); + al_str_free(&monitor->wine_log_path); + al_str_free(&monitor->selection_path); + //al_str_free(&monitor->pid_path); + aki_fs_event_stop(&monitor->fs_event); + aki_fs_event_free(&monitor->fs_event); +} + +static void ipc_handle_subscribe_success(struct daemon *dmon, json_t *root) +{ + (void)dmon; + 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) +{ + json_error_t error; + al_log_debug("tarod", "IN: %.*s", size, (char *)buf); + 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(dmon, 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) { + if (monitor->pid == -1) goto done; + if (!monitor->paused && covered) { + if (kill(monitor->pid, SIGSTOP) == 0) { + monitor->paused = true; + } + } else if (monitor->paused && !covered) { + if (kill(monitor->pid, SIGCONT) == 0) { + 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) +{ + struct daemon *dmon = (struct daemon *)userdata; + al_assert(revents & AKI_POLL_READ); + dmon->context->process_events(dmon->context); + dmon->context->prepare_read(dmon->context); +} +*/ + +// 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); + + aki_event_loop_init(&dmon.loop); + + al_array_init(dmon.monitors); + al_array_init(dmon.queued_monitors); + dmon.queue_running = false; + + al_str_from(&dmon.wp_config_dir, WALLPAPER_ENGINE_DIR); + al_str_cat(&dmon.wp_config_dir, al_str_c("/config.json")); + + stl_set_output_discovered_callback(output_discovered_callback, &dmon); + if (!stl_global_init()) return EXIT_FAILURE; + //dmon.context = stl_window_create(); + //aki_poll_init(&dmon.context_poll, context_poll_callback, &dmon); + //aki_poll_set(&dmon.context_poll, dmon.context->get_poll_fd(dmon.context), AKI_POLL_READ); + //dmon.context->prepare_read(dmon.context); + //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_from(&monitors_path, 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_array_free(dmon.queued_monitors); + al_str_free(&dmon.wp_config_dir); + al_free(dmon.ipc.buf); + + dmon.context->free(&dmon.context); + + aki_event_loop_destroy(&dmon.loop); + + aki_common_close(); + + return EXIT_SUCCESS; +} diff --git a/taro/src/db.c b/taro/src/db.c new file mode 100644 index 0000000..9d2ffa2 --- /dev/null +++ b/taro/src/db.c @@ -0,0 +1,212 @@ +#include <al/log.h> + +#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 new file mode 100644 index 0000000..78f4c77 --- /dev/null +++ b/taro/src/db.h @@ -0,0 +1,41 @@ +#pragma once + +#include <al/array.h> +#include <al/wstr.h> + +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/dirs.h b/taro/src/dirs.h new file mode 100644 index 0000000..e508983 --- /dev/null +++ b/taro/src/dirs.h @@ -0,0 +1,10 @@ +#pragma once + +#define WINE64_PATH "/opt/wine-wayland/bin/wine64" + +#define HOME "/home/andrew" +#define CONFIG_DIR HOME"/.config/taro" +//#define WALLPAPER_DIR HOME"/c/mauri/workshop_wallpapers_11-8-21" +#define WALLPAPER_DIR HOME"/c/mauri/workshop_wallpapers" +//#define WALLPAPER_DIR "/mnt/hdd/SteamLibrary/steamapps/workshop/content/431960" +#define WALLPAPER_ENGINE_DIR HOME"/c/wpe-wine/wallpaper_engine" diff --git a/taro/src/file_ext.c b/taro/src/file_ext.c new file mode 100644 index 0000000..48dffce --- /dev/null +++ b/taro/src/file_ext.c @@ -0,0 +1,56 @@ +#include <al/log.h> + +#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 new file mode 100644 index 0000000..ddaceba --- /dev/null +++ b/taro/src/file_ext.h @@ -0,0 +1,7 @@ +#include <aki/file.h> +#include <jansson.h> + +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 new file mode 100644 index 0000000..e8307f5 --- /dev/null +++ b/taro/src/ui.c @@ -0,0 +1,1183 @@ +#include <notcurses/notcurses.h> +#include <notcurses/nckeys.h> +#include <aki/common.h> +#include <aki/event_loop.h> +#include <aki/timer.h> +#include <al/log.h> + +#include "db.h" +#include "dirs.h" +#include "file_ext.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; +}; + +// --------- +// 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_from(&u->m.selection_path, 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; + if (!aki_file_open_and_read_wholly(al_str_c(CONFIG_DIR"/monitors"), &monitors, false)) { + 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_from(&monitor_dir, 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 int 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) +{ + aki_common_init(); + + if (!db_load_entries(&u.db, al_str_c(WALLPAPER_DIR))) { + return EXIT_FAILURE; + } + + if (!init_ui(&u)) return EXIT_FAILURE; + + 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"); + return EXIT_FAILURE; + } + 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); + + aki_common_close(); + + return EXIT_SUCCESS; +} |