summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-12 17:20:15 +0300
committerPaul Buetow <paul@buetow.org>2026-07-12 17:20:15 +0300
commitf2b8ed6d2ea0744cfd1a36a6fbd1763926b873bb (patch)
treec5d9ce57e7faf8801c12ca350f5aa355f0fabff9
parent78fa24a665002c01f0ea7ded8791c7e6af470305 (diff)
docs: add implementation plan with two test tracks and leak-check rules
-rw-r--r--docs/IMPLEMENTATION.md382
1 files changed, 382 insertions, 0 deletions
diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md
new file mode 100644
index 0000000..359511a
--- /dev/null
+++ b/docs/IMPLEMENTATION.md
@@ -0,0 +1,382 @@
+# ggaze — Implementation Plan
+
+The engineering execution layer between the design docs and the first line of
+code. The design is locked in via the decisions log in [PLAN.md](PLAN.md);
+this page says *how* we build it, in what order, and what "done" means.
+
+## Guiding principles
+
+1. **Every milestone ends runnable** (per [roadmap.md](roadmap.md)) — and every
+ commit within a milestone compiles + `meson test` passes.
+2. **Plain-C modules first, GTK shell thin.** Build and unit-test the logic
+ modules before (or alongside) the UI that consumes them, so coverage is
+ natural rather than retrofit.
+3. **Conventions are build-time enforced**, not just documented: a `clang-format`
+ CI gate + an ASan/UBSan build lane are worth more than a doc page.
+4. **Optional features are off by default in CI** (`gegl`, `jxl`, `avif`,
+ `heif`), with one CI lane that builds `gegl=enabled` so it never silently
+ bit-rots.
+
+## Two test tracks (mandatory)
+
+ggaze has **two complementary test tracks**. Both run under `meson test`.
+
+### Unit tests — `tests/test_<module>.c`
+
+- Target the **plain-C modules** (`detect`, `navigator`, `thumbnail`,
+ `trash`, `mover`, `opener`, `runner`, `enhancer`, `info`, `texturecache`,
+ `clipboard` helpers). No GTK display needed.
+- GLib `GTest` framework; per-module `a(ss)`/path/EXIF fixtures.
+- **Coverage gate ≥80%** on these modules (gcov/lcov), flipped to *fail* at
+ M10, *warn* before.
+- Each module lands its test file in the same milestone as the module.
+
+### Integration tests — `tests/integration/test_<flow>.c`
+
+- Exercise **multiple modules together** through the real public API, with a
+ real temp directory and real files (no mocks of `GFile`/`GFileMonitor`). They
+ verify the *contracts* between modules the unit tests can't reach.
+- Use a lightweight **offscreen GTK harness** (`gtk_test_init` +
+ `GtkWindow` offscreen via `GdkSurface`/`gtk_widget_realize` under
+ `GDK_DEBUG=no-grabs`) where a widget is needed; prefer pure-GLib harnesses
+ where one isn't.
+- No coverage gate (they cross module boundaries), but they must be green in CI.
+- Land at the milestone that first makes the flow possible, and grow with it.
+
+#### Planned integration suites (mapped to milestones)
+
+| Suite | Lands at | Verifies |
+|-------|----------|----------|
+| `test_open_and_show.c` | M1 | CLI file arg → window → viewer has a non-null `GdkTexture` of the right size + upright orientation. |
+| `test_walk_folder.c` | M2 | Folder arg → navigator listing; `h`/`l` action changes current; `GFileMonitor` add/delete propagates to navigator; wrap at ends. |
+| `test_responsive_nav.c` | M3 | Rapid `next` ×10: only the last `GdkTexture` is shown (last-write-wins invariant), UI thread not blocked (measured via a main-loop timer). |
+| `test_progressive_jpeg.c` | M6 | A progressive JPEG fires the partial-texture callback at increasing resolution before the final. |
+| `test_grid_cull.c` | M7 | Grid view shows N cells; `d` bins one into `./Trash`, cell dims; `u` restores; counter reflects remaining; `Enter`→large on the right cell. |
+| `test_move_undo.c` | M8 | Mark 3 → `m`→dest2 → files gone from folder, present in dest; `u` moves back; collision suffixing. |
+| `test_runner_rescan.c` | M8 | `!` runs a script that writes a file into the dir; on exit the navigator rescans and the new file appears; injection-guard filename is single-quoted. |
+| `test_enhance_flow.c` | M9 (gated) | `a`→preset applies a preview (texture differs from raw); `s` writes `-enhanced.<ext>` with EXIF Orientation=1 and original byte-identical; navigate-away dirty prompt Save/Discard/Cancel. |
+| `test_clipboard_copy.c` | M8 | `Ctrl+c` with no marks → `image/png` on `GdkClipboard`; with marks → `text/uri-list`; paste back into a fake target. |
+| `test_full_lifecycle.c` | M10 | The elevator-pitch session scripted: open → walk → `i` → `d` ×k → mark → `m`→dest → `e`→program (use `true`) → `!`→script → quit. End-to-end smoke. |
+
+### Test infrastructure (built in Phase 0)
+
+- `tests/meson.build` wires `unit` and `integration` subdirs as separate
+ `meson test` suites (`-t suite:unit`, `-t suite:integration`) so they can be
+ run selectively in CI.
+- `tests/helpers/` — shared helpers: temp-dir factory, fixture locator,
+ offscreen-window builder, fake `GdkClipboard` target, main-loop drain with
+ timeout (prevents a hung test from blocking CI).
+- `tests/fixtures/` — curated images per format + a rotated-EXIF JPEG +
+ progressive JPEG + RAW+JPEG pair + an injection-hostile filename (`;rm -rf /`).
+- CI runs: unit always; integration on the minimal lane; the `gegl` lane adds
+ the gated suites; ASan lane runs both.
+
+---
+
+## Phase 0 — Bootstrap (prerequisite to M0)
+
+One commit. Lays the build/test/convention groundwork before any feature.
+
+**Files**
+- `meson.build` (root) — project `org.buetow.ggaze`, C11,
+ `default_options: warning_level=2`, extra `-Wextra`, ninja. Declare
+ `dependency('gtk4')`, `glib`, `gio`. Feature options `gegl`, `jxl`, `avif`,
+ `heif` (each `auto`).
+- `src/`, `tests/` (+ `tests/integration/`, `tests/helpers/`, `tests/fixtures/`),
+ `data/`, `po/`, `build-aux/` dirs.
+- `.clang-format` matching [coding-conventions.md](coding-conventions.md);
+ `.editorconfig` mirroring it. CI `clang-format --dry-run` gate.
+- `meson.build` test infra: GLib `g_test_init`; `-Db_coverage=true` support; a
+ `coverage` target; separate `unit`/`integration` suites.
+- CI matrix: `{minimal, gegl}` × `{x86_64}`, ASan/UBSan lane on minimal;
+ coverage upload; gate set to *warn* until M10.
+- `LICENSE`.
+- **Top-level `README.md` for humans** — project pitch (the elevator
+ pitch from PLAN.md), a screenshot placeholder, install/run quick-start
+ (`ggaze ~/Downloads/Camera/IMG_0001.jpg`), keybindings cheat-sheet table
+ (the one from ui-and-interactions.md), build/test commands, a "status:
+ planning → skeleton" line, and links into `docs/`. The audience is a
+ Fedora user who finds the repo and wants to know what it is and how to
+ run it — not a contributor-only wall of links.
+- **Top-level `AGENTS.md` for agents** — the machine-facing entry point: how
+ to build/test (`meson setup`, `meson test -t suite:unit` / `suite:integration`,
+ coverage target), the mandatory two test tracks and their locations,
+ convention enforcement (`clang-format`, header guards, `type_new`/
+ `type_delete`), where the design lives (`docs/` index), the module map, the
+ "optional features are off in CI" rule, the single-`GCancellable`/last-
+ write-wins invariant, and a pointer to load the `agent-task-management` +
+ `c-best-practices` + `solid-principles` + `beyond-solid-principles` skills.
+ This is the file an agent in a fresh context reads first.
+
+**Acceptance:** `meson setup build && ninja -C build && meson test -C build`
+green (empty suites); CI builds the matrix; `clang-format --dry-run` clean.
+
+---
+
+## M0 — Skeleton (app + empty window)
+
+**Deliverables**
+- `src/main.c` — GApplication with `G_APPLICATION_HANDLES_OPEN`; local-options
+ for `--version`/`--help`.
+- `src/app.c/.h` — owns `GtkApplication`, single-instance (decision #32);
+ `open` handler: file → parent dir + that file current; folder → grid.
+- `src/window.c/.h` — `GgazeWindow : GtkApplicationWindow` (`G_DEFINE_TYPE`);
+ empty `GtkStack` (`grid`, `large` children); `AdwHeaderBar` (decision #29).
+- `data/org.buetow.ggaze.desktop` stub (`image/*` handler).
+- `data/org.buetow.ggaze.gschema.xml` — **all keys now** with defaults so
+ modules read them as they land.
+
+**Tests**
+- Unit: `test_app.c` — `--version`/`--help`/unknown arg.
+- Smoke: `test_window.c` — offscreen window, stack has two children.
+
+**Acceptance:** `ggaze IMG_0001.jpg` opens an empty window; `--version` works.
+
+---
+
+## M1 — Show one image (zoom/pan)
+
+**Deliverables**
+- `src/loader/loader.c/.h` — `loader_load(GFile*, GCancellable*, GError**) →
+ GdkTexture*`; static backend dispatch; only `pixbuf` registered here.
+- `src/loader/detect.c/.h` — `detect_format(head, len) → GgazeFormat`;
+ magic-byte sniff. Unit-testable.
+- `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`.
+- `src/window.c` — wire `open` → load → `viewer_set_texture`; large view.
+
+**Tests**
+- Unit: `test_detect.c`, `test_loader_pixbuf.c` (incl. rotated-EXIF fixture).
+- Integration: `test_open_and_show.c`.
+
+**Acceptance:** one image shows upright; zoom/pan works; detect+loader ≥80%.
+
+---
+
+## M2 — Walk the directory
+
+**Deliverables**
+- `src/navigator.c/.h` — plain-C; dir listing, MIME filter, sort (name default;
+ time/size stubs), current index, mark set, rescan; hide RAW sidecars by
+ default (decision #33). `GFileMonitor` debounced 250 ms (decision AA);
+ nearest-fallback on current removal.
+- `src/window.c` — `h`/`l`/`←`/`→` → nav → load → viewer; header subtitle
+ `n / total · filename`; single `GCancellable` (architecture invariant);
+ `GtkDropTarget` for file/folder.
+- `src/shortcuts.c/.h` — `GtkShortcut`+`GtkShortcutController`; actions on the
+ window/app; one table all milestones add to.
+
+**Tests**
+- Unit: `test_navigator.c` (the big one — filter, sort, wrap, marks, rescan,
+ nearest-fallback). Target ~90%.
+- Integration: `test_walk_folder.c`.
+
+**Acceptance:** `ggaze dir/` lists; `h`/`l` walks with wrap; drop reloads;
+external `touch` appears in ~250 ms; counter updates.
+
+---
+
+## M3 — Responsive + prefetch
+
+**Deliverables**
+- `loader_load_async` + `_finish` via `GTask`; keep a sync worker for tests.
+- `src/texturecache.c/.h` — bounded LRU (cap 4) of `GFile → GdkTexture`;
+ prefetch next+prev on current change.
+- Window enforces: one `GCancellable`, drop results whose path ≠ current.
+
+**Tests**
+- Unit: `test_texturecache.c` (LRU eviction, cap; `gdk_memory_texture_new`
+ 1×1 so no display).
+- Integration: `test_responsive_nav.c` (last-write-wins + non-blocking UI).
+
+**Acceptance:** rapid `jjjjjj` never blocks; visible texture matches current;
+bounded memory.
+
+---
+
+## M4 — Fullscreen + slideshow + info
+
+**Deliverables**
+- Fullscreen (`f`, auto-hide header); `Esc` contextual back
+ (marks → fs → large → grid → quit).
+- Slideshow (`S`), configurable delay; pause on manual key.
+- `src/info.c/.h` — plain-C EXIF gather via `libexif`; rendered as viewer
+ overlay.
+
+**Tests**
+- Unit: `test_info.c` (EXIF extraction + orientation tag).
+
+**Acceptance:** `f`/`S`/`i` work; EXIF shows; `Esc` chain correct.
+
+---
+
+## M5 — Modern formats
+
+**Deliverables**
+- `src/loader/backends/jxl.c`, `avif.c`, `heif.c` behind meson features;
+ register into the dispatcher.
+- Animated GIF/WebP via `GdkPixbufAnimation` → `GdkPaintable`.
+
+**Tests**
+- Extend `test_detect` + `test_loader_*` per backend, feature-gated.
+
+**Acceptance:** JXL/AVIF/HEIF open when built; minimal build still green.
+
+---
+
+## M6 — Progressive low-res preview
+
+**Deliverables**
+- `src/loader/backends/jpeg.c` (libjpeg-turbo, optional; supersedes pixbuf for
+ JPEG when enabled) — two-phase load emitting partial `GdkTexture`.
+- Generalize `Loader` with a `progress_cb(GdkTexture *partial)`.
+- Viewer accepts progressive replacement.
+
+**Tests**
+- Unit: `test_loader_jpeg.c`.
+- Integration: `test_progressive_jpeg.c`.
+
+**Acceptance:** 40 MP JPEG shows a coarse frame <50 ms, refines to full.
+
+---
+
+## M7 — Thumbnail cache + grid view
+
+**Deliverables**
+- `src/thumbnail.c/.h` — freedesktop TMS `~/.cache/thumbnails/{normal,large}`
+ + custom bucket for 64–512 (decision T); mtime verify; thread-safe worker;
+ `thumbnail_get_async`.
+- `src/gridview.c/.h` — `GgazeGrid` over navigator `GListModel`; lazy cell
+ decode; `+`/`-` resize → `thumbnail-size`; reflow; mark badges; dim
+ 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.
+
+**Tests**
+- Unit: `test_thumbnail.c`, `test_trash.c`.
+- Integration: `test_grid_cull.c`.
+
+**Acceptance:** `ggaze dir/` → grid; thumbnails load async; `Enter`→large;
+`d` dims+advances; `u` restores; `+`/`-` resizes and persists.
+
+---
+
+## M8 — Selection, move, open-external & scripts, clipboard, prefs
+
+**Deliverables**
+- `src/mover.c/.h` — `destinations` `a(ss)`; `mover_move` (`g_file_move` +
+ suffix); undo move-back; acts on marks-or-current.
+- `src/opener.c/.h` — `editors` `a(ss)`; `%f` expand; detached `GSubprocess`;
+ 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).
+- 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).
+- Preferences dialog (`,`): `AdwPreferencesWindow` editing ordered `a(ss)`
+ lists + sort/background/scroll/slideshow/hide-trashed.
+
+**Tests**
+- Unit: `test_mover.c`, `test_opener.c` (`true`/`false` commands, weird
+ filenames), `test_runner.c` (injection guard, exit status), `test_clipboard.c`.
+- Integration: `test_move_undo.c`, `test_runner_rescan.c`, `test_clipboard_copy.c`.
+
+**Acceptance:** full culling workflow works keyboard-only.
+
+---
+
+## M9 — GEGL quick-enhance, crop/straighten/rotate, compare (optional)
+
+**Deliverables**
+- `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).
+- Viewer: preset active → import → `GeglBuffer` → apply → `GdkTexture`; not
+ during scrub (decision #34); "enhanced" badge.
+- Compose order load→enhance→rotate→straighten→crop→export (decision #35).
+ Crop (`c`), straighten (`R`), rotate 90 (`[`/`]`) stack on the preview graph.
+- Dirty flag: navigate/`d`/`D`/`m`/quit with dirty → Save/Discard/Cancel
+ (decisions #34/#18); `s` clears; re-press/Esc discards directly.
+- Hold-`Space` compare (decision #23/#24).
+- ICC decode/export via GEGL/babl (closes open question G).
+- "GEGL not built in" toast when off.
+
+**Tests**
+- Unit: `test_enhancer.c` (gated): each preset dims + non-zero; export file
+ written + orientation=1 + original untouched; rotate-then-crop compose.
+- Integration: `test_enhance_flow.c`.
+
+**Acceptance:** `a` preview; `s` copy; `c`/`R`/`[`/`]`; hold-`Space`; dirty
+prompt; minimal build toasts cleanly.
+
+---
+
+## M10 — Polish & packaging
+
+**Deliverables**
+- AppStream metainfo, app icons (symbolic + full).
+- Fedora RPM spec; optional Flatpak manifest.
+- `ggaze(1)` man page (stub in M0, finalized here).
+- Window geometry persistence (GSettings `window-geometry`).
+- **Coverage gate → fail** at <80% on plain-C modules.
+- **Quality audit:** `auditing-code-quality` skill (C-adapted:
+ c-best-practices + find-code-bugs + solid-principles + beyond-solid-principles),
+ triage via `agent-task-management`, fix all HIGH/MEDIUM.
+- **Keyboard-completeness audit:** visible hotkeys, mnemonics, `?` overlay,
+ full dialog traversal.
+- Empty-`./Trash` menu action.
+- Integration: `test_full_lifecycle.c` (the elevator-pitch session scripted).
+
+**Acceptance:** RPM builds/installs/registers; man page; coverage gate green;
+audit findings resolved; lifecycle integration green.
+
+---
+
+## Cross-cutting
+
+- **Audit cadence:** run the C-adapted `auditing-code-quality` skill at each
+ M-boundary (decision #40); findings → `agent-task-management` tasks.
+- **Conventions:** `clang-format` CI gate + header-guard/`type_new`-`type_delete`
+ pairing checks where feasible.
+- **Fixtures** grow per milestone; one known-good + one known-bad per format.
+- **Dependency risk:** pin libadwaita to a Fedora target; validate each
+ `gegl:op` exists at M9 via a `gegl_operations` introspection test.
+- **Memory:** bounded texture LRU from M3; ASan/UBSan lane in CI.
+- **Memory-leak profiling (mandatory, per feature):** C has no GC, so **after
+ every major feature milestone completes** (M1, M2, M3, M7, M8, M9, M10)
+ run a dedicated leak-profiling pass before starting the next feature:
+ - Build with `-Db_sanitize=address` and `G_DEBUG=gc-friendly`;
+ - Run the full `meson test` suite under ASan (unit **and** integration) and
+ assert zero leak reports for the plain-C modules (`navigator`, `loader`,
+ `detect`, `thumbnail`, `trash`, `mover`, `opener`, `runner`, `enhancer`,
+ `info`, `texturecache`, `clipboard`) — every `type_new` must have a matching
+ `type_delete` and every `GTask`/`GSubprocess`/`GFileMonitor`/`GdkTexture`
+ must be unreffed;
+ - Run a scripted session (the elevator-pitch workflow from PLAN.md) under
+ ASan — open a folder, walk, `d`/`u`, mark, `m`, `e`, `!`, `a`, `s`, quit —
+ and assert no leak at exit;
+ - `valgrind --leak-check=full --error-exitcode=1` on a representative subset
+ where ASan is unavailable (e.g. the gegl lane), as a cross-check;
+ - Any leak found is a **blocker** for progressing to the next milestone — fix
+ it, re-run, then move on. Each leak pass is its own tracked task
+ (`+leakcheck`) that depends on the feature milestone it follows.
+ Rationale: deferring leak hunting to M10 means chasing leaks across the
+ whole codebase at once; catching them right after each feature keeps the
+ cost local and keeps the `_new`/`_delete` discipline honest as the tree grows.
+
+## Suggested execution order
+
+Phase 0 → M0 → M1 → M2 → M3 → **M7** → M4 → M5 → M6 → M8 → M9 → M10.
+
+Pulling **M7 before M4/M5**: the grid is the core differentiator and depends
+only on navigator + thumbnail + trash — the highest-value plain-C modules.
+Getting them + tests in early maximizes coverage return and de-risks the most
+visible feature. Fullscreen/slideshow/modern-formats are polish relative to
+"flip through a folder and cull." \ No newline at end of file