diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-14 14:55:24 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-14 14:55:24 +0300 |
| commit | 0b9dc4eec752d6d48aab696d06b33f4782e9db4f (patch) | |
| tree | 28271308c6280a6d0bb87ed31ebe8a1a01837b67 | |
| parent | de1d108d97bc55a6f047c47bb56667068aac71ff (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.1 | 71 | ||||
| -rw-r--r-- | data/meson.build | 11 | ||||
| -rw-r--r-- | data/org.buetow.ggaze.metainfo.xml | 42 | ||||
| -rw-r--r-- | packaging/ggaze.spec | 49 | ||||
| -rw-r--r-- | tests/meson.build | 9 | ||||
| -rw-r--r-- | tests/test_full_lifecycle.c | 176 |
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 |
