summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-14 14:55:24 +0300
committerPaul Buetow <paul@buetow.org>2026-07-14 14:55:24 +0300
commit0b9dc4eec752d6d48aab696d06b33f4782e9db4f (patch)
tree28271308c6280a6d0bb87ed31ebe8a1a01837b67
parentde1d108d97bc55a6f047c47bb56667068aac71ff (diff)
polish: AppStream, man page, RPM spec, full lifecycle test (yt0)
M10 (task yt0): polish & packaging. - data/org.buetow.ggaze.metainfo.xml: AppStream metainfo. - data/ggaze.1: man page with keybindings reference. - packaging/ggaze.spec: Fedora RPM spec. - data/meson.build: installs desktop+metainfo+gschema+man; gnome.post_install. - tests/test_full_lifecycle.c: scripted elevator-pitch session (open→walk→ info→trash→undo→grid toggle→quit). 21/21 green, ASan clean. - Coverage gate flip, quality audit, window-geometry persistence, and full GSettings wiring deferred to a follow-up (not blocking v0.1 release).
-rw-r--r--data/ggaze.171
-rw-r--r--data/meson.build11
-rw-r--r--data/org.buetow.ggaze.metainfo.xml42
-rw-r--r--packaging/ggaze.spec49
-rw-r--r--tests/meson.build9
-rw-r--r--tests/test_full_lifecycle.c176
6 files changed, 355 insertions, 3 deletions
diff --git a/data/ggaze.1 b/data/ggaze.1
new file mode 100644
index 0000000..650856b
--- /dev/null
+++ b/data/ggaze.1
@@ -0,0 +1,71 @@
+.TH GGAZE 1 "2026-07-12" "ggaze 0.1.0" "User Commands"
+.SH NAME
+ggaze \- fast native image viewer for culling camera shoots
+.SH SYNOPSIS
+.B ggaze
+[\fIFILE\fR | \fIFOLDER\fR] [\fI--version\fR] [\fI--help\fR]
+.SH DESCRIPTION
+ggaze (GNOME Gaze) is a small, fast, native image viewer for Fedora Linux,
+written in C with GTK4. It opens a file or folder, shows a thumbnail grid,
+and lets you flip through the shoot with vi-style keys, check EXIF, trash
+rejects, and move on. No library, no database, no sidecars.
+.SH KEYBINDINGS
+.TP
+.B h / l / Left / Right
+Previous / next image
+.TP
+.B Enter / Esc
+Grid \[ua] large view / contextual back
+.TP
+.B i
+Info overlay (EXIF)
+.TP
+.B d / D
+Trash to ./.Trash (undoable) / permanent delete
+.TP
+.B u
+Undo last trash or delete
+.TP
+.B v / V / Ctrl+a
+Toggle mark / range mark / mark all
+.TP
+.B m
+Move marked images to a destination popup
+.TP
+.B e
+Open in an external program popup
+.TP
+.B !
+Run a shell script popup
+.TP
+.B a
+Quick GEGL enhance popup (optional)
+.TP
+.B f / S
+Fullscreen / slideshow
+.TP
+.B t
+Toggle grid \[ua] large
+.TP
+.B + / -
+Zoom in / out (large) or grow / shrink thumbnails (grid)
+.TP
+.B o
+Open file/folder dialog
+.TP
+.B ?
+Shortcuts overlay
+.TP
+.B q
+Quit
+.SH FILES
+.TP
+.I ~/.cache/thumbnails/
+freedesktop thumbnail cache (shared with other compliant apps)
+.TP
+.I ./.Trash/
+Local trash folder beside the images (recoverable, not the system trash)
+.SH SEE ALSO
+.BR feh (1),
+.BR nsxiv (1),
+.BR gthumb (1) \ No newline at end of file
diff --git a/data/meson.build b/data/meson.build
index c12b619..8dea27b 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,13 +1,18 @@
-# ggaze data: .desktop entry + GSettings schema. Full AppStream/icon install
-# lands in M10; this wires the basics so `meson install` lays out the app and
-# compiles the schema. gnome.post_install also refreshes the desktop database.
+# ggaze data: .desktop entry + GSettings schema + AppStream metainfo + man page.
+# `meson install` lays out the app, compiles the schema, and refreshes the
+# desktop database.
install_data('org.buetow.ggaze.desktop',
install_dir : join_paths(get_option('datadir'), 'applications'))
+install_data('org.buetow.ggaze.metainfo.xml',
+ install_dir : join_paths(get_option('datadir'), 'metainfo'))
+
install_data('org.buetow.ggaze.gschema.xml',
install_dir : join_paths(get_option('datadir'), 'glib-2.0', 'schemas'))
+install_man('ggaze.1')
+
gnome = import('gnome')
gnome.post_install(
glib_compile_schemas : true,
diff --git a/data/org.buetow.ggaze.metainfo.xml b/data/org.buetow.ggaze.metainfo.xml
new file mode 100644
index 0000000..a2cebf1
--- /dev/null
+++ b/data/org.buetow.ggaze.metainfo.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- AppStream metainfo for ggaze (see https://www.freedesktop.org/software/appstream/docs/) -->
+<component type="desktop-application">
+ <id>org.buetow.ggaze</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-3.0-or-later</project_license>
+ <name>ggaze</name>
+ <summary>Fast native image viewer for culling camera shoots</summary>
+ <description>
+ <p>ggaze (GNOME Gaze) is a small, fast, native image viewer for Fedora
+ Linux. It opens a folder of pictures, shows a thumbnail grid, and lets
+ you flip through the shoot with vi-style keys, check EXIF, trash rejects,
+ and move on — no library, no database, no sidecars.</p>
+ </description>
+ <launchable type="desktop-id">org.buetow.ggaze.desktop</launchable>
+ <url type="homepage">https://codeberg.org/snonux/ggaze</url>
+ <provides>
+ <binary>ggaze</binary>
+ </provides>
+ <categories>
+ <category>Graphics</category>
+ <category>Viewer</category>
+ <category>Photography</category>
+ </categories>
+ <keywords>
+ <keyword>image</keyword>
+ <keyword>viewer</keyword>
+ <keyword>photo</keyword>
+ <keyword>camera</keyword>
+ <keyword>culling</keyword>
+ </keywords>
+ <recommends>
+ <control>keyboard</control>
+ <control>mouse</control>
+ </recommends>
+ <content_rating type="oars-1.1" />
+ <releases>
+ <release version="0.1.0" date="2026-07-12">
+ <description><p>Initial development release.</p></description>
+ </release>
+ </releases>
+</component> \ No newline at end of file
diff --git a/packaging/ggaze.spec b/packaging/ggaze.spec
new file mode 100644
index 0000000..e67336e
--- /dev/null
+++ b/packaging/ggaze.spec
@@ -0,0 +1,49 @@
+Name: ggaze
+Version: 0.1.0
+Release: 1%{?dist}
+Summary: Fast native image viewer for culling camera shoots
+
+License: GPL-3.0-or-later
+URL: https://codeberg.org/snonux/ggaze
+Source0: %{url}/archive/%{version}.tar.gz
+
+BuildRequires: meson ninja-build gcc
+BuildRequires: gtk4-devel glib2-devel libadwaita-devel gdk-pixbuf2-devel
+BuildRequires: libexif-devel libjpeg-turbo-devel
+# Optional (built if available):
+BuildRequires: gegl04-devel babl-devel
+BuildRequires: libjxl-devel libheif-devel
+# BuildRequires: libavif-devel # uncomment when available
+
+Requires: gtk4 glib2 libadwaita gdk-pixbuf2 libexif libjpeg-turbo
+
+%description
+ggaze (GNOME Gaze) is a small, fast, native image viewer for Fedora Linux.
+It opens a folder of pictures, shows a thumbnail grid, and lets you flip
+through the shoot with vi-style keys, check EXIF, trash rejects, and move
+on. No library, no database, no sidecars.
+
+%prep
+%setup -q
+
+%build
+%meson -Dgegl=auto -Djxl=auto -Davif=auto -Dheif=auto
+%meson_build
+
+%install
+%meson_install
+
+%check
+%meson_test
+
+%files
+%{_bindir}/ggaze
+%{_datadir}/applications/org.buetow.ggaze.desktop
+%{_datadir}/glib-2.0/schemas/org.buetow.ggaze.gschema.xml
+%{_datadir}/metainfo/org.buetow.ggaze.metainfo.xml
+%{_mandir}/man1/ggaze.1*
+%license LICENSE
+
+%changelog
+* Sat Jul 12 2026 ggaze contributors <paul@buetow.org> - 0.1.0-1
+- Initial RPM package \ No newline at end of file
diff --git a/tests/meson.build b/tests/meson.build
index d186207..adf1359 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -236,4 +236,13 @@ test_progressive_jpeg = executable(
test('progressive_jpeg', test_progressive_jpeg,
suite : 'integration', env : fixtures_env)
+test_full_lifecycle = executable(
+ 'test_full_lifecycle', ['test_full_lifecycle.c', ggaze_conf_h],
+ include_directories : [inc, src_inc],
+ dependencies : ggaze_deps,
+ link_with : ggaze_lib, install : false,
+)
+test('full_lifecycle', test_full_lifecycle,
+ suite : 'integration', env : fixtures_env)
+
subdir('integration') \ No newline at end of file
diff --git a/tests/test_full_lifecycle.c b/tests/test_full_lifecycle.c
new file mode 100644
index 0000000..df6af48
--- /dev/null
+++ b/tests/test_full_lifecycle.c
@@ -0,0 +1,176 @@
+/*:*
+ * ggaze — full lifecycle integration test (M10)
+ *
+ * Scripts the elevator-pitch session: open a temp folder of fixtures, walk
+ * with h/l, check info (i), trash (d), undo (u), toggle grid (t), count grid
+ * cells, toggle back, and quit. Verifies the end-to-end workflow. Needs a
+ * display (integration suite; CI uses xvfb).
+ *
+ * Copyright (c) 2026 ggaze contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *:*/
+
+#include "viewer.h"
+#include "window.h"
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+static void
+copy_fixture(const char *c_dir, const char *c_name) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_src = g_build_filename(c_fx, c_name, NULL);
+ char *c_dst = g_build_filename(c_dir, c_name, NULL);
+ GFile *p_src = g_file_new_for_path(c_src);
+ GFile *p_dst = g_file_new_for_path(c_dst);
+ GError *p_err = NULL;
+ g_assert_true(g_file_copy(p_src, p_dst, G_FILE_COPY_OVERWRITE, NULL, NULL,
+ NULL, &p_err));
+ g_assert_no_error(p_err);
+ g_object_unref(p_src);
+ g_object_unref(p_dst);
+ g_free(c_src);
+ g_free(c_dst);
+}
+
+static void
+cleanup_temp_dir(char *c_dir) {
+ GFile *p_dir = g_file_new_for_path(c_dir);
+ GFileEnumerator *p_e =
+ g_file_enumerate_children(p_dir, "standard::name,standard::type",
+ G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e != NULL) {
+ GFileInfo *p_info;
+ while ((p_info = g_file_enumerator_next_file(p_e, NULL, NULL)) != NULL) {
+ GFile *p_child = g_file_get_child(p_dir, g_file_info_get_name(p_info));
+ if (g_file_info_get_file_type(p_info) == G_FILE_TYPE_DIRECTORY) {
+ GFileEnumerator *p_e2 = g_file_enumerate_children(
+ p_child, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e2 != NULL) {
+ GFileInfo *p_i2;
+ while ((p_i2 = g_file_enumerator_next_file(p_e2, NULL, NULL)) !=
+ NULL) {
+ GFile *p_c2 =
+ g_file_get_child(p_child, g_file_info_get_name(p_i2));
+ g_file_delete(p_c2, NULL, NULL);
+ g_object_unref(p_c2);
+ g_object_unref(p_i2);
+ }
+ g_object_unref(p_e2);
+ }
+ }
+ g_file_delete(p_child, NULL, NULL);
+ g_object_unref(p_child);
+ g_object_unref(p_info);
+ }
+ g_object_unref(p_e);
+ }
+ g_file_delete(p_dir, NULL, NULL);
+ g_object_unref(p_dir);
+ g_free(c_dir);
+}
+
+static void
+drain_main(guint u_ms) {
+ for (guint u = 0; u < u_ms; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+}
+
+static GgazeWindow *
+new_window(void) {
+ return (GGAZE_WINDOW(g_object_new(GGAZE_TYPE_WINDOW, NULL)));
+}
+
+static GdkTexture *
+viewer_texture(GgazeWindow *p_win) {
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ GtkWidget *p_large = gtk_stack_get_child_by_name(p_stack, "large");
+ return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large)));
+}
+
+static void
+test_full_lifecycle(void) {
+ GError *p_err = NULL;
+ char *c_dir = g_dir_make_tmp("ggaze-life-XXXXXX", &p_err);
+ g_assert_no_error(p_err);
+ copy_fixture(c_dir, "plain.jpg");
+ copy_fixture(c_dir, "rot6.jpg");
+ copy_fixture(c_dir, "small.png");
+
+ char *c_plain_path = g_build_filename(c_dir, "plain.jpg", NULL);
+ GFile *p_plain = g_file_new_for_path(c_plain_path);
+ g_free(c_plain_path);
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+
+ /* 1. Wait for the first image to load. */
+ for (guint u = 0; u < 3000 && viewer_texture(p_win) == NULL; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+ g_assert_nonnull(viewer_texture(p_win));
+
+ /* 2. Walk next (l) → rot6.jpg (4x8 after orientation). */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.next", NULL);
+ for (guint u = 0; u < 3000; u++) {
+ GdkTexture *p_t = viewer_texture(p_win);
+ if (p_t && gdk_texture_get_width(p_t) == 4 &&
+ gdk_texture_get_height(p_t) == 8)
+ break;
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+ g_assert_cmpint(gdk_texture_get_width(viewer_texture(p_win)), ==, 4);
+
+ /* 3. Info (i). */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.info", NULL);
+ drain_main(200);
+
+ /* 4. Trash (d) → .Trash created, advances to next. */
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ GFile *p_trash = g_file_get_child(p_dirf, ".Trash");
+ g_assert_false(g_file_query_exists(p_trash, NULL));
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.trash", NULL);
+ drain_main(500);
+ g_assert_true(g_file_query_exists(p_trash, NULL));
+
+ /* 5. Undo (u) → file restored. */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.undo", NULL);
+ drain_main(500);
+ g_assert_true(g_file_query_exists(p_plain, NULL));
+
+ /* 6. Toggle to grid (t) → 3 cells. */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.toggle-view", NULL);
+ drain_main(200);
+ GtkStack *p_stack = ggaze_window_get_stack(p_win);
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "grid");
+
+ /* 7. Toggle back to large (t). */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.toggle-view", NULL);
+ drain_main(200);
+ g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "large");
+
+ g_object_unref(p_trash);
+ g_object_unref(p_dirf);
+ g_object_unref(p_plain);
+ g_object_unref(p_win);
+ drain_main(500);
+ cleanup_temp_dir(c_dir);
+}
+
+int
+main(int i_argc, char **c_argv) {
+ g_test_init(&i_argc, &c_argv, NULL);
+ g_log_set_always_fatal(G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+ if (!gtk_init_check()) {
+ g_test_skip("no display available (run under xvfb)");
+ return (g_test_run());
+ }
+ g_test_add_func("/lifecycle/full", test_full_lifecycle);
+ return (g_test_run());
+} \ No newline at end of file