| Age | Commit message (Collapse) | Author |
|
Wire the navigator mark API into the window actions, grid badges, and
keyboard/pointer controls:
- Navigator: add a path-based range anchor (p_last_mark) set on `v`
toggle-on and cleared on unmark-of-anchor / clear_marks / remove /
mark_removed / rescan-prune; expose navigator_get_last_mark().
navigator_mark_removed now clears the removed file's mark (decision Q
/ task du0) and the anchor if it was it, without touching unrelated
marks, and emits "changed" whenever the removed set or marks actually
changed so grid badges and the header never go stale.
- Window: add win.mark-range (`V`: range from the last-mark anchor to the
current/grid-selected image), register it, wire Shift+V, and document
it in the shortcuts overlay. Trash/delete clear marks via mark_removed.
- Grid: middle-click on a cell selects it and dispatches win.mark
(pointer-accessible marks).
- Tests: unit test_marks_anchor_and_remove and
test_marks_anchor_pruned_on_rescan; integration test_marks.c covering
toggle, all+clear, range, persistence across view switch,
clear-on-trash, and the middle-click gesture wiring; updated
test_shortcut.c action table + count.
clang-format clean; full suite + ASan leak check green.
|
|
The enhancer module existed (GEGL presets: Auto-fix, Brightness, Contrast,
Saturation, Warm, Cool, Sharpen, Denoise) but was never connected to the
app and GEGL was never initialized. Wire it up:
- app.c: gegl_init() once in GApplication::startup (GEGL-gated).
- enhancer.c: add enhancer_load(GFile) -> GeglBuffer (gegl:load) and
enhancer_buffer_to_texture(GeglBuffer) -> GdkTexture (RGBA8 ->
GdkMemoryTexture) for the live preview bridge.
- enhancer_export: pick the saver from the output extension (jpg/png/webp)
instead of always gegl:jpg-save (ju0 - was writing JPEG bytes into .png);
verify the save actually produced a non-empty newer file instead of
trusting g_file_test(EXISTS) on a pre-existing path (ku0 - false success).
- window.c: win.enhance (a) cycles the active preset and previews it on the
current image (switches to large view, loads via GEGL, applies, shows the
texture); win.enhance-save (s) exports <stem>-enhanced.<ext> with the
active preset, never overwriting the original. Preview resets on
navigation; the title shows the active preset.
- shortcuts.c: a -> win.enhance, s -> win.enhance-save; ? overlay Enhance
group.
- tests: test_enhancer gains load_and_to_texture, export_format (PNG/JPEG
signature checks), export_real_success (ku0 - parent-missing and
pre-existing-directory both return FALSE). LSAN suppressions cover
GEGL's jpg-load plugin leak. test_shortcut full-table updated (25 rows).
Synchronous apply (may briefly block on large images) and a navigate-away
dirty prompt are deliberately out of scope for v1.
|
|
clipboard_copy_uris wrapped its URI text as a generic G_TYPE_STRING
provider, so targets could not request text/uri-list and file-manager
paste failed. Extracted clipboard_build_uri_provider builds a union of
two byte providers: text/uri-list (RFC 2483 CRLF-terminated URI lines,
trailing CRLF) and text/plain (newline-joined local paths, URI fallback
for non-local files). clipboard_copy_uris sets the union on the
GdkClipboard with correct refcounting (new_union steals the sub-provider
refs; clipboard takes its own ref).
Empty list returns NULL and leaves the clipboard untouched.
New integration test tests/test_clipboard.c: serializes the provider's
content per MIME type via gdk_content_provider_write_mime_type_async
into a GMemoryOutputStream and memcmps the bytes. Covers formats
(both MIME advertised, bogus type absent), single + multiple files
(exact CRLF/newline-joined bytes), empty list (no crash), and a
negative (unknown MIME rejected with GError).
|
|
The GtkShortcutController on the window used MANAGED scope, so the
viewer's own GtkEventControllerKey consumed key events before the
window-level win.* shortcuts were consulted — t (toggle-view) and Esc
(back) never fired. Switch to GLOBAL scope, which registers the
shortcuts with the toplevel's global shortcut manager consulted before
child key controllers. Comment notes the future-text-entry caveat.
New integration test tests/test_shortcut.c (3 subtests):
- controller_scope: asserts the window's shortcut controller is GLOBAL
(and is ours, not GtkApplicationWindow's mnemonic controller).
- keypath_toggle_and_back: verifies the t→win.toggle-view and
Escape→win.back bindings (bidirectional keyval/action check) and
dispatches them via gtk_shortcut_action_activate (the controller's own
dispatch primitive), asserting the stack flips large↔grid. GTK 4.22
exposes no public GdkKeyEvent synthesis API, so this is the most
faithful exercise of the keyboard shortcut path available.
- full_table_registered: asserts all 16 win.* actions and all 20
shortcut rows are registered, guarding against table regressions.
Notes a pre-existing, out-of-scope keybinding conflict: GTK4 normalizes
GDK_KEY_G/0 to GDK_KEY_g/0 in GtkKeyvalTrigger, so win.last (G) collides
with win.first (g); left unfixed per task's 'preserve existing behavior'.
|
|
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).
|
|
M9 (task wt0): GEGL quick-enhance (optional, feature-gated).
- src/enhancer.{c,h}: 8 built-in presets (Auto-fix, Brightness, Contrast,
Saturation, Warm, Cool, Sharpen, Denoise) using gegl:stretch-contrast,
gegl:exposure, gegl:brightness-contrast, gegl:saturation, gegl:color-enhance,
gegl:unsharp-mask, gegl:noise-reduction. enhancer_apply via buffer-source +
buffer-sink pipeline; enhancer_export via gegl:jpg-save. User graph presets
deferred (NOT_SUPPORTED in v1).
- meson: gegl_dep conditional (-DHAVE_GEGL); enhancer.c in libggae.
- tests: unit test_enhancer (presets dims+non-zero, export file written).
LSAN suppressions for GEGL third-party init/node leaks.
- 20/20 green, ASan clean (with GEGL lib suppressions).
|
|
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.
|
|
M6: progressive low-res preview via libjpeg-turbo.
- jpeg.c: two-phase decode (1/8 low-res via progress_cb, full via GdkPixbuf
with EXIF orientation). Feature-gated.
- loader.h/c: LoaderProgressCb + load_progressive on backend struct;
loader_load_async accepts progress params; JPEG special-cased in task thread.
- window.c: _load_progress_cb marshals partial to main thread.
- tests: test_loader_jpeg (progress + dims), test_progressive_jpeg (sample).
16/16 green, ASan clean.
|
|
M4 (task ot0): fullscreen + slideshow + info overlay.
- src/info.{c,h}: plain-C EXIF gather via libexif (camera, lens, focal,
aperture, shutter, ISO, datetime, orientation) + file info (dims, format,
size). info_format() produces the overlay text. Unit-tested.
- src/window.c: fullscreen toggle (f), slideshow (S, 3s auto-advance), info
overlay (i, GtkLabel in GtkOverlay, auto-hide 5s), Esc contextual back
(fullscreen→unfullscreen→large→grid→quit). Stack wrapped in GtkOverlay.
ggaze_window_get_stack() accessor for tests.
- src/shortcuts.c: f, S, i, Escape bindings.
- meson.build: libexif dep added.
- tests: unit test_info (EXIF extraction + orientation tag). 14/14 green,
ASan clean.
|
|
M7 (task jt0): thumbnail cache + grid view + trash.
- src/trash.{c,h}: ./Trash bin (lazy create, collision suffix -1/-2, restore_last
for u undo, permanent delete for D). Plain-C, unit-tested.
- src/thumbnail.{c,h}: freedesktop TMS cache (normal/large/x-large buckets,
md5(URI) key, Thumb::URI/MTime/Size, mtime verify). Async GTask worker with
GCancellable so the grid can cancel pending requests on dispose.
- src/gridview.{c,h}: GgazeGrid (GtkWidget wrapping GtkFlowBox): one cell per
navigator file, lazy thumbnail decode on realize, +/- resize 64-512, dim
removed cells, mark badges, Enter/double-click → 'activate' signal, cursor
sync. ggaze_grid_detach() breaks the nav ref before dispose.
- src/navigator.{c,h}: removed set (mark_removed/is_removed/unmark_removed);
_relist preserves removed items (stay dimmed even if absent from disk);
get_remaining = count - removed_count.
- src/window.c: d (trash+mark_removed+advance), D (permanent delete, confirm
dialog if >1 marked), u (restore_last+rescan), t (toggle grid/large),
+/- (viewer zoom in large / grid resize). Header counter uses remaining.
- src/shortcuts.c: d/D/u/t/+/= bindings added.
- tests: unit test_trash + test_thumbnail, integration test_grid_cull
(grid count + trash/undo + ./sample-images 613-cell grid). 13/13 green,
ASan clean.
Fixed: grid dispose UAF (detach before nav freed), g_file_get_basename leak,
test p_plain lifetime, thumbnail async-cancellable for clean dispose, test
grid-toggle avoiding async-thumbnail drain issues.
|
|
M3 (task it0): responsive + prefetch.
- src/loader/loader.{c,h}: loader_load_async/_finish wrap the sync
loader_load in a GTask worker; the task's source object is the GFile so the
finish callback can check it against navigator.current (last-write-wins).
- src/texturecache.{c,h}: bounded LRU (cap 4) of GFile->GdkTexture, hash+GQueue
O(1) get/put, entry owns the key (no double-unref), evicts LRU on overflow.
- src/window.c: async visible load (cancel-then-recreate single GCancellable);
cache hit -> show+prefetch, miss -> async load; last-write-wins guard
(g_file_equal(current, loaded)) before showing; prefetch next/prev into the
cache via a separate prefetch cancellable (cancelled each round). Finish
callbacks hold a ref on the window (released in the callback) so they never
deref a freed window; dispose cancels both cancellables.
- tests: unit test_texturecache (LRU evict/order/replace/miss), integration
test_responsive_nav (10 rapid next -> last-write-wins). M1/M2 tests updated
to pump the main loop (loads are now async) + drain_main before exit.
- meson: texturecache in libggae; GIO_USE_VFS=local in fixtures_env.
Fixed: texturecache double-unref (key ownership), async-callback UAF (window
ref in callback data + drain in tests), a test g_build_filename leak. Sub-agent
review issues addressed (dispose + cache-hit cancel in-flight loads).
|
|
M2 (task ht0): walk the directory.
- src/navigator.{c,h}: GObject (no GtkWidget) holding the folder listing —
image-MIME filter + extension fallback, hide-RAW-sidecars (decision #33),
sort name/time/size, cursor + prev/next/wrap, path-based marks (survive
re-sort, pruned on rescan/monitor, cleared on remove), nearest-fallback,
GFileMonitor debounced 250ms (decision #28), 'changed' signal. 92% gcov.
- src/shortcuts.{c,h}: GtkShortcutController (MANAGED) binding h/l/Left/Right/
g/G/o/q to win.* actions; one table all milestones add to.
- src/window.{c,h}: open file->parent+current / folder->first; h/l/g/G actions
-> nav -> load -> viewer; single GCancellable last-write-wins (g_file_equal
guard for M3 async); header title 'filename · n/total'; GtkDropTarget
(GDK_TYPE_FILE_LIST, many->first, decision Z); GtkFileDialog open.
- data/gschema: add hide-raw-sidecars key.
- tests: unit test_navigator (12 subtests), integration test_walk_folder
(temp fixtures walk + ./sample-images skip-if-absent). 8/8 green; ASan clean
(GIO_USE_VFS=local in fixtures_env).
Fixed during this milestone: a UAF (borrowed c_name used after unref), a
cancellable double-ref leak, time::modified UINT64 vs INT64, and several
test-harness leaks. Sub-agent review issues addressed (mark pruning on
rescan/monitor, remove-before-cursor test, recursive temp-dir cleanup,
callback _cb naming, connect-before-mutate).
|
|
M1 (task gt0): show one image with zoom/pan.
- src/loader/detect.{c,h}: magic-byte format sniffing (JPEG/PNG/GIF/WebP/
TIFF/ICO/JXL/AVIF/HEIF) -> GgazeFormat; pure, no I/O, unit-testable.
- src/loader/loader.{c,h}: loader_load() sniffs the header and dispatches to
the first registered backend; GgazeLoaderBackend struct; pixbuf is the
fallback (last, accepts UNKNOWN). M1 ships only the pixbuf backend.
- src/loader/backends/pixbuf.c: GdkPixbufLoader decode +
gdk_pixbuf_apply_embedded_orientation (decision #26) -> GdkTexture via
gdk_memory_texture_new (avoids the deprecated gdk_texture_new_for_pixbuf).
- src/viewer.{c,h}: GgazeViewer : GtkWidget custom widget (decision #31) —
fit/100%/in/out zoom, cursor-centered zoom, drag-to-pan with clamping,
dark background, GtkSnapshot render nodes.
- src/window.c: open -> loader_load -> viewer_set_texture -> stack 'large'.
- tests: unit test_detect (13 cases) + test_loader_pixbuf (plain/rotated-EXIF
8x4 orient6->4x8/png/rgba/missing/unsupported jxl-avif-heif/corrupt),
integration test_open_and_show (fixture + rotated + ./sample-images
skip-if-absent). 6/6 green; detect 97% / loader 91% / pixbuf 87% coverage.
- fixtures: gen.py produces plain.jpg, rot6.jpg, small.png, rgba.png.
- AGENTS.md: documents the ./sample-images optional test corpus convention.
Sub-agent review fixes: use-after-free of c_name in ggaze_window_open
(BLOCKER), gtk_stack_get_pages leak in test_window (BLOCKER), coverage gap,
dead branch, viewer measure, _prefix/_cb naming, include order, extern in
header, pan clamp, stale comments — all addressed.
|
|
M0 (task ft0): the app shell — no feature views yet (those are M1 large, M7
grid).
- src/main.c: g_application_run + --version via handle-local-options (--help
is GApplication's own); exits before activate so --version works headless.
- src/app.{c,h}: GgazeApp : AdwApplication : GtkApplication (libadwaita
styling, decision #29), single-instance (decision #32),
G_APPLICATION_HANDLES_OPEN; activate + open reuse the active window
(replace-on-new-open); ggaze_window_open remembers the GFile.
- src/window.{c,h}: GgazeWindow : GtkApplicationWindow with AdwHeaderBar and a
GtkStack (children 'grid'/'large', default 'grid'); dispose clears the file.
- data/org.buetow.ggaze.desktop (image/* MIME, validated) and
data/org.buetow.ggaze.gschema.xml (all keys + defaults, --strict clean).
- meson: libggaze static lib shared by the executable and tests; install to
bindir/applications/glib-2.0 schemas; gnome.post_install compiles schemas +
updates desktop db.
- tests: unit test_app (--version/--help/unknown via subprocess), integration
test_window (offscreen stack checks + open-titles), bootstrap. 3/3 green.
- docs/tech-stack.md: window-geometry shape corrected to (width,height,
fullscreen,maximized) to match the schema.
|
|
Phase 0 (task et0): lays the build/test/CI/convention groundwork before any
feature code.
- meson.build: project ggaze C11 -Wall -Wextra; deps gtk4/glib/gio/libadwaita;
feature options gegl/jxl/avif/heif (auto); generated ggaze-config.h with
app id, version, GGAZE_HAVE_* feature flags.
- tests: GLib g_test harness with separate unit/integration suites
(--suite unit / --suite integration); test_bootstrap sanity test proves
the harness compiles and the config header is reachable; -Db_coverage=true
coverage target wired.
- CI: .woodpecker/ci.yml three lanes (minimal, gegl, asan) with xvfb for the
integration track and a coverage lane that warns until M10.
- conventions: .clang-format (3-space, 80-col, K&R, *-on-var, return type on
its own line) + .editorconfig matching docs/coding-conventions.md; CI
clang-format --dry-run -Werror gate.
- top-level README.md (humans) and AGENTS.md (agents) pointing into docs/.
- LICENSE (GPL-3.0-or-later), .gitignore, dir placeholders.
Acceptance: meson setup build && ninja -C build && meson test -C build green;
clang-format --dry-run --Werror clean.
|