summaryrefslogtreecommitdiff
path: root/taro
diff options
context:
space:
mode:
authorAndrew Opalach <andrew@akon.city> 2024-10-03 19:08:44 -0400
committerAndrew Opalach <andrew@akon.city> 2024-10-04 14:19:40 -0400
commit0bf9b26b87075448d49482fe4411739af765d872 (patch)
treee3f0e3312a92e5250fa72b410adb370734deca04 /taro
parentacd44bb898996ff4e36b39b10e870e40bcaf2634 (diff)
downloadmauri-0bf9b26b87075448d49482fe4411739af765d872.tar.gz
mauri-0bf9b26b87075448d49482fe4411739af765d872.tar.bz2
mauri-0bf9b26b87075448d49482fe4411739af765d872.zip
Update codebase
Bring up-to-date with my other projects. Wallpaper engine behavior is mostly unchanged. Signed-off-by: Andrew Opalach <andrew@akon.city>
Diffstat (limited to 'taro')
-rw-r--r--taro/attic/sway_background_covered.diff314
-rw-r--r--taro/attic/sway_test_config9
-rw-r--r--taro/meson.build47
-rwxr-xr-xtaro/scripts/kill_wallpaper.sh11
-rwxr-xr-xtaro/scripts/make_prefix.sh25
-rwxr-xr-xtaro/scripts/run_wallpaper.sh20
-rw-r--r--taro/src/daemon.c723
-rw-r--r--taro/src/db.c212
-rw-r--r--taro/src/db.h41
-rw-r--r--taro/src/dirs.h10
-rw-r--r--taro/src/file_ext.c56
-rw-r--r--taro/src/file_ext.h7
-rw-r--r--taro/src/ui.c1183
-rw-r--r--taro/subprojects/libakiyo.wrap4
-rw-r--r--taro/subprojects/libalabaster.wrap4
-rw-r--r--taro/subprojects/notcurses.wrap12
-rw-r--r--taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff166
-rw-r--r--taro/subprojects/stela.wrap4
-rw-r--r--taro/tests/notcurses_ffmpeg.c35
19 files changed, 2883 insertions, 0 deletions
diff --git a/taro/attic/sway_background_covered.diff b/taro/attic/sway_background_covered.diff
new file mode 100644
index 0000000..fe375e2
--- /dev/null
+++ b/taro/attic/sway_background_covered.diff
@@ -0,0 +1,314 @@
+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..3727116d 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,78 @@ 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;
++ const char *app_id;
++ 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.
++ if ((app_id = view_get_app_id(con->view)) && 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
new file mode 100644
index 0000000..1223428
--- /dev/null
+++ b/taro/attic/sway_test_config
@@ -0,0 +1,9 @@
+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/meson.build b/taro/meson.build
new file mode 100644
index 0000000..87edfe7
--- /dev/null
+++ b/taro/meson.build
@@ -0,0 +1,47 @@
+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()
+ notcurses_opts = cmake.subproject_options()
+ notcurses_opts.add_cmake_defines({ 'CMAKE_BUILD_TYPE': is_debug ? 'Debug' : 'Release' })
+ notcurses_opts.add_cmake_defines({ 'USE_CXX': false })
+ notcurses_opts.add_cmake_defines({ 'USE_PANDOC': false })
+ notcurses_opts.add_cmake_defines({ 'BUILD_EXECUTABLES': false })
+ notcurses_opts.add_cmake_defines({ 'USE_POC': false })
+ notcurses_proj = cmake.subproject('notcurses', options: notcurses_opts)
+ notcurses = notcurses_proj.dependency('notcurses')
+ 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']
+
+tarod_deps = [akiyo, stela]
+tarod_src = ['src/daemon.c', 'src/file_ext.c']
+
+executable('taro', taro_src, dependencies: taro_deps, install: true)
+executable('tarod', tarod_src, dependencies: tarod_deps, install: true)
+
+executable('notcurses_test', ['tests/notcurses_ffmpeg.c'], dependencies: [akiyo, notcurses, notcurses_core])
diff --git a/taro/scripts/kill_wallpaper.sh b/taro/scripts/kill_wallpaper.sh
new file mode 100755
index 0000000..04f01f8
--- /dev/null
+++ b/taro/scripts/kill_wallpaper.sh
@@ -0,0 +1,11 @@
+#! /usr/bin/env sh
+
+USE_MAURI=$1
+
+if [ "$USE_MAURI" = "true" ]; then
+ pkill mauri
+else
+ export WINEPREFIX=$2
+ wineserver -k
+ wineserver -w
+fi
diff --git a/taro/scripts/make_prefix.sh b/taro/scripts/make_prefix.sh
new file mode 100755
index 0000000..9facae2
--- /dev/null
+++ b/taro/scripts/make_prefix.sh
@@ -0,0 +1,25 @@
+#! /usr/bin/env sh
+
+export WINEPREFIX=$1
+
+CONFIG_DIR=$2
+
+install_dxvk() {
+ mkdir -p $CONFIG_DIR/tmp && cd $CONFIG_DIR/tmp
+ VER=$(curl https://raw.githubusercontent.com/doitsujin/dxvk/master/RELEASE)
+ curl -OL https://github.com/doitsujin/dxvk/releases/download/v$VER/dxvk-$VER.tar.gz
+ tar xf dxvk-$VER.tar.gz
+ cd dxvk-$VER
+ curl -OL https://gist.githubusercontent.com/doitsujin/1652e0e3382f0e0ff611e70142684d01/raw/4b80903eb4a8c1033750175de4ebe64685725a3e/setup_dxvk.sh
+ chmod +x ./setup_dxvk.sh
+ ./setup_dxvk.sh . install
+ rm -r $CONFIG_DIR/tmp
+}
+
+if [ ! -d $WINEPREFIX ]; then
+ wineboot --init
+ winecfg -v win10
+ wineserver -w
+ install_dxvk
+ wineserver -w
+fi
diff --git a/taro/scripts/run_wallpaper.sh b/taro/scripts/run_wallpaper.sh
new file mode 100755
index 0000000..5ce8bc6
--- /dev/null
+++ b/taro/scripts/run_wallpaper.sh
@@ -0,0 +1,20 @@
+#! /usr/bin/env sh
+
+USE_MAURI=$1
+
+export mesa_glthread=true
+
+if [ "$USE_MAURI" = "true" ]; then
+ if [ "$5" = "true" ]; then
+ exec mauri -p $6/scene.pkg -w $7 -h $8 -m $3
+ else
+ exec mauri -d $6 -w $7 -h $8 -m $3
+ fi
+else
+ export WINEPREFIX=$2
+ wineserver -k
+ wineserver -w
+ export WINEMONITOR=$3
+ exec $4 ../../wallpaper_engine/wallpaper64.exe -control openWallpaper -file $6/project.json \
+ -playInWindow "wallpaper" -width $7 -height $8 > $9 2>&1
+fi
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;
+}
diff --git a/taro/subprojects/libakiyo.wrap b/taro/subprojects/libakiyo.wrap
new file mode 100644
index 0000000..2cd2caa
--- /dev/null
+++ b/taro/subprojects/libakiyo.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://git.akon.city/libakiyo
+revision = master
+depth = 1
diff --git a/taro/subprojects/libalabaster.wrap b/taro/subprojects/libalabaster.wrap
new file mode 100644
index 0000000..c0358cf
--- /dev/null
+++ b/taro/subprojects/libalabaster.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://git.akon.city/libalabaster
+revision = master
+depth = 1
diff --git a/taro/subprojects/notcurses.wrap b/taro/subprojects/notcurses.wrap
new file mode 100644
index 0000000..64f6731
--- /dev/null
+++ b/taro/subprojects/notcurses.wrap
@@ -0,0 +1,12 @@
+[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
new file mode 100644
index 0000000..71680f7
--- /dev/null
+++ b/taro/subprojects/packagefiles/notcurses/duration_and_ffmpeg_fix.diff
@@ -0,0 +1,166 @@
+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
new file mode 100644
index 0000000..7807a8c
--- /dev/null
+++ b/taro/subprojects/stela.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+url = https://git.akon.city/stela
+revision = master
+depth = 1
diff --git a/taro/tests/notcurses_ffmpeg.c b/taro/tests/notcurses_ffmpeg.c
new file mode 100644
index 0000000..1a9f44d
--- /dev/null
+++ b/taro/tests/notcurses_ffmpeg.c
@@ -0,0 +1,35 @@
+#include <notcurses/notcurses.h>
+#include <notcurses/nckeys.h>
+#include <al/types.h>
+#include <aki/common.h>
+
+s32 main(void)
+{
+ if (!aki_common_init()) return EXIT_FAILURE;
+
+ struct notcurses *nc = notcurses_init(NULL, stdin);
+
+ struct ncplane_options nopts = { 0 };
+ nopts.cols = 25;
+ nopts.rows = 15;
+ nopts.x = 0;
+ nopts.y = 0;
+ struct ncplane *p = ncplane_create(notcurses_stdplane(nc), &nopts);
+
+ //struct ncvisual *v = ncvisual_from_file("/home/andrew/c/mauri/workshop_wallpapers_11-8-21/2025095336/preview.gif");
+ struct ncvisual *v = ncvisual_from_file("/home/andrew/dl/mp4/This is FRIED [svlczpgt2m3d1].mp4");
+ if (!v) goto out;
+ struct ncvisual_options vopts = { 0 };
+ vopts.scaling = NCSCALE_SCALE;
+ vopts.blitter = NCBLIT_PIXEL;
+ vopts.n = p;
+
+ ncvisual_stream(nc, v, 1.0f, NULL, &vopts, NULL);
+
+out:
+ notcurses_stop(nc);
+
+ aki_common_close();
+
+ return EXIT_SUCCESS;
+}