summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-07-22Fix minimal CI lane (gegl=disabled): move _proceed_* out of the GEGL guardmainPaul Buetow
The _proceed_prev/next/first/last helpers are trivial wrappers over the public ggaze_window navigation API and do not depend on GEGL, but were defined only inside the #if GGAZE_HAVE_GEGL block. The always-compiled _action_prev/next/first/last handlers reference them via _maybe_save_then, so the gegl=disabled (minimal) lane failed with '_proceed_prev undeclared' (regression introduced by the enhance save-dialog commit, before M8 marks). Move the four helpers out of the GEGL guard so they are always compiled. Verified: meson setup build-min -Dgegl=disabled -Djxl=disabled -Davif=disabled -Dheif=disabled && ninja -C build-min && meson test -C build-min (23/23 OK); default gegl=enabled lane still 24/24 OK.
2026-07-22enhancer: test stale pre-existing destination export (ku0)Paul Buetow
The _save_buffer fix that rejects pre-existence as proof of a successful save (mtime/size change verification) was already in place. Add the missing test case (c) for a stale pre-existing regular file at the destination, which the task requires alongside the directory and unwritable-destination cases: /enhancer/export_stale_dest: (a) writable stale file -> correctly overwritten; assert the new PNG bytes differ from the stale content (not mere presence). (b) read-only stale file the saver cannot replace -> export returns FALSE with a GError; skipped under root (root bypasses file-mode perms; the directory case in export_real_success covers a root-safe save failure). Gated behind gegl_dep.found() (same as the rest of test_enhancer), so the minimal (gegl=disabled) lane is unaffected. All 7 enhancer subtests + the full 24-test suite pass; ASan/LSan clean.
2026-07-22Integrate navigator marks into window and grid UI (M8)Paul Buetow
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.
2026-07-22enhance: save hint in the panel + Save/Discard/Cancel dialog on navigationPaul Buetow
Add a dim 's Save enhanced copy' hint at the bottom of the enhance panel so the save shortcut is visible right where the toggles are. When you navigate to another image (h/l/g/G) with an unsaved enhance preview active, a Save / Discard / Cancel alert now asks what to do: Save exports <stem>-enhanced.<ext> with the enabled-preset chain then moves on; Discard drops the preview and moves on; Cancel keeps the preview and stays on the current image. _action_prev/next/first/last route through _maybe_save_then; the s action and the dialog's Save button share _enhance_do_save. Non-GEGL builds get a passthrough gate.
2026-07-22enhance: layered (composable) filter toggles, highlighted rows, side panel ↵Paul Buetow
next to image Filters are now independent toggles that compose: the active set is a bitmask (u_enhance_mask) and the enabled GEGL ops are chained in array order (enhancer_apply_chain / enhancer_export_chain). Press 1-8 to toggle any preset on/off at any time; 0 / the Original row clears all; the live preview re-applies the whole stack each time, so you can add/remove filters in any order. The window title lists the enabled presets. The side panel now sits NEXT TO the image (a horizontal content box: [panel] + image area) instead of over it; in fullscreen it reparents to an overlay over the image so fullscreen stays full. Enabled rows are highlighted (.ggaze-enhance-on: accent background, white bold). enhancer.c: extract _op_for_builtin, add enhancer_apply_chain and enhancer_export_chain (single-preset apply/export kept for the tests); _save_buffer factored out (ju0/ku0 fixes unchanged). New test /enhancer/apply_chain (compose + empty-mask rejection).
2026-07-21enhance: replace the popup with a toggleable side panel + per-preset hotkeysPaul Buetow
Instead of a transient 'a' popup, add a persistent enhance side panel (GtkRevealer overlaid on the left of the main content) toggled by 'a'. It lists every preset as a button showing its hotkey (1..8) plus a '0 Original' row, and stays open while you compare presets. Each optimization now has a dedicated global hotkey: 1-8 apply preset 1-8 directly (win.enhance-1 .. win.enhance-8), whether the panel is visible or not. 'a' toggles the panel; 's' still saves the enhanced copy; the panel's '0 Original' row (and navigating away) reverts. Adds win.enhance-1..8 actions (one _action_enhance_n handler parses the index from the action name) and the 1-8 keybindings; the ? overlay Enhance group documents the panel + 1-8 + s. test_shortcut full-table updated (33 rows, 29 actions).
2026-07-21enhance: 'a' opens a preset menu with number hotkeys (1-9, 0) instead of cyclingPaul Buetow
Replace the a=cycle behavior with a GtkPopover listing the presets as rows (1 Auto-fix, 2 Brightness, ... 8 Denoise, 0 Original). Click a row or press its digit to apply that preset as the live preview; Escape or 'a' again closes. Refactored the apply logic into _apply_enhance(idx) shared by the menu and (unchanged) win.enhance-save. The popover is parented to the overlay and torn down on the 'closed' signal. The ? overlay Enhance group now documents the menu.
2026-07-21enhance: wire up GEGL image filters (a cycle / s save) + fix export bugsPaul Buetow
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.
2026-07-20grid: j/k move the cursor down/up one row; stop blanking thumbnails on navPaul Buetow
_on_nav_changed rebuilt every cell on every navigator "changed" signal, so a single h/l/j/k press destroyed and recreated all 613 GtkPicture cells (each re-requesting its thumbnail) and the grid flashed blank. Now it only does a full ggaze_grid_refresh when the file count actually changed (rescan/trash/move); a mere current/mark change just refreshes mark badges in place and re-selects the cursor. Add j/k row navigation: ggaze_grid_move_cursor finds the cell in the adjacent row closest to the current column via gtk_widget_compute_bounds and sets navigator.current to it (so the header / large-view preview track the move and the light nav-changed path re-selects it). h/l stay linear via the existing global win.next/prev. Also document j/k in the ? shortcuts overlay.
2026-07-20shortcuts: fix the GtkBuilder UI XML so ? doesn't crash on startupPaul Buetow
The inline SHORTCUTS_UI string had mismatched closing tags (extra </child> elements), so gtk_builder_new_from_string failed with "Element child was closed, but the currently open element is object" and aborted the process the moment ? was pressed. Regenerate the UI with correct <object>/<child> nesting (validated with xmllint).
2026-07-20grid: mark multi-selection (v / Ctrl+a / Esc) and a ? shortcuts overlayPaul Buetow
The navigator already had a mark API and the grid rendered mark badges, but nothing could set marks — so multi-file ops (D, Ctrl+c, m) only ever acted on the single current image. Wire up marking in the UI: - v toggles a mark on the highlighted grid cell (or the current image in large view). navigator_toggle_mark does not emit "changed", so the cell's badge is updated in place via ggaze_grid_update_mark_badge (no reflow / re-decode). - Ctrl+a marks all; Esc now clears marks first (contextual), then keeps its existing back/quit behavior. - The window title appends "N marked" so multi-selection is visible. - A GtkCssProvider styles .ggaze-marked (accent border + tint) — the class was set but had no styling, so badges were invisible. Also add a ? -> win.shortcuts action that presents a GtkShortcutsWindow (grouped: navigation, view, selection, files, zoom) built from an inline GtkBuilder UI string, transient to the window. tests/test_shortcut.c full-table test updated for the three new bindings (v, Ctrl+a, ?) -> 23 rows, 19 actions.
2026-07-20thumbnail: bound the decode thread pool so large folders don't peg the CPUPaul Buetow
thumbnail_get_async used g_task_run_in_thread, which runs on GLib's unlimited shared thread pool — every visible cell's thumbnail decode spawned its own thread, pegging every core (load ~20, 74 running threads on a 613-image folder). Move the decode onto a GThreadPool owned by Thumbnail, capped at max(1, min(num_cpus/2, 4)) threads. g_task_return_* still marshals each result to the main thread, so callbacks are unchanged. Observed: 74 -> 2 active decode threads, load ~20 -> ~5. If the pool failed to create, fall back to g_task_run_in_thread so thumbnails still work (just unbounded) rather than not at all.
2026-07-20window: open directories in the grid view (3u0) and stop late load forcing largePaul Buetow
ggaze_window_open always set the stack to "large" and loaded the first image even for a directory argument, contradicting the documented folder-to-grid behavior. A directory arg now shows the populated thumbnail grid; a file arg still opens large on that image. _show_texture additionally forced the stack to "large" whenever an image finished loading, which yanked a just-opened folder back out of the grid the moment its first image decoded. Drop the forced stack switch — the caller already sets the right view (file-open/toggle/grid-activate set large; directory-open sets grid), so _show_texture only needs to update the viewer's texture.
2026-07-20grid: sync flowbox selection to navigator.current on Enter/t (cu0)Paul Buetow
Arrow-key navigation in the grid moved the GtkFlowBox selection but never updated navigator.current, so Enter (and toggling to large with t) opened the stale current image instead of the highlighted one. _on_flow_key emitted the activate signal directly without syncing, unlike the double-click path (_on_child_activated) which does sync. Add ggaze_grid_sync_current(): reads the selected flowbox child and calls navigator_set_current_file. Call it from _on_flow_key before emitting activate, and from _action_toggle_view's grid→large branch (then load the now-current image). Mirrors the existing double-click sync.
2026-07-20gridview: allocate the flowbox via GtkBinLayout so the grid renders (fixes ↵Paul Buetow
black grid) GgazeGrid is a GtkWidget with a single child (the GtkScrolledWindow holding the GtkFlowBox) but set no layout manager and overrode no measure/size_allocate vfunc, so GTK4 never allocated the child and the grid stayed a 0x0 black area (`Trying to snapshot GtkScrolledWindow without a current allocation`). Add a GtkBinLayout in class_init so the child is sized to fill the grid and the flowbox renders.
2026-07-20runner: pass full expanded command as one sh -c arg (8u0)Paul Buetow
runner_run built "/bin/sh -c " + cmd and ran g_shell_parse_argv on it, which split the command into argv words — sh -c then received only the first word as its script and treated the rest as $0/$1..., silently truncating multi-word commands, pipelines, and redirections. The existing 'true %f' test passed only because true ignores missing args. Build argv directly as {"/bin/sh", "-c", c_cmd, NULL} so sh -c gets the whole expanded command as one script string. %f/%d stay single- quoted via g_shell_quote, so hostile filenames cannot break out of the quotes and inject commands. Also removed dead code in _shell_quote. New observable tests (write to a temp file, read it back): - multi_word: printf 'hello world' (would fail on old code) - pipeline: echo | cat - redirection: echo > file - spaces_in_args: printf 'a b c' - hostile_filename: ';touch sentinel;' name must NOT create the sentinel
2026-07-20clipboard: offer text/uri-list (CRLF) + text/plain via union provider (iu0)Paul Buetow
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).
2026-07-20shortcuts: GLOBAL scope so t/Esc reach window actions (2u0)Paul Buetow
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'.
2026-07-20opener: shell-argv command parsing with %f as one safe arg (hu0)Paul Buetow
_expand_command now parses the command template with g_shell_parse_argv FIRST, then substitutes %f into the already-parsed argv elements, so %f is always exactly one argv value regardless of spaces or shell metacharacters in the path. Previously it split on literal spaces, breaking quoted commands like sh -c '...' and mis-splitting paths with spaces. opener_launch propagates parse errors (returns FALSE, sets GError) instead of launching malformed argv. A non-local GFile (no path) is now rejected with G_IO_ERROR_NOT_SUPPORTED rather than producing an argv with an embedded NULL. Tests: quoted, quoted-with-%f, escaped spaces, option flags, weird filename (quoted + unquoted), malformed (unclosed quote), empty command, direct argv-shape regression (one argv value), and no-local-path rejection.
2026-07-14polish: AppStream, man page, RPM spec, full lifecycle test (yt0)Paul Buetow
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).
2026-07-14gegl: enhancer presets + export, LSAN suppressions (wt0)Paul Buetow
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).
2026-07-14operate: mover, opener, runner, clipboard modules + tests (ut0)Paul Buetow
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.
2026-07-14progressive: libjpeg-turbo two-phase JPEG backend (st0)Paul Buetow
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.
2026-07-13modernfmt: JXL/HEIF/AVIF loader backends, conditional (qt0)Paul Buetow
M5 (task qt0): modern format backends. - src/loader/backends/jxl.c: libjxl decoder → RGBA → GdkMemoryTexture. - src/loader/backends/heif.c: libheif decoder → interleaved RGBA → GdkMemoryTexture (also handles AVIF when libavif is absent). - src/loader/backends/avif.c: libavif decoder (written; auto-disabled when libavif is not installed). - src/loader/loader.c: BACKENDS array conditionally includes the backends via -DHAVE_JXL/HAVE_AVIF/HAVE_HEIF; pixbuf remains last (fallback). - meson.build: conditional backend sources + c_args + extra deps. - tests: assert_unsupports updated to assert NULL only (any error, works with or without the specific backend compiled in). - Minimal build (all disabled) + full build (jxl+heif) both 14/14 green, ASan clean.
2026-07-13fullscreen: info/EXIF, fullscreen, slideshow, Esc back (ot0)Paul Buetow
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.
2026-07-13gridcull: thumbnail TMS cache, grid view, trash, window d/D/u/t (jt0)Paul Buetow
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.
2026-07-13responsive: async loader, texture cache LRU, prefetch, tests (it0)Paul Buetow
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).
2026-07-12walkdir: navigator, shortcuts, window wiring, drop target, tests (ht0)Paul Buetow
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).
2026-07-12ci: install libasan+libubsan in the ASan lane (kt0)Paul Buetow
The asan lane used -Db_sanitize=address,undefined but never installed the sanitizer runtime libs, so it could not link. kt0 (leak check after M1) ran clean locally after installing them: full unit+integration suite under ASan, zero leaks / zero UBSan issues.
2026-07-12showimage: loader, detect, viewer widget, EXIF orientation, tests (gt0)Paul Buetow
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.
2026-07-12add sample images to gitignoresPaul Buetow
2026-07-12skeleton: GtkApplication, window, GSettings schema, desktop, tests (ft0)Paul Buetow
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.
2026-07-12bootstrap: meson build, test harness, CI, conventions, README/AGENTS (et0)Paul Buetow
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.
2026-07-12docs: add implementation plan with two test tracks and leak-check rulesPaul Buetow
2026-07-12Add ggaze implementation plan (docs/)Paul Buetow