diff options
| -rw-r--r-- | data/.gitkeep | 0 | ||||
| -rw-r--r-- | data/meson.build | 15 | ||||
| -rw-r--r-- | data/org.buetow.ggaze.desktop | 13 | ||||
| -rw-r--r-- | data/org.buetow.ggaze.gschema.xml | 106 | ||||
| -rw-r--r-- | docs/IMPLEMENTATION.md | 18 | ||||
| -rw-r--r-- | docs/tech-stack.md | 2 | ||||
| -rw-r--r-- | meson.build | 18 | ||||
| -rw-r--r-- | src/app.c | 74 | ||||
| -rw-r--r-- | src/app.h | 28 | ||||
| -rw-r--r-- | src/main.c | 51 | ||||
| -rw-r--r-- | src/meson.build | 13 | ||||
| -rw-r--r-- | src/window.c | 74 | ||||
| -rw-r--r-- | src/window.h | 37 | ||||
| -rw-r--r-- | tests/meson.build | 26 | ||||
| -rw-r--r-- | tests/test_app.c | 80 | ||||
| -rw-r--r-- | tests/test_window.c | 80 |
16 files changed, 623 insertions, 12 deletions
diff --git a/data/.gitkeep b/data/.gitkeep deleted file mode 100644 index e69de29..0000000 --- a/data/.gitkeep +++ /dev/null diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..c12b619 --- /dev/null +++ b/data/meson.build @@ -0,0 +1,15 @@ +# 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. + +install_data('org.buetow.ggaze.desktop', + install_dir : join_paths(get_option('datadir'), 'applications')) + +install_data('org.buetow.ggaze.gschema.xml', + install_dir : join_paths(get_option('datadir'), 'glib-2.0', 'schemas')) + +gnome = import('gnome') +gnome.post_install( + glib_compile_schemas : true, + update_desktop_database : true, +)
\ No newline at end of file diff --git a/data/org.buetow.ggaze.desktop b/data/org.buetow.ggaze.desktop new file mode 100644 index 0000000..c5c31d0 --- /dev/null +++ b/data/org.buetow.ggaze.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=ggaze +GenericName=Image Viewer +Comment=Fast native image viewer for culling camera shoots +# M0 stub; full MIME/keywords/AppStream wiring lands in M10. +Exec=ggaze %f +Icon=ggaze +Terminal=false +StartupNotify=true +Categories=Graphics;Viewer;Photography; +MimeType=image/png;image/jpeg;image/gif;image/webp;image/tiff;image/avif;image/heif;image/heic;image/jxl; +Keywords=image;viewer;photo;camera;culling;
\ No newline at end of file diff --git a/data/org.buetow.ggaze.gschema.xml b/data/org.buetow.ggaze.gschema.xml new file mode 100644 index 0000000..70ddfa4 --- /dev/null +++ b/data/org.buetow.ggaze.gschema.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ggaze GSettings schema — org.buetow.ggaze. + + ALL keys are declared up front (M0) with sensible defaults so later modules + just read them. See docs/tech-stack.md "Settings keys" for the intended + semantics; consumption lands with the matching milestones. + + `window-geometry` is (iiib) = (width, height, fullscreen-as-int-0/1, + maximized). The width/height/maximized mapping is finalized in M10; the + shape is fixed here so the schema is stable. +--> +<schemalist> + <enum id="org.buetow.ggaze.sort"> + <value nick="name" value="0"/> + <value nick="capture-time" value="1"/> + <value nick="size" value="2"/> + </enum> + + <enum id="org.buetow.ggaze.background"> + <value nick="black" value="0"/> + <value nick="dark" value="1"/> + <value nick="grey" value="2"/> + <value nick="checker" value="3"/> + </enum> + + <enum id="org.buetow.ggaze.scroll-behavior"> + <value nick="zoom" value="0"/> + <value nick="pan-when-zoomed" value="1"/> + <value nick="navigate" value="2"/> + </enum> + + <schema id="org.buetow.ggaze" path="/org/buetow/ggaze/"> + <key name="sort" enum="org.buetow.ggaze.sort"> + <default>'name'</default> + <summary>Sort order</summary> + <description>Sort the folder by name, EXIF capture time, or size.</description> + </key> + + <key name="wrap" type="b"> + <default>true</default> + <summary>Wrap at folder ends</summary> + <description>Wrap prev/next at the first/last image.</description> + </key> + + <key name="background" enum="org.buetow.ggaze.background"> + <default>'dark'</default> + <summary>Viewer background</summary> + <description>Background behind the image in large view.</description> + </key> + + <key name="scroll-behavior" enum="org.buetow.ggaze.scroll-behavior"> + <default>'zoom'</default> + <summary>Scroll behavior</summary> + <description>What the mouse scroll wheel does in large view.</description> + </key> + + <key name="slideshow-delay" type="d"> + <default>3.0</default> + <summary>Slideshow delay</summary> + <description>Seconds between slides in slideshow mode.</description> + </key> + + <key name="thumbnail-size" type="i"> + <default>128</default> + <summary>Grid thumbnail size</summary> + <description>Pixel size of grid thumbnails (resizable via +/-).</description> + </key> + + <key name="hide-trashed" type="b"> + <default>false</default> + <summary>Hide trashed items</summary> + <description>Hide ./Trash/deleted items from the grid entirely.</description> + </key> + + <key name="window-geometry" type="(iiib)"> + <default>(800, 600, 0, false)</default> + <summary>Window geometry</summary> + <description>(width, height, fullscreen, maximized) restored on launch.</description> + </key> + + <key name="destinations" type="a(ss)"> + <default>[]</default> + <summary>Move destinations</summary> + <description>Ordered (name, path) pairs for the `m` move popup; order = hotkey order.</description> + </key> + + <key name="editors" type="a(ss)"> + <default>[]</default> + <summary>External editors</summary> + <description>Ordered (name, command) pairs for the `e` open-in popup; %f = current file.</description> + </key> + + <key name="scripts" type="a(ss)"> + <default>[]</default> + <summary>Shell scripts</summary> + <description>Ordered (name, command) pairs for the `!` run-script popup; %f/%d expanded, run via /bin/sh -c.</description> + </key> + + <key name="enhance-presets" type="a(ss)"> + <default>[]</default> + <summary>Enhance presets</summary> + <description>Ordered (name, gegl-graph) pairs for the `a` enhance popup (GEGL only).</description> + </key> + </schema> +</schemalist>
\ No newline at end of file diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 359511a..c2be2c3 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -144,7 +144,8 @@ green (empty suites); CI builds the matrix; `clang-format --dry-run` clean. - `src/loader/backends/pixbuf.c` — GdkPixbuf → `gdk_pixbuf_apply_embedded_orientation` (decision #26) → `GdkTexture`. - `src/viewer.c/.h` — `GgazeViewer : GtkWidget` (custom, decision #31): zoom, - pan, fit, cursor-centered zoom, pan clamp; `viewer_set_texture`. + pan, fit, cursor-centered zoom, pan clamp, `viewer_set_texture`; scroll + follows `scroll-behavior` (`zoom` / `pan-when-zoomed` / `navigate`). - `src/window.c` — wire `open` → load → `viewer_set_texture`; large view. **Tests** @@ -253,7 +254,7 @@ bounded memory. trashed/deleted; `Enter`/double-click → large; cursor sync both ways. - `src/trash.c/.h` — `./Trash` bin (lazy, collision suffix), restore-last, permanent delete. -- Window: `d`/`D`/`u`; `d` advances; counter = remaining; `t` toggle. +- Window: `d`/`D`/`u`; `d` advances; `D` on **>1 marked** asks a confirm dialog; counter = remaining; `t` toggle. **Tests** - Unit: `test_thumbnail.c`, `test_trash.c`. @@ -273,11 +274,15 @@ bounded memory. acts on original file (decision #38). - `src/runner.c/.h` — `scripts` `a(ss)`; `/bin/sh -c`, single-quoted `%f`/`%d` (decision S); `wait_async`; rescan + toast on done. -- `src/clipboard.c/.h` — `image/png` (decode in `GTask`) / `text/uri-list`; - union provider for one file (decision V). +- `src/clipboard.c/.h` — copies the **displayed** image (modified if a + preview is active, else original) as `image/png` (decode in `GTask`) / + marked files as `text/uri-list`; union provider for one file (decision V). + `Ctrl+Shift+c` (later) copies the original/path. - Reusable popover (`ggaze_popup`) for move/open/scripts/(enhance later): `(hotkey, label)` rows + key controller firing on digit/letter. - Unified one-level undo `u` (decision P). +- Mark UI: `v`/`V`/`Ctrl+a`/`Esc`; header subtitle shows `N marked`; grid + check-badges + large-view indicator. - Preferences dialog (`,`): `AdwPreferencesWindow` editing ordered `a(ss)` lists + sort/background/scroll/slideshow/hide-trashed. @@ -296,8 +301,9 @@ bounded memory. - `meson` `gegl` feature; `src/enhancer.c/.h` plain-C. - `enhancer_get_presets` (built-in programmatic / user `gegl-graph` text, decision #34); `enhancer_apply` in `GTask`; `enhancer_export` → - `<stem>-enhanced.<ext>` same dir, collision `-1`; EXIF Orientation=1 on - export (decision #26). + `<stem>-enhanced.<ext>` same dir, collision `-1`, EXIF Orientation=1 on + export (decision #26); defaults to the original format (JPEG quality 95); + a format/quality chooser and a lossless `jpegtran`/`exiftool` path are later. - Viewer: preset active → import → `GeglBuffer` → apply → `GdkTexture`; not during scrub (decision #34); "enhanced" badge. - Compose order load→enhance→rotate→straighten→crop→export (decision #35). diff --git a/docs/tech-stack.md b/docs/tech-stack.md index f8b54b3..7dc1191 100644 --- a/docs/tech-stack.md +++ b/docs/tech-stack.md @@ -70,7 +70,7 @@ it. Treat as a later milestone, but design the loader signature for it now. - `slideshow-delay` — double (seconds) - `thumbnail-size` — int: grid thumbnail pixel size (resizable via `+`/`-`) - `hide-trashed` — bool -- `window-geometry` — `(iiib)` width, height, maximized; persisted, restored on launch +- `window-geometry` — `(iiib)` (width, height, fullscreen, maximized); persisted, restored on launch - `destinations` — `a(ss)`: ordered array of `(name, path)` pairs for the `m` move popup. List order determines auto-assigned hotkeys (`1`, `2`, …). Edited via the Preferences dialog (`,`) or `gsettings`. diff --git a/meson.build b/meson.build index 4c67730..e21b70c 100644 --- a/meson.build +++ b/meson.build @@ -43,9 +43,23 @@ ggaze_conf_h = configure_file( configuration : conf, ) -# Include path: source root (generated header lands in the build root, which -# meson adds automatically for targets that list ggaze_conf_h as a source). +# Include paths: source root (generated header) + src/ (module headers). inc = include_directories('.') +src_inc = include_directories('src') + +# --- ggaze library: the app + window (shared between the executable and +# tests so the GApplication/GtkApplicationWindow code is exercised without +# duplicating sources). Plain-C modules will join this library as they +# land (navigator, trash, mover, ...). +ggaze_lib = static_library('ggaze', + files('src/app.c', 'src/window.c'), + include_directories : [inc, src_inc], + dependencies : [gtk4_dep, glib_dep, gio_dep, adwaita_dep], + install : false, +) + +ggaze_deps = [gtk4_dep, glib_dep, gio_dep, adwaita_dep] subdir('src') +subdir('data') subdir('tests')
\ No newline at end of file diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..587feb7 --- /dev/null +++ b/src/app.c @@ -0,0 +1,74 @@ +/*:* + * ggaze — application object + * + * Implements GgazeApp (GgazeApp : AdwApplication : GtkApplication). Wires the + * GApplication `open` signal (file or folder) and the default `activate` + * (no-arg) path to a GgazeWindow, reusing the active window for the + * single-instance "replace on new open" behaviour (decision #32). Real + * file-vs-folder resolution is the navigator's job in M2; M0 just remembers + * the path and titles the window. See docs/architecture.md. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "app.h" +#include "window.h" +#include "ggaze-config.h" + +#include <glib.h> + +struct _GgazeApp { + AdwApplication parent_instance; +}; + +G_DEFINE_TYPE(GgazeApp, ggaze_app, ADW_TYPE_APPLICATION) + +static GgazeWindow * +_ensure_window(GgazeApp *p_app) { + GtkWindow *p_active = + gtk_application_get_active_window(GTK_APPLICATION(p_app)); + if (p_active != NULL && GGAZE_IS_WINDOW(p_active)) { + return (GgazeWindow *)p_active; + } + return (ggaze_window_new(p_app)); +} + +static void +ggaze_app_activate(GApplication *p_app) { + GgazeWindow *p_win = _ensure_window(GGAZE_APP(p_app)); + gtk_window_present(GTK_WINDOW(p_win)); +} + +static void +ggaze_app_open(GApplication *p_app, GFile **p_files, gint n_files, + const gchar *c_hint) { + (void)c_hint; + GgazeApp *p_app_self = GGAZE_APP(p_app); + GgazeWindow *p_win = _ensure_window(p_app_self); + /* open-question Z / decision #27: one file opens it; many files open the + * first file's folder with the first current (navigator wiring in M2). */ + if (n_files > 0 && p_files != NULL) { + ggaze_window_open(p_win, p_files[0]); + } + gtk_window_present(GTK_WINDOW(p_win)); +} + +static void +ggaze_app_init(GgazeApp *p_app) { + (void)p_app; +} + +static void +ggaze_app_class_init(GgazeAppClass *p_klass) { + GApplicationClass *p_app_class = G_APPLICATION_CLASS(p_klass); + p_app_class->activate = ggaze_app_activate; + p_app_class->open = ggaze_app_open; +} + +GgazeApp * +ggaze_app_new(void) { + return ( + GGAZE_APP(g_object_new(GGAZE_TYPE_APP, "application-id", GGAZE_APP_ID, + "flags", G_APPLICATION_HANDLES_OPEN, NULL))); +}
\ No newline at end of file diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..7b40ff6 --- /dev/null +++ b/src/app.h @@ -0,0 +1,28 @@ +#ifndef GGAZE_APP_H +#define GGAZE_APP_H + +/*:* + * ggaze — application object + * + * GgazeApp is the GApplication/single-instance entry point. It owns the + * GtkApplication, registers actions, and routes the GApplication `open` + * signal (a file OR a directory) to the window. See docs/architecture.md + * "Responsibilities / app". + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include <adwaita.h> + +G_BEGIN_DECLS + +#define GGAZE_TYPE_APP (ggaze_app_get_type()) +G_DECLARE_FINAL_TYPE(GgazeApp, ggaze_app, GGAZE, APP, AdwApplication) + +/* Construct a new single-instance ggaze application. */ +GgazeApp *ggaze_app_new(void); + +G_END_DECLS + +#endif /* GGAZE_APP_H */
\ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..69cf84d --- /dev/null +++ b/src/main.c @@ -0,0 +1,51 @@ +/*:* + * ggaze — entry point + * + * Builds the GgazeApp, registers the `--version` option, and hands control to + * g_application_run. `--help` is provided by GApplication itself; `--version` + * is handled in handle-local-options and exits before any window is created + * (so it works headless, which the unit test relies on). See docs/roadmap.md + * M0. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "app.h" +#include "ggaze-config.h" + +#include <glib.h> +#include <gtk/gtk.h> + +static const GOptionEntry GGAZE_OPTIONS[] = { + {"version", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, + "Show version information and exit", NULL}, + {NULL, '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, NULL, NULL}, +}; + +static gint +on_local_options(GApplication *p_app, GVariantDict *p_opts, gpointer p_data) { + (void)p_app; + (void)p_data; + gboolean b_version = FALSE; + if (g_variant_dict_lookup(p_opts, "version", "b", &b_version) && b_version) { + g_print("ggaze %s\n", GGAZE_VERSION); + return (0); /* exit success, before activate */ + } + return (-1); /* continue normal startup */ +} + +int +main(int i_argc, char **c_argv) { + g_set_application_name("ggaze"); + + GgazeApp *p_app = ggaze_app_new(); + g_application_add_main_option_entries(G_APPLICATION(p_app), GGAZE_OPTIONS); + g_signal_connect(p_app, "handle-local-options", G_CALLBACK(on_local_options), + NULL); + + int i_status = g_application_run(G_APPLICATION(p_app), i_argc, c_argv); + + g_object_unref(p_app); + return (i_status); +}
\ No newline at end of file diff --git a/src/meson.build b/src/meson.build index a46993e..187c79d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,3 +1,10 @@ -# ggaze production sources. Populated from M0 onward (see docs/roadmap.md and -# docs/architecture.md for the module map). Phase 0 ships no sources yet — -# only the build/test/CI/convention harness, so `ninja` has no C targets here.
\ No newline at end of file +# ggaze executable. The app/window code lives in libggae (see meson.build); the +# executable only adds the entry point and the --version CLI option. + +ggaze_exe = executable('ggaze', + 'main.c', + include_directories : [inc, src_inc], + dependencies : ggaze_deps, + link_with : ggaze_lib, + install : true, +)
\ No newline at end of file diff --git a/src/window.c b/src/window.c new file mode 100644 index 0000000..049be4e --- /dev/null +++ b/src/window.c @@ -0,0 +1,74 @@ +/*:* + * ggaze — main window + * + * Implements GgazeWindow. Builds an AdwHeaderBar + a GtkStack with two named + * placeholder children ("grid", "large"); the real views arrive in M1 (large) + * and M7 (grid). Tracks the current GFile for later milestones. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "window.h" + +#include <adwaita.h> +#include <glib.h> + +struct _GgazeWindow { + GtkApplicationWindow parent_instance; + GFile *p_file; /* current file/folder, remembered for later use */ + GtkWidget *p_stack; /* GtkStack: grid/large placeholder children */ +}; + +G_DEFINE_TYPE(GgazeWindow, ggaze_window, GTK_TYPE_APPLICATION_WINDOW) + +static void +ggaze_window_dispose(GObject *p_obj) { + GgazeWindow *p_win = GGAZE_WINDOW(p_obj); + g_clear_object(&p_win->p_file); + G_OBJECT_CLASS(ggaze_window_parent_class)->dispose(p_obj); +} + +static void +ggaze_window_class_init(GgazeWindowClass *p_klass) { + GObjectClass *p_obj_class = G_OBJECT_CLASS(p_klass); + p_obj_class->dispose = ggaze_window_dispose; +} + +static void +ggaze_window_init(GgazeWindow *p_win) { + /* Header bar (libadwaita, decision #29). */ + GtkWidget *p_header = adw_header_bar_new(); + gtk_window_set_titlebar(GTK_WINDOW(p_win), p_header); + + /* Two-view stack. Children are placeholders for M1 (large) and M7 (grid). */ + p_win->p_stack = gtk_stack_new(); + gtk_stack_set_transition_type(GTK_STACK(p_win->p_stack), + GTK_STACK_TRANSITION_TYPE_CROSSFADE); + gtk_window_set_child(GTK_WINDOW(p_win), p_win->p_stack); + + GtkWidget *p_grid = gtk_label_new("grid"); + gtk_widget_add_css_class(p_grid, "dim-label"); + GtkWidget *p_large = gtk_label_new("large"); + gtk_widget_add_css_class(p_large, "dim-label"); + gtk_stack_add_named(GTK_STACK(p_win->p_stack), p_grid, "grid"); + gtk_stack_add_named(GTK_STACK(p_win->p_stack), p_large, "large"); + gtk_stack_set_visible_child_name(GTK_STACK(p_win->p_stack), "grid"); +} + +GgazeWindow * +ggaze_window_new(GgazeApp *p_app) { + return (GGAZE_WINDOW(g_object_new(GGAZE_TYPE_WINDOW, "application", p_app, + "default-width", 800, "default-height", + 600, NULL))); +} + +void +ggaze_window_open(GgazeWindow *p_win, GFile *p_file) { + g_return_if_fail(GGAZE_IS_WINDOW(p_win)); + g_return_if_fail(G_IS_FILE(p_file)); + g_set_object(&p_win->p_file, p_file); + char *c_name = g_file_get_basename(p_file); + gtk_window_set_title(GTK_WINDOW(p_win), c_name); + g_free(c_name); +}
\ No newline at end of file diff --git a/src/window.h b/src/window.h new file mode 100644 index 0000000..aeddf64 --- /dev/null +++ b/src/window.h @@ -0,0 +1,37 @@ +#ifndef GGAZE_WINDOW_H +#define GGAZE_WINDOW_H + +/*:* + * ggaze — main window + * + * GgazeWindow : GtkApplicationWindow owns the layout: an AdwHeaderBar and a + * GtkStack with two placeholder children (`grid`, `large`). M0 keeps the + * stack empty of real views; gridview (M7) and viewer (M1) populate it. The + * window remembers the current GFile so later milestones can build on it. + * See docs/architecture.md "Responsibilities / window". + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "app.h" + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GGAZE_TYPE_WINDOW (ggaze_window_get_type()) +G_DECLARE_FINAL_TYPE(GgazeWindow, ggaze_window, GGAZE, WINDOW, + GtkApplicationWindow) + +/* Construct a new window attached to p_app. */ +GgazeWindow *ggaze_window_new(GgazeApp *p_app); + +/* Remember p_file as the current file (or folder) and title the window with + * its basename. The GFile reference is held until replaced or the window is + * destroyed. Real file-vs-folder handling lands in M2. */ +void ggaze_window_open(GgazeWindow *p_win, GFile *p_file); + +G_END_DECLS + +#endif /* GGAZE_WINDOW_H */
\ No newline at end of file diff --git a/tests/meson.build b/tests/meson.build index c0ec9fb..4c02ccd 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -10,6 +10,7 @@ # meson test -C build # ninja -C build coverage (needs lcov + genhtml) +# --- unit track --- test_bootstrap = executable( 'test_bootstrap', ['test_bootstrap.c', ggaze_conf_h], @@ -19,4 +20,29 @@ test_bootstrap = executable( ) test('bootstrap', test_bootstrap, suite : 'unit') +# test_app runs the ggaze binary as a subprocess; meson passes its path. +test_app = executable( + 'test_app', + ['test_app.c', ggaze_conf_h], + include_directories : inc, + dependencies : [glib_dep, gio_dep], + install : false, +) +test('app', test_app, + suite : 'unit', + args : [ggaze_exe.full_path()], + depends : ggaze_exe, +) + +# --- integration track (needs a display; CI uses xvfb-run) --- +test_window = executable( + 'test_window', + ['test_window.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : ggaze_deps, + link_with : ggaze_lib, + install : false, +) +test('window', test_window, suite : 'integration') + subdir('integration')
\ No newline at end of file diff --git a/tests/test_app.c b/tests/test_app.c new file mode 100644 index 0000000..b1438a1 --- /dev/null +++ b/tests/test_app.c @@ -0,0 +1,80 @@ +/*:* + * ggaze — app CLI unit test + * + * Exercises the ggaze binary as a subprocess (so --help/--version, which call + * exit() inside g_application_run, do not terminate the test process itself). + * Verifies: `--version` prints "ggaze" and exits 0; `--help` exits 0; an + * unknown flag exits non-zero. No display is needed — all three exit before + * activate. The ggaze binary path is passed as argv[1] by meson. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "ggaze-config.h" + +#include <glib.h> +#include <gio/gio.h> + +static const char *GGAZE_BIN; + +/* Run GGAZE_BIN with c_arg, return the exit status (0 on success). If + * c_out is non-NULL, receive the combined stdout+stderr. */ +static gint +run_one(const char *c_arg, char **c_out) { + GError *p_err = NULL; + GSubprocess *p_sub = g_subprocess_new(G_SUBPROCESS_FLAGS_STDOUT_PIPE | + G_SUBPROCESS_FLAGS_STDERR_MERGE, + &p_err, GGAZE_BIN, c_arg, NULL); + g_assert_no_error(p_err); + + char *c_stdout = NULL; + g_subprocess_communicate_utf8(p_sub, NULL, NULL, &c_stdout, NULL, &p_err); + g_assert_no_error(p_err); + + g_assert_true(g_subprocess_get_if_exited(p_sub)); + gint i_status = g_subprocess_get_exit_status(p_sub); + + if (c_out != NULL) { + *c_out = c_stdout; + } else { + g_free(c_stdout); + } + g_object_unref(p_sub); + return (i_status); +} + +static void +test_version(void) { + char *c_out = NULL; + gint i_status = run_one("--version", &c_out); + g_assert_cmpint(i_status, ==, 0); + g_assert_nonnull(c_out); + g_assert_nonnull(g_strstr_len(c_out, -1, "ggaze")); + g_free(c_out); +} + +static void +test_help(void) { + /* GApplication provides --help itself; it prints and exits 0. */ + gint i_status = run_one("--help", NULL); + g_assert_cmpint(i_status, ==, 0); +} + +static void +test_unknown_flag(void) { + gint i_status = run_one("--definitely-not-a-ggaze-flag", NULL); + g_assert_cmpint(i_status, !=, 0); +} + +int +main(int i_argc, char **c_argv) { + g_assert_cmpint(i_argc, >=, 2); + GGAZE_BIN = c_argv[1]; + + g_test_init(&i_argc, &c_argv, NULL); + g_test_add_func("/app/version", test_version); + g_test_add_func("/app/help", test_help); + g_test_add_func("/app/unknown_flag", test_unknown_flag); + return (g_test_run()); +}
\ No newline at end of file diff --git a/tests/test_window.c b/tests/test_window.c new file mode 100644 index 0000000..0570397 --- /dev/null +++ b/tests/test_window.c @@ -0,0 +1,80 @@ +/*:* + * ggaze — window smoke test (integration) + * + * Constructs a GgazeWindow offscreen and asserts the two-view stack exists + * (children "grid" and "large", default visible = "grid"), and that + * ggaze_window_open titles the window with the file's basename. + * + * The window is built with g_object_new() (no "application" property): the + * stack/header are constructed in ggaze_window_init, independent of the app + * association. Setting GtkWindow:application requires the GApplication + * ::startup signal to have fired (it does in production via activate/open; + * not in this isolated smoke test). Needs a display, so this lives in the + * `integration` suite (CI runs it under xvfb); skipped cleanly when no + * display is available. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "window.h" + +#include <gio/gio.h> +#include <glib.h> +#include <gtk/gtk.h> + +static GgazeWindow * +new_window(void) { + return (GGAZE_WINDOW(g_object_new(GGAZE_TYPE_WINDOW, NULL))); +} + +static void +test_stack_has_two_views(void) { + GgazeWindow *p_win = new_window(); + g_assert_nonnull(p_win); + + GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win)); + g_assert_nonnull(p_child); + g_assert_true(GTK_IS_STACK(p_child)); + + GtkStack *p_stack = GTK_STACK(p_child); + GListModel *p_pages = G_LIST_MODEL(gtk_stack_get_pages(p_stack)); + g_assert_cmpint(g_list_model_get_n_items(p_pages), ==, 2); + g_assert_nonnull(gtk_stack_get_child_by_name(p_stack, "grid")); + g_assert_nonnull(gtk_stack_get_child_by_name(p_stack, "large")); + g_assert_cmpstr(gtk_stack_get_visible_child_name(p_stack), ==, "grid"); + + g_object_unref(p_win); +} + +static void +test_open_titles_window(void) { + GgazeWindow *p_win = new_window(); + + GFile *p_file = g_file_new_for_path("/tmp/IMG_0001.jpg"); + ggaze_window_open(p_win, p_file); + + g_assert_cmpstr(gtk_window_get_title(GTK_WINDOW(p_win)), ==, "IMG_0001.jpg"); + + g_object_unref(p_file); + g_object_unref(p_win); +} + +int +main(int i_argc, char **c_argv) { + g_test_init(&i_argc, &c_argv, NULL); + + /* Tolerate host GTK WARNINGs (e.g. an unknown gtk-modules key delivered + * by the live GNOME session via XSettings) that g_test_init makes fatal. + * Keep CRITICALs fatal — those are real bugs. */ + 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("/window/stack_has_two_views", test_stack_has_two_views); + g_test_add_func("/window/open_titles_window", test_open_titles_window); + return (g_test_run()); +}
\ No newline at end of file |
