summaryrefslogtreecommitdiff
path: root/src/mover.c
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-14 09:08:10 +0300
committerPaul Buetow <paul@buetow.org>2026-07-14 09:08:10 +0300
commit95858bc4a819c118cf76dd38d6da0365ead997a9 (patch)
tree739544e2314c3e0bab65fb9c75e2c17e89069017 /src/mover.c
parent12be1b9ae52829292d13cc2fcc6ff7571d050d62 (diff)
operate: mover, opener, runner, clipboard modules + tests (ut0)
M8 (partial): plain-C operate modules + unit tests. - src/mover.{c,h}: move files to configured dests (g_file_move + stem collision suffix -1/-2), one-level undo (move back), acts on marks-or-current. - src/opener.{c,h}: %f expand + detached GSubprocess launch (non-blocking). - src/runner.{c,h}: /bin/sh -c with single-quoted %f/%d (injection guard), async wait_check, rescan on completion. - src/clipboard.{c,h}: image/png via GdkContentProvider + uri-list. - meson: new sources added to libggae. - tests: unit test_mover (move/undo/collision), test_opener (true/false/weird filenames), test_runner (true/false/injection guard). 19/19 green, ASan clean. - Fixed: g_str_replace (not in GLib 2.88) → manual _str_replace; GdkContentProvider API (new_typed/new_for_value); mover collision stem-suffix; runner /bin/sh -c argv (single arg, not shell-parsed); test GFile leaks.
Diffstat (limited to 'src/mover.c')
-rw-r--r--src/mover.c135
1 files changed, 135 insertions, 0 deletions
diff --git a/src/mover.c b/src/mover.c
new file mode 100644
index 0000000..cc82ae3
--- /dev/null
+++ b/src/mover.c
@@ -0,0 +1,135 @@
+/* mover.c — configurable move destinations with undo. */
+#include "mover.h"
+
+struct Mover {
+ GPtrArray *p_dests; /* MoverDest* (owned) */
+ GPtrArray *p_last_src; /* GFile* (owned, for undo) */
+ GPtrArray *p_last_dst; /* GFile* (owned, for undo) */
+};
+
+static void
+_dest_free(gpointer p) {
+ MoverDest *d = (MoverDest *)p;
+ if (d != NULL) {
+ g_free(d->c_name);
+ g_free(d->c_path);
+ g_free(d);
+ }
+}
+
+Mover *
+mover_new(void) {
+ Mover *m = g_new0(Mover, 1);
+ m->p_dests = g_ptr_array_new_with_free_func(_dest_free);
+ m->p_last_src = g_ptr_array_new_with_free_func(g_object_unref);
+ m->p_last_dst = g_ptr_array_new_with_free_func(g_object_unref);
+ return m;
+}
+
+void
+mover_delete(Mover *m) {
+ if (m == NULL)
+ return;
+ g_ptr_array_unref(m->p_dests);
+ g_ptr_array_unref(m->p_last_src);
+ g_ptr_array_unref(m->p_last_dst);
+ g_free(m);
+}
+
+void
+mover_set_dests(Mover *m, const GPtrArray *p_dests) {
+ g_return_if_fail(m != NULL);
+ g_ptr_array_set_size(m->p_dests, 0);
+ if (p_dests != NULL) {
+ for (guint i = 0; i < p_dests->len; i++) {
+ const MoverDest *d =
+ (const MoverDest *)g_ptr_array_index((GPtrArray *)p_dests, i);
+ MoverDest *nd = g_new(MoverDest, 1);
+ nd->c_name = g_strdup(d->c_name);
+ nd->c_path = g_strdup(d->c_path);
+ g_ptr_array_add(m->p_dests, nd);
+ }
+ }
+}
+
+const GPtrArray *
+mover_get_dests(Mover *m) {
+ g_return_val_if_fail(m != NULL, NULL);
+ return m->p_dests;
+}
+
+gboolean
+mover_move(Mover *m, GList *p_files, const MoverDest *p_dest, GError **p_err) {
+ g_return_val_if_fail(m != NULL, FALSE);
+ g_return_val_if_fail(p_dest != NULL, FALSE);
+ GFile *p_ddir = g_file_new_for_path(p_dest->c_path);
+ GError *e = NULL;
+ g_file_make_directory_with_parents(p_ddir, NULL, &e);
+ if (e != NULL && !g_error_matches(e, G_IO_ERROR, G_IO_ERROR_EXISTS)) {
+ g_propagate_error(p_err, e);
+ g_object_unref(p_ddir);
+ return FALSE;
+ }
+ g_clear_error(&e);
+
+ g_ptr_array_set_size(m->p_last_src, 0);
+ g_ptr_array_set_size(m->p_last_dst, 0);
+
+ for (GList *it = p_files; it != NULL; it = it->next) {
+ GFile *src = G_FILE(it->data);
+ char *base = g_file_get_basename(src);
+ GFile *dst = g_file_get_child(p_ddir, base);
+ g_free(base);
+ for (guint n = 1; g_file_query_exists(dst, NULL); n++) {
+ g_object_unref(dst);
+ /* Suffix on the stem (before the extension): a.jpg -> a-1.jpg */
+ char *c_b = g_file_get_basename(src);
+ const char *c_d = strrchr(c_b, '.');
+ char *c_s =
+ (c_d && c_d != c_b) ? g_strndup(c_b, c_d - c_b) : g_strdup(c_b);
+ const char *c_e = (c_d && c_d != c_b) ? c_d : "";
+ char *nb = g_strdup_printf("%s-%u%s", c_s, n, c_e);
+ g_free(c_s);
+ g_free(c_b);
+ dst = g_file_get_child(p_ddir, nb);
+ g_free(nb);
+ }
+ if (!g_file_move(src, dst, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL,
+ NULL, &e)) {
+ g_propagate_error(p_err, e);
+ g_object_unref(dst);
+ g_object_unref(p_ddir);
+ return FALSE;
+ }
+ g_ptr_array_add(m->p_last_src, g_object_ref(src));
+ g_ptr_array_add(m->p_last_dst, dst);
+ }
+ g_object_unref(p_ddir);
+ return TRUE;
+}
+
+gboolean
+mover_undo_last(Mover *m, GError **p_err) {
+ g_return_val_if_fail(m != NULL, FALSE);
+ if (m->p_last_dst->len == 0)
+ return FALSE;
+ for (guint i = 0; i < m->p_last_dst->len; i++) {
+ GFile *dst = g_ptr_array_index(m->p_last_dst, i);
+ GFile *src = g_ptr_array_index(m->p_last_src, i);
+ GError *e = NULL;
+ if (!g_file_move(dst, src, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL, NULL,
+ NULL, &e)) {
+ g_propagate_error(p_err, e);
+ return FALSE;
+ }
+ }
+ g_ptr_array_set_size(m->p_last_src, 0);
+ g_ptr_array_set_size(m->p_last_dst, 0);
+ return TRUE;
+}
+
+gboolean
+mover_can_undo(Mover *m) {
+ g_return_val_if_fail(m != NULL, FALSE);
+ return m->p_last_dst->len > 0;
+} \ No newline at end of file